@desplega.ai/agent-swarm 1.103.0 → 1.104.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/README.md +2 -0
- package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
- package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
- package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
- package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
- package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
- package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
- package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
- package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
- package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
- package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
- package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
- package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
- package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
- package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
- package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
- package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
- package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
- package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
- package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
- package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
- package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
- package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
- package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
- package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
- package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
- package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
- package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
- package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
- package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
- package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
- package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
- package/dist/cli.js +10 -10
- package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
- package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
- package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
- package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
- package/dist/http-59ygpn2h.js +17263 -0
- package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
- package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
- package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
- package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
- package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
- package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
- package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
- package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
- package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
- package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
- package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
- package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
- package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
- package/openapi.json +90 -4
- package/package.json +3 -3
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/providers/sqlite-store.ts +468 -71
- package/src/be/memory/raters/retrieval.ts +4 -2
- package/src/be/memory/reranker.ts +5 -2
- package/src/be/memory/retrieval-store.ts +2 -0
- package/src/be/memory/types.ts +46 -0
- package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
- package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
- package/src/be/migrations/101_script_connections.sql +67 -0
- package/src/be/script-connections.ts +744 -0
- package/src/be/script-credential-broker.ts +38 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
- package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
- package/src/commands/runner.ts +8 -6
- package/src/heartbeat/heartbeat.ts +42 -1
- package/src/heartbeat/index.ts +7 -1
- package/src/http/memory.ts +127 -14
- package/src/http/scripts.ts +15 -14
- package/src/scripts-runtime/api-client.ts +100 -0
- package/src/scripts-runtime/api-types.ts +45 -0
- package/src/scripts-runtime/credential-broker/README.md +40 -0
- package/src/scripts-runtime/credential-broker/broker.ts +51 -0
- package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
- package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
- package/src/scripts-runtime/credential-broker/index.ts +17 -0
- package/src/scripts-runtime/credential-broker/store.ts +72 -0
- package/src/scripts-runtime/credential-broker/types.ts +54 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/egress-secrets.ts +14 -76
- package/src/scripts-runtime/eval-harness.ts +1 -1
- package/src/scripts-runtime/executors/types.ts +5 -5
- package/src/scripts-runtime/loader.ts +6 -1
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +11 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
- package/src/server.ts +6 -0
- package/src/tests/credential-broker.test.ts +281 -0
- package/src/tests/heartbeat.test.ts +239 -0
- package/src/tests/memory-edit.test.ts +177 -0
- package/src/tests/memory-hybrid.test.ts +212 -0
- package/src/tests/runner-repo-autostash.test.ts +39 -8
- package/src/tests/script-connections.test.ts +360 -0
- package/src/tests/scripts-http.test.ts +16 -10
- package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
- package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
- package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
- package/src/tools/credential-bindings/index.ts +1 -0
- package/src/tools/credential-bindings/tool.ts +238 -0
- package/src/tools/memory-edit.ts +148 -0
- package/src/tools/memory-search.ts +17 -10
- package/src/tools/schedules/delete-schedule.ts +18 -8
- package/src/tools/schedules/update-schedule.ts +4 -9
- package/src/tools/script-connections/index.ts +1 -0
- package/src/tools/script-connections/tool.ts +167 -0
- package/src/tools/swarm-config/delete-config.ts +5 -0
- package/src/tools/swarm-config/set-config.ts +23 -1
- package/src/tools/tool-config.ts +6 -1
- package/src/types.ts +6 -0
- package/src/workflows/executors/swarm-script.ts +2 -0
- package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
- package/templates/community/dora-metrics/README.md +39 -0
- package/templates/community/dora-metrics/lead-prompt.md +56 -0
- package/templates/community/dora-metrics/report.mjs +393 -0
- package/templates/community/dora-metrics/run.sh +113 -0
- package/templates/schedules/weekly-dora-metrics/config.json +13 -0
- package/templates/schedules/weekly-dora-metrics/content.md +61 -0
- package/dist/cli-2307phk1.js +0 -24223
- package/dist/cli-2deg0cen.js +0 -120
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CredentialBroker,
|
|
3
|
+
DEFAULT_CREDENTIAL_BINDINGS,
|
|
4
|
+
SwarmConfigCredentialBindingStore,
|
|
5
|
+
} from "@/scripts-runtime/credential-broker";
|
|
6
|
+
import type { EgressSecretEntry } from "@/scripts-runtime/executors/types";
|
|
7
|
+
import { registerVolatileSecret } from "@/utils/secret-scrubber";
|
|
8
|
+
import { getResolvedConfig, getSwarmConfigs } from "./db";
|
|
9
|
+
import { listRelationalCredentialBindings } from "./script-connections";
|
|
10
|
+
|
|
11
|
+
class RelationalCredentialBindingStore extends SwarmConfigCredentialBindingStore {
|
|
12
|
+
override listActiveBindings(
|
|
13
|
+
context: Parameters<SwarmConfigCredentialBindingStore["listActiveBindings"]>[0],
|
|
14
|
+
) {
|
|
15
|
+
const relational = listRelationalCredentialBindings(context);
|
|
16
|
+
if (relational.length > 0) return relational;
|
|
17
|
+
return super.listActiveBindings(context);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildScriptCredentialBindings(input: {
|
|
22
|
+
agentId?: string;
|
|
23
|
+
repoId?: string;
|
|
24
|
+
}): EgressSecretEntry[] {
|
|
25
|
+
const resolvedConfigs = getResolvedConfig(input.agentId, input.repoId);
|
|
26
|
+
const configMap = new Map(resolvedConfigs.map((config) => [config.key, config.value]));
|
|
27
|
+
const broker = new CredentialBroker(
|
|
28
|
+
new RelationalCredentialBindingStore((filters) => getSwarmConfigs(filters)),
|
|
29
|
+
(configKey) => configMap.get(configKey) ?? process.env[configKey],
|
|
30
|
+
DEFAULT_CREDENTIAL_BINDINGS,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const bindings = broker.resolveBindings({ agentId: input.agentId, repoId: input.repoId });
|
|
34
|
+
for (const binding of bindings) {
|
|
35
|
+
registerVolatileSecret(binding.value, binding.configKey);
|
|
36
|
+
}
|
|
37
|
+
return bindings;
|
|
38
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
+
import { getScriptApiTypes } from "@/be/script-connections";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Structured diagnostic record returned to API callers when typecheck fails.
|
|
@@ -108,6 +109,17 @@ export interface SwarmSdk {
|
|
|
108
109
|
|
|
109
110
|
// --- write: memory ---
|
|
110
111
|
memory_delete(args: { id: string }): Promise<unknown>;
|
|
112
|
+
memory_edit(args: {
|
|
113
|
+
memoryId?: string;
|
|
114
|
+
key?: string;
|
|
115
|
+
scope?: "agent" | "swarm";
|
|
116
|
+
mode?: "replace" | "exact";
|
|
117
|
+
content?: string;
|
|
118
|
+
oldString?: string;
|
|
119
|
+
newString?: string;
|
|
120
|
+
intent: string;
|
|
121
|
+
expectedVersion?: number;
|
|
122
|
+
}): Promise<unknown>;
|
|
111
123
|
inject_learning(args: { content: string; name?: string; scope?: "agent" | "swarm"; source?: string; tags?: string[] }): Promise<unknown>;
|
|
112
124
|
|
|
113
125
|
// --- write: tasks ---
|
|
@@ -261,6 +273,7 @@ export interface ScriptContext {
|
|
|
261
273
|
run?: ScriptRunContext;
|
|
262
274
|
step?: ScriptWorkflowSteps;
|
|
263
275
|
swarm: SwarmSdk & { config: SwarmConfig };
|
|
276
|
+
api: ScriptApiRegistry;
|
|
264
277
|
stdlib: ScriptStdlib;
|
|
265
278
|
logger: ScriptLogger;
|
|
266
279
|
}
|
|
@@ -269,6 +282,10 @@ export interface ScriptContext {
|
|
|
269
282
|
export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
|
|
270
283
|
`;
|
|
271
284
|
|
|
285
|
+
export function scriptSdkTypesWithGeneratedApis(apiTypes = getScriptApiTypes()): string {
|
|
286
|
+
return `${SCRIPT_SDK_TYPES}\n${apiTypes}\n`;
|
|
287
|
+
}
|
|
288
|
+
|
|
272
289
|
export const SCRIPT_STDLIB_TYPES = `
|
|
273
290
|
declare module "stdlib" {
|
|
274
291
|
export interface Redacted<T> {
|
|
@@ -764,7 +781,10 @@ function toStructured(diag: ts.Diagnostic): ScriptDiagnostic {
|
|
|
764
781
|
};
|
|
765
782
|
}
|
|
766
783
|
|
|
767
|
-
export function typecheckScript(
|
|
784
|
+
export function typecheckScript(
|
|
785
|
+
source: string,
|
|
786
|
+
context: { agentId?: string; repoId?: string } = {},
|
|
787
|
+
): ScriptTypecheckResult {
|
|
768
788
|
const options: ts.CompilerOptions = {
|
|
769
789
|
allowImportingTsExtensions: true,
|
|
770
790
|
lib: ["lib.es2022.d.ts"],
|
|
@@ -777,9 +797,10 @@ export function typecheckScript(source: string): ScriptTypecheckResult {
|
|
|
777
797
|
types: [],
|
|
778
798
|
};
|
|
779
799
|
|
|
800
|
+
const sdkTypes = scriptSdkTypesWithGeneratedApis(getScriptApiTypes(context));
|
|
780
801
|
const files = new Map<string, string>([
|
|
781
802
|
[USER_FILE, source],
|
|
782
|
-
[SDK_FILE,
|
|
803
|
+
[SDK_FILE, sdkTypes],
|
|
783
804
|
[STDLIB_FILE, SCRIPT_STDLIB_TYPES],
|
|
784
805
|
[RUNTIME_GLOBALS_FILE, SCRIPT_RUNTIME_GLOBALS],
|
|
785
806
|
[
|
|
@@ -138,20 +138,32 @@ export default async function bootTriage(args: any, ctx: any) {
|
|
|
138
138
|
.filter((row) => !BENIGN_FAILURE_RE.test(String(row.failureReason || "")))
|
|
139
139
|
.map(summarizeTask);
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
// Catches in_progress tasks on offline agents OR tasks whose session is a
|
|
142
|
+
// pre-boot artifact (stale lastHeartbeatAt). The session check mirrors the
|
|
143
|
+
// heartbeat.ts runRebootSweep boot-epoch logic — a session that heartbeated
|
|
144
|
+
// after the stuckMinutes threshold is considered live.
|
|
145
|
+
const recentThreshold = new Date(nowMs - stuckMinutes * 60 * 1000).toISOString();
|
|
146
|
+
const stuckRows = await query(
|
|
142
147
|
ctx,
|
|
143
148
|
`SELECT t.id, t.task, t.status, t.taskType, t.agentId, a.name as agentName,
|
|
144
149
|
t.scheduleId, t.parentTaskId, t.failureReason, t.createdAt, t.lastUpdatedAt
|
|
145
150
|
FROM agent_tasks t
|
|
146
151
|
JOIN agents a ON a.id = t.agentId
|
|
147
152
|
WHERE t.status = 'in_progress'
|
|
148
|
-
AND a.status = 'offline'
|
|
149
153
|
AND datetime(t.lastUpdatedAt) <= datetime(?, ?)
|
|
154
|
+
AND (
|
|
155
|
+
a.status = 'offline'
|
|
156
|
+
OR NOT EXISTS (
|
|
157
|
+
SELECT 1 FROM active_sessions s
|
|
158
|
+
WHERE s.taskId = t.id
|
|
159
|
+
AND datetime(s.lastHeartbeatAt) >= datetime(?)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
150
162
|
ORDER BY datetime(t.lastUpdatedAt) ASC
|
|
151
163
|
LIMIT 50`,
|
|
152
|
-
[now.toISOString(), `-${stuckMinutes} minutes
|
|
164
|
+
[now.toISOString(), `-${stuckMinutes} minutes`, recentThreshold],
|
|
153
165
|
);
|
|
154
|
-
const stuckInProgressOnOfflineAgents =
|
|
166
|
+
const stuckInProgressOnOfflineAgents = stuckRows
|
|
155
167
|
.filter((row) => !row.unavailable)
|
|
156
168
|
.map(summarizeTask);
|
|
157
169
|
|
|
@@ -138,20 +138,32 @@ export default async function bootTriage(args: any, ctx: any) {
|
|
|
138
138
|
.filter((row) => !BENIGN_FAILURE_RE.test(String(row.failureReason || "")))
|
|
139
139
|
.map(summarizeTask);
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
// Catches in_progress tasks on offline agents OR tasks whose session is a
|
|
142
|
+
// pre-boot artifact (stale lastHeartbeatAt). The session check mirrors the
|
|
143
|
+
// heartbeat.ts runRebootSweep boot-epoch logic — a session that heartbeated
|
|
144
|
+
// after the stuckMinutes threshold is considered live.
|
|
145
|
+
const recentThreshold = new Date(nowMs - stuckMinutes * 60 * 1000).toISOString();
|
|
146
|
+
const stuckRows = await query(
|
|
142
147
|
ctx,
|
|
143
148
|
`SELECT t.id, t.task, t.status, t.taskType, t.agentId, a.name as agentName,
|
|
144
149
|
t.scheduleId, t.parentTaskId, t.failureReason, t.createdAt, t.lastUpdatedAt
|
|
145
150
|
FROM agent_tasks t
|
|
146
151
|
JOIN agents a ON a.id = t.agentId
|
|
147
152
|
WHERE t.status = 'in_progress'
|
|
148
|
-
AND a.status = 'offline'
|
|
149
153
|
AND datetime(t.lastUpdatedAt) <= datetime(?, ?)
|
|
154
|
+
AND (
|
|
155
|
+
a.status = 'offline'
|
|
156
|
+
OR NOT EXISTS (
|
|
157
|
+
SELECT 1 FROM active_sessions s
|
|
158
|
+
WHERE s.taskId = t.id
|
|
159
|
+
AND datetime(s.lastHeartbeatAt) >= datetime(?)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
150
162
|
ORDER BY datetime(t.lastUpdatedAt) ASC
|
|
151
163
|
LIMIT 50`,
|
|
152
|
-
[now.toISOString(), `-${stuckMinutes} minutes
|
|
164
|
+
[now.toISOString(), `-${stuckMinutes} minutes`, recentThreshold],
|
|
153
165
|
);
|
|
154
|
-
const stuckInProgressOnOfflineAgents =
|
|
166
|
+
const stuckInProgressOnOfflineAgents = stuckRows
|
|
155
167
|
.filter((row) => !row.unavailable)
|
|
156
168
|
.map(summarizeTask);
|
|
157
169
|
|
package/src/commands/runner.ts
CHANGED
|
@@ -171,7 +171,8 @@ export type SwarmAutostash = { ref: string; message: string };
|
|
|
171
171
|
|
|
172
172
|
async function listSwarmAutostashes(clonePath: string, role: string): Promise<SwarmAutostash[]> {
|
|
173
173
|
try {
|
|
174
|
-
const result =
|
|
174
|
+
const result =
|
|
175
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} stash list --format=%gd%x09%s`.quiet();
|
|
175
176
|
return result
|
|
176
177
|
.text()
|
|
177
178
|
.split("\n")
|
|
@@ -195,7 +196,8 @@ async function refreshExistingRepoForTask(
|
|
|
195
196
|
role: string,
|
|
196
197
|
): Promise<string | null> {
|
|
197
198
|
const { name, clonePath, defaultBranch } = repoConfig;
|
|
198
|
-
const statusResult =
|
|
199
|
+
const statusResult =
|
|
200
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} status --porcelain`.quiet();
|
|
199
201
|
const statusOutput = statusResult.text().trim();
|
|
200
202
|
let stashMessage: string | null = null;
|
|
201
203
|
|
|
@@ -203,7 +205,7 @@ async function refreshExistingRepoForTask(
|
|
|
203
205
|
stashMessage = `swarm-autostash ${defaultBranch} ${new Date().toISOString()}`;
|
|
204
206
|
try {
|
|
205
207
|
console.log(`[${role}] Auto-stashing pending work in ${name}: ${stashMessage}`);
|
|
206
|
-
await Bun.$`
|
|
208
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} stash push --include-untracked -m ${stashMessage}`.quiet();
|
|
207
209
|
console.log(`[${role}] Auto-stashed pending work in ${name}`);
|
|
208
210
|
} catch (err) {
|
|
209
211
|
const errorMsg = scrubSecrets((err as Error).message);
|
|
@@ -216,15 +218,15 @@ async function refreshExistingRepoForTask(
|
|
|
216
218
|
console.log(`[${role}] Refreshing ${name} from origin/${defaultBranch}...`);
|
|
217
219
|
const fetchSpec = `${defaultBranch}:refs/remotes/origin/${defaultBranch}`;
|
|
218
220
|
const remoteRef = `refs/remotes/origin/${defaultBranch}`;
|
|
219
|
-
await Bun.$`
|
|
220
|
-
await Bun.$`
|
|
221
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} fetch origin ${fetchSpec}`.quiet();
|
|
222
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} merge --no-edit --no-stat ${remoteRef}`.quiet();
|
|
221
223
|
console.log(`[${role}] Refreshed ${name}`);
|
|
222
224
|
return null;
|
|
223
225
|
} catch (err) {
|
|
224
226
|
const errorMsg = scrubSecrets((err as Error).message);
|
|
225
227
|
console.warn(`[${role}] Could not refresh ${name}: ${errorMsg}`);
|
|
226
228
|
try {
|
|
227
|
-
await Bun.$`
|
|
229
|
+
await Bun.$`env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX git -C ${clonePath} merge --abort`.quiet();
|
|
228
230
|
} catch {
|
|
229
231
|
// No merge in progress, or abort failed. The original refresh warning is
|
|
230
232
|
// the actionable signal; repo setup remains best-effort.
|
|
@@ -450,9 +450,29 @@ function remediateCrashedWorkerTask(
|
|
|
450
450
|
if (remaining === 0) updateAgentStatus(task.agentId, "idle");
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Parse the API boot epoch from `globalThis.__runId` (format: `run_<epochMs>`).
|
|
455
|
+
* Returns the epoch in ms, or null if missing/unparseable.
|
|
456
|
+
*/
|
|
457
|
+
export function getBootEpochMs(): number | null {
|
|
458
|
+
const gs = globalThis as typeof globalThis & { __runId?: string };
|
|
459
|
+
const runId = gs.__runId;
|
|
460
|
+
if (!runId || typeof runId !== "string") return null;
|
|
461
|
+
const match = runId.match(/^run_(\d+)$/);
|
|
462
|
+
if (!match) return null;
|
|
463
|
+
const epoch = Number(match[1]);
|
|
464
|
+
return Number.isFinite(epoch) ? epoch : null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// API and workers share the same host clock, so skew is minimal.
|
|
468
|
+
// 5s tolerance errs toward NOT failing a borderline-live session.
|
|
469
|
+
const BOOT_EPOCH_SKEW_MS = 5_000;
|
|
470
|
+
|
|
453
471
|
/**
|
|
454
472
|
* Aggressive sweep that runs once after server restart.
|
|
455
473
|
* Ignores age thresholds — any in_progress task with no active session is auto-failed.
|
|
474
|
+
* A session is only considered "live" if it heartbeated AFTER the current API boot
|
|
475
|
+
* (concurrency-safe: workers with multiple tasks keep fresh heartbeats on live ones).
|
|
456
476
|
* Creates exactly one retry task per failed task via parentTaskId.
|
|
457
477
|
*/
|
|
458
478
|
export async function runRebootSweep(): Promise<void> {
|
|
@@ -474,6 +494,13 @@ export async function runRebootSweep(): Promise<void> {
|
|
|
474
494
|
}
|
|
475
495
|
const reason = "Auto-failed by reboot sweep: worker session not found after server restart";
|
|
476
496
|
|
|
497
|
+
const bootEpoch = getBootEpochMs();
|
|
498
|
+
if (bootEpoch === null) {
|
|
499
|
+
console.warn(
|
|
500
|
+
"[Heartbeat] Reboot sweep: could not parse boot epoch from __runId — falling back to legacy session-exists check",
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
477
504
|
for (const task of allInProgress) {
|
|
478
505
|
if (!task.agentId) {
|
|
479
506
|
console.warn(
|
|
@@ -483,7 +510,21 @@ export async function runRebootSweep(): Promise<void> {
|
|
|
483
510
|
}
|
|
484
511
|
|
|
485
512
|
const session = getActiveSessionForTask(task.id);
|
|
486
|
-
if (session)
|
|
513
|
+
if (session) {
|
|
514
|
+
if (bootEpoch === null) {
|
|
515
|
+
// Legacy fallback: session exists → skip (pre-fix behavior, never more aggressive)
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
const sessionLastSeen = new Date(session.lastHeartbeatAt).getTime();
|
|
519
|
+
if (sessionLastSeen >= bootEpoch - BOOT_EPOCH_SKEW_MS) {
|
|
520
|
+
// Heartbeated after (or within skew of) this boot → genuinely live, skip
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
// Pre-boot stale session → fall through to auto-fail + reboot-retry child
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Clean up pre-boot stale session before failing (if it existed)
|
|
527
|
+
if (session) deleteActiveSession(task.id);
|
|
487
528
|
|
|
488
529
|
// Auto-fail the task
|
|
489
530
|
const failed = failTask(task.id, reason);
|
package/src/heartbeat/index.ts
CHANGED
package/src/http/memory.ts
CHANGED
|
@@ -72,6 +72,33 @@ const searchMemory = route({
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
const editMemory = route({
|
|
76
|
+
method: "post",
|
|
77
|
+
path: "/api/memory/edit",
|
|
78
|
+
pattern: ["api", "memory", "edit"],
|
|
79
|
+
summary:
|
|
80
|
+
"Edit a single memory in place while preserving its ID and usefulness posterior. Modes: 'replace' overwrites entire content; 'exact' performs surgical find-and-replace of oldString→newString (fails if missing or ambiguous)",
|
|
81
|
+
tags: ["Memory"],
|
|
82
|
+
auth: { apiKey: true, agentId: true },
|
|
83
|
+
body: z.object({
|
|
84
|
+
memoryId: z.string().uuid().optional(),
|
|
85
|
+
key: z.string().min(1).optional(),
|
|
86
|
+
scope: AgentMemoryScopeSchema.optional(),
|
|
87
|
+
mode: z.enum(["replace", "exact"]).default("replace"),
|
|
88
|
+
content: z.string().min(1).optional(),
|
|
89
|
+
oldString: z.string().min(1).optional(),
|
|
90
|
+
newString: z.string().optional(),
|
|
91
|
+
intent: z.string().min(1),
|
|
92
|
+
expectedVersion: z.number().int().min(1).optional(),
|
|
93
|
+
}),
|
|
94
|
+
responses: {
|
|
95
|
+
200: { description: "Memory edited" },
|
|
96
|
+
400: { description: "Validation error" },
|
|
97
|
+
404: { description: "Memory not found" },
|
|
98
|
+
409: { description: "Version conflict" },
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
75
102
|
const reEmbedMemory = route({
|
|
76
103
|
method: "post",
|
|
77
104
|
path: "/api/memory/re-embed",
|
|
@@ -319,7 +346,39 @@ export async function handleMemory(
|
|
|
319
346
|
const store = getMemoryStore();
|
|
320
347
|
const provider = getEmbeddingProvider();
|
|
321
348
|
|
|
322
|
-
|
|
349
|
+
if (sourcePath && agentId && contentChunks.length === 1) {
|
|
350
|
+
const existing = store
|
|
351
|
+
.list(agentId, {
|
|
352
|
+
scope,
|
|
353
|
+
limit: 2,
|
|
354
|
+
ownerAgentId: agentId,
|
|
355
|
+
sourcePath,
|
|
356
|
+
})
|
|
357
|
+
.filter((memory) => memory.sourcePath === sourcePath);
|
|
358
|
+
if (existing.length === 1 && existing[0]?.totalChunks === 1) {
|
|
359
|
+
const result = store.edit({
|
|
360
|
+
id: existing[0].id,
|
|
361
|
+
mode: "replace",
|
|
362
|
+
content: contentChunks[0]!.content,
|
|
363
|
+
intent: "re-index memory source path",
|
|
364
|
+
changedByAgentId: agentId,
|
|
365
|
+
});
|
|
366
|
+
const embedding = await provider.embed(contentChunks[0]!.content);
|
|
367
|
+
if (embedding) store.updateEmbedding(result.memory.id, embedding, provider.name);
|
|
368
|
+
try {
|
|
369
|
+
storeLinks(result.memory.id, agentId, result.memory.content);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
console.error(
|
|
372
|
+
`[memory] Link resolution failed for ${result.memory.id}:`,
|
|
373
|
+
(err as Error).message,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
json(res, { queued: false, memoryIds: [result.memory.id], edited: result.changed }, 202);
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Dedup multi-chunk or ambiguous source paths via the existing lossy path.
|
|
323
382
|
if (sourcePath && agentId) {
|
|
324
383
|
store.deleteBySourcePath(sourcePath, agentId);
|
|
325
384
|
}
|
|
@@ -345,6 +404,8 @@ export async function handleMemory(
|
|
|
345
404
|
totalChunks: chunk.totalChunks,
|
|
346
405
|
tags: tags || [],
|
|
347
406
|
contextKey: resolvedContextKey ?? null,
|
|
407
|
+
intent: "index memory content",
|
|
408
|
+
key: sourcePath || null,
|
|
348
409
|
})),
|
|
349
410
|
);
|
|
350
411
|
|
|
@@ -396,17 +457,13 @@ export async function handleMemory(
|
|
|
396
457
|
const store = getMemoryStore();
|
|
397
458
|
const queryEmbedding = await provider.embed(query);
|
|
398
459
|
|
|
399
|
-
if (!queryEmbedding) {
|
|
400
|
-
json(res, { results: [] });
|
|
401
|
-
return true;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
460
|
const candidateLimit = Math.min(limit, 20) * CANDIDATE_SET_MULTIPLIER;
|
|
405
|
-
const candidates = store.search(queryEmbedding, myAgentId, {
|
|
461
|
+
const candidates = store.search(queryEmbedding ?? new Float32Array(0), myAgentId, {
|
|
406
462
|
scope,
|
|
407
463
|
limit: candidateLimit,
|
|
408
464
|
source,
|
|
409
465
|
isLead: false,
|
|
466
|
+
queryText: query,
|
|
410
467
|
});
|
|
411
468
|
const ranked = rerank(candidates, { limit: Math.min(limit, 20) });
|
|
412
469
|
|
|
@@ -426,7 +483,11 @@ export async function handleMemory(
|
|
|
426
483
|
recordRetrievals(
|
|
427
484
|
sourceTaskId,
|
|
428
485
|
myAgentId,
|
|
429
|
-
ranked.map((r) => ({
|
|
486
|
+
ranked.map((r) => ({
|
|
487
|
+
memoryId: r.id,
|
|
488
|
+
similarity: r.similarity,
|
|
489
|
+
retrievalSource: r.retrievalSource,
|
|
490
|
+
})),
|
|
430
491
|
undefined,
|
|
431
492
|
{ intent, contextKey, eventType: "search" },
|
|
432
493
|
);
|
|
@@ -443,6 +504,7 @@ export async function handleMemory(
|
|
|
443
504
|
similarity: r.similarity,
|
|
444
505
|
rawSimilarity: r.rawSimilarity,
|
|
445
506
|
compositeScore: r.compositeScore,
|
|
507
|
+
retrievalSource: r.retrievalSource,
|
|
446
508
|
source: r.source,
|
|
447
509
|
scope: r.scope,
|
|
448
510
|
})),
|
|
@@ -470,20 +532,16 @@ export async function handleMemory(
|
|
|
470
532
|
const provider = getEmbeddingProvider();
|
|
471
533
|
const queryEmbedding = await provider.embed(query.trim());
|
|
472
534
|
|
|
473
|
-
if (!queryEmbedding) {
|
|
474
|
-
json(res, { results: [], total: 0, limit: pageLimit, offset, mode: "semantic" });
|
|
475
|
-
return true;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
535
|
const candidateLimit = Math.min(
|
|
479
536
|
4096,
|
|
480
537
|
Math.max(offset + pageLimit, pageLimit) * CANDIDATE_SET_MULTIPLIER,
|
|
481
538
|
);
|
|
482
|
-
let candidates = store.search(queryEmbedding, agentId ?? "", {
|
|
539
|
+
let candidates = store.search(queryEmbedding ?? new Float32Array(0), agentId ?? "", {
|
|
483
540
|
scope,
|
|
484
541
|
limit: candidateLimit,
|
|
485
542
|
isLead: true,
|
|
486
543
|
source,
|
|
544
|
+
queryText: query.trim(),
|
|
487
545
|
});
|
|
488
546
|
if (agentId) {
|
|
489
547
|
candidates = candidates.filter((c) => c.agentId === agentId);
|
|
@@ -505,6 +563,7 @@ export async function handleMemory(
|
|
|
505
563
|
similarity: r.similarity,
|
|
506
564
|
rawSimilarity: r.rawSimilarity,
|
|
507
565
|
compositeScore: r.compositeScore,
|
|
566
|
+
retrievalSource: r.retrievalSource,
|
|
508
567
|
createdAt: r.createdAt,
|
|
509
568
|
accessedAt: r.accessedAt,
|
|
510
569
|
accessCount: r.accessCount ?? 0,
|
|
@@ -567,6 +626,60 @@ export async function handleMemory(
|
|
|
567
626
|
return true;
|
|
568
627
|
}
|
|
569
628
|
|
|
629
|
+
if (editMemory.match(req.method, pathSegments)) {
|
|
630
|
+
if (!myAgentId) {
|
|
631
|
+
jsonError(res, "Missing X-Agent-ID header", 400);
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const parsed = await editMemory.parse(req, res, pathSegments, new URLSearchParams());
|
|
636
|
+
if (!parsed) return true;
|
|
637
|
+
|
|
638
|
+
const { memoryId, key, scope, mode, content, oldString, newString, intent, expectedVersion } =
|
|
639
|
+
parsed.body;
|
|
640
|
+
if (!memoryId && !(key && scope)) {
|
|
641
|
+
jsonError(res, "memoryId or key+scope required", 400);
|
|
642
|
+
return true;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const store = getMemoryStore();
|
|
647
|
+
const result = store.edit({
|
|
648
|
+
id: memoryId,
|
|
649
|
+
key,
|
|
650
|
+
scope,
|
|
651
|
+
agentId: myAgentId,
|
|
652
|
+
mode,
|
|
653
|
+
content,
|
|
654
|
+
oldString,
|
|
655
|
+
newString,
|
|
656
|
+
intent,
|
|
657
|
+
expectedVersion,
|
|
658
|
+
changedByAgentId: myAgentId,
|
|
659
|
+
});
|
|
660
|
+
if (result.changed) {
|
|
661
|
+
const provider = getEmbeddingProvider();
|
|
662
|
+
const embedding = await provider.embed(result.memory.content);
|
|
663
|
+
if (embedding) store.updateEmbedding(result.memory.id, embedding, provider.name);
|
|
664
|
+
try {
|
|
665
|
+
storeLinks(result.memory.id, myAgentId, result.memory.content);
|
|
666
|
+
} catch (err) {
|
|
667
|
+
console.error(
|
|
668
|
+
`[memory-edit] Link resolution failed for ${result.memory.id}:`,
|
|
669
|
+
(err as Error).message,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
json(res, result);
|
|
674
|
+
} catch (err) {
|
|
675
|
+
const message = (err as Error).message;
|
|
676
|
+
if (message.includes("not found")) jsonError(res, message, 404);
|
|
677
|
+
else if (message.includes("conflict")) jsonError(res, message, 409);
|
|
678
|
+
else jsonError(res, message, 400);
|
|
679
|
+
}
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
|
|
570
683
|
if (memoryHealth.match(req.method, pathSegments)) {
|
|
571
684
|
const parsed = await memoryHealth.parse(req, res, pathSegments, new URLSearchParams());
|
|
572
685
|
if (!parsed) return true;
|
package/src/http/scripts.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { z } from "zod";
|
|
|
3
3
|
import { resolveHttpAuditUserId } from "../be/audit-user";
|
|
4
4
|
import { getAgentById, recordInlineScriptRun, upsertKv } from "../be/db";
|
|
5
5
|
import { createEvent } from "../be/events";
|
|
6
|
+
import { getScriptApiConnectionDescriptors, getScriptApiTypes } from "../be/script-connections";
|
|
7
|
+
import { buildScriptCredentialBindings } from "../be/script-credential-broker";
|
|
6
8
|
import {
|
|
7
9
|
deleteScript,
|
|
8
10
|
getScript,
|
|
@@ -13,7 +15,11 @@ import {
|
|
|
13
15
|
} from "../be/scripts/db";
|
|
14
16
|
import { searchScripts } from "../be/scripts/embeddings";
|
|
15
17
|
import { extractArgsJsonSchema } from "../be/scripts/extract-schema";
|
|
16
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
SCRIPT_STDLIB_TYPES,
|
|
20
|
+
scriptSdkTypesWithGeneratedApis,
|
|
21
|
+
typecheckScript,
|
|
22
|
+
} from "../be/scripts/typecheck";
|
|
17
23
|
import { extractScriptSignature } from "../scripts-runtime/extract-signature";
|
|
18
24
|
import { runScript } from "../scripts-runtime/loader";
|
|
19
25
|
import {
|
|
@@ -287,12 +293,7 @@ export async function handleScripts(
|
|
|
287
293
|
const agent = requireAgent(res, agentId);
|
|
288
294
|
if (!agent) return true;
|
|
289
295
|
|
|
290
|
-
|
|
291
|
-
jsonError(res, "Global scripts require a lead agent", 403);
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const typecheck = typecheckScript(parsed.body.source);
|
|
296
|
+
const typecheck = typecheckScript(parsed.body.source, { agentId: agent.id });
|
|
296
297
|
if (!typecheck.ok) {
|
|
297
298
|
json(
|
|
298
299
|
res,
|
|
@@ -375,6 +376,8 @@ export async function handleScripts(
|
|
|
375
376
|
args: parsed.body.args,
|
|
376
377
|
fsMode,
|
|
377
378
|
agentId: agent.id,
|
|
379
|
+
egressSecrets: buildScriptCredentialBindings({ agentId: agent.id }),
|
|
380
|
+
apiConnections: getScriptApiConnectionDescriptors({ agentId: agent.id }),
|
|
378
381
|
});
|
|
379
382
|
|
|
380
383
|
// Persist output to KV when idempotencyKey is provided and run succeeded
|
|
@@ -529,7 +532,10 @@ export async function handleScripts(
|
|
|
529
532
|
// Must be matched before getScriptByIdRoute — its ["api", "scripts", null]
|
|
530
533
|
// pattern would otherwise swallow the literal "type-defs" segment.
|
|
531
534
|
if (typeDefsRoute.match(req.method, pathSegments)) {
|
|
532
|
-
json(res, {
|
|
535
|
+
json(res, {
|
|
536
|
+
sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes()),
|
|
537
|
+
stdlibTypes: SCRIPT_STDLIB_TYPES,
|
|
538
|
+
});
|
|
533
539
|
return true;
|
|
534
540
|
}
|
|
535
541
|
|
|
@@ -577,7 +583,7 @@ export async function handleScripts(
|
|
|
577
583
|
json(res, {
|
|
578
584
|
signature: JSON.parse(script.signatureJson),
|
|
579
585
|
argsJsonSchema: script.argsJsonSchema ? (JSON.parse(script.argsJsonSchema) as unknown) : null,
|
|
580
|
-
sdkTypes:
|
|
586
|
+
sdkTypes: scriptSdkTypesWithGeneratedApis(getScriptApiTypes({ agentId: agent.id })),
|
|
581
587
|
stdlibTypes: SCRIPT_STDLIB_TYPES,
|
|
582
588
|
});
|
|
583
589
|
return true;
|
|
@@ -589,11 +595,6 @@ export async function handleScripts(
|
|
|
589
595
|
const agent = requireAgent(res, agentId);
|
|
590
596
|
if (!agent) return true;
|
|
591
597
|
|
|
592
|
-
if (parsed.query.scope === "global" && !agent.isLead) {
|
|
593
|
-
jsonError(res, "Global scripts require a lead agent", 403);
|
|
594
|
-
return true;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
598
|
const deleted = deleteScript({
|
|
598
599
|
name: parsed.params.name,
|
|
599
600
|
scope: parsed.query.scope,
|