@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.
Files changed (122) hide show
  1. package/README.md +2 -0
  2. package/dist/{actions-7txktrdj.js → actions-2expzt47.js} +6 -6
  3. package/dist/{app-xm3xsdye.js → app-dr8hwx7z.js} +3 -3
  4. package/dist/{assistant-qpv63wk6.js → assistant-9rhgsw7v.js} +9 -10
  5. package/dist/{boot-reembed-chxhrwv8.js → boot-reembed-3q1aywnz.js} +5 -4
  6. package/dist/{boot-reembed-9cxqka3e.js → boot-reembed-9kt97g1z.js} +4 -3
  7. package/dist/{boot-scrub-logs-8qtfkcz8.js → boot-scrub-logs-vskq108e.js} +2 -2
  8. package/dist/{cli-fedn86nz.js → cli-0jw6j755.js} +8 -3
  9. package/dist/{cli-yeemwevj.js → cli-23zk7xqy.js} +20 -16
  10. package/dist/{cli-v9283nma.js → cli-2g9jf9ek.js} +1 -1
  11. package/dist/{cli-rvae030h.js → cli-2hap19dv.js} +3 -3
  12. package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
  13. package/dist/{cli-8hyv6c2v.js → cli-54h5daha.js} +41 -23
  14. package/dist/{cli-fygaw191.js → cli-5pf0b84v.js} +2 -2
  15. package/dist/{cli-m8f6qzck.js → cli-7v4y3hyd.js} +1 -1
  16. package/dist/{cli-fwxdt7kt.js → cli-d4z1csms.js} +30 -5
  17. package/dist/{cli-jt9d9fjw.js → cli-e7bwjntj.js} +1 -1
  18. package/dist/{cli-s14sb64w.js → cli-etvgz5x8.js} +5 -7
  19. package/dist/{cli-2qynerth.js → cli-gj2n2ec1.js} +1 -1
  20. package/dist/{cli-dz3d6zjg.js → cli-nzc0h54v.js} +121 -2
  21. package/dist/{cli-8w95v19d.js → cli-p4cj4vhc.js} +351 -24
  22. package/dist/{cli-xmtsan9k.js → cli-q5tshxd6.js} +1 -1
  23. package/dist/{http-zts4haq7.js → cli-qvzgncd8.js} +83966 -76422
  24. package/dist/{cli-4fnp2tc3.js → cli-s42f54ja.js} +1 -1
  25. package/dist/{cli-krvxq8bc.js → cli-t9v14cnd.js} +23 -4
  26. package/dist/{cli-gzepjz54.js → cli-v1syqkcb.js} +900 -352
  27. package/dist/{cli-ksq38x4n.js → cli-w1nfskkg.js} +2 -2
  28. package/dist/{cli-ns0r7mkr.js → cli-y9p3e94h.js} +3 -3
  29. package/dist/{cli-85pf4z38.js → cli-yjt8a36k.js} +3 -3
  30. package/dist/{cli-yfwwjeft.js → cli-yvektv79.js} +1 -1
  31. package/dist/{cli-kk51f3tf.js → cli-zaxksbgj.js} +5 -5
  32. package/dist/{cli-rttgde5f.js → cli-zghef0em.js} +1 -1
  33. package/dist/cli.js +10 -10
  34. package/dist/{commands-aqn63mcw.js → commands-wfrkef57.js} +2 -2
  35. package/dist/{db-azrvwsnj.js → db-c42pxt7h.js} +2 -2
  36. package/dist/{handlers-hf61har3.js → handlers-n36zkfvx.js} +10 -12
  37. package/dist/{hook-306p3yz4.js → hook-gw3vhv6s.js} +7 -5
  38. package/dist/http-59ygpn2h.js +17263 -0
  39. package/dist/{index-h2yqjyk0.js → index-881c1baf.js} +9 -8
  40. package/dist/{index-29vg51x6.js → index-jca7jdpt.js} +23 -10
  41. package/dist/{index-bfh9hgek.js → index-xf6fp4y1.js} +5 -5
  42. package/dist/{index-3c651yfk.js → index-xz8vaa5c.js} +14 -11
  43. package/dist/{keepalive-a0dg5pds.js → keepalive-mj4302vw.js} +4 -4
  44. package/dist/{lead-7mcxedkc.js → lead-qm0xjvkv.js} +19 -19
  45. package/dist/{maintenance-mrzqd53t.js → maintenance-xppxbe4t.js} +5 -4
  46. package/dist/{onboard-4dkpv9wa.js → onboard-ejx6yxh8.js} +2 -2
  47. package/dist/{otel-impl-jt7gpyp9.js → otel-impl-zs5exkpk.js} +1 -1
  48. package/dist/{pricing-refresh-xqnzh8dt.js → pricing-refresh-nqyf8bkb.js} +4 -4
  49. package/dist/{seed-pricing-56tjm1fj.js → seed-pricing-w3x550cj.js} +3 -3
  50. package/dist/{setup-tbwk8j10.js → setup-7729ktfj.js} +2 -2
  51. package/dist/{worker-e03za92t.js → worker-4ttemwhw.js} +19 -19
  52. package/openapi.json +90 -4
  53. package/package.json +3 -3
  54. package/src/be/memory/constants.ts +6 -0
  55. package/src/be/memory/providers/sqlite-store.ts +468 -71
  56. package/src/be/memory/raters/retrieval.ts +4 -2
  57. package/src/be/memory/reranker.ts +5 -2
  58. package/src/be/memory/retrieval-store.ts +2 -0
  59. package/src/be/memory/types.ts +46 -0
  60. package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
  61. package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
  62. package/src/be/migrations/101_script_connections.sql +67 -0
  63. package/src/be/script-connections.ts +744 -0
  64. package/src/be/script-credential-broker.ts +38 -0
  65. package/src/be/scripts/typecheck.ts +23 -2
  66. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
  67. package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
  68. package/src/commands/runner.ts +8 -6
  69. package/src/heartbeat/heartbeat.ts +42 -1
  70. package/src/heartbeat/index.ts +7 -1
  71. package/src/http/memory.ts +127 -14
  72. package/src/http/scripts.ts +15 -14
  73. package/src/scripts-runtime/api-client.ts +100 -0
  74. package/src/scripts-runtime/api-types.ts +45 -0
  75. package/src/scripts-runtime/credential-broker/README.md +40 -0
  76. package/src/scripts-runtime/credential-broker/broker.ts +51 -0
  77. package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
  78. package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
  79. package/src/scripts-runtime/credential-broker/index.ts +17 -0
  80. package/src/scripts-runtime/credential-broker/store.ts +72 -0
  81. package/src/scripts-runtime/credential-broker/types.ts +54 -0
  82. package/src/scripts-runtime/ctx.ts +11 -1
  83. package/src/scripts-runtime/egress-secrets.ts +14 -76
  84. package/src/scripts-runtime/eval-harness.ts +1 -1
  85. package/src/scripts-runtime/executors/types.ts +5 -5
  86. package/src/scripts-runtime/loader.ts +6 -1
  87. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  88. package/src/scripts-runtime/types/stdlib.d.ts +11 -0
  89. package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
  90. package/src/server.ts +6 -0
  91. package/src/tests/credential-broker.test.ts +281 -0
  92. package/src/tests/heartbeat.test.ts +239 -0
  93. package/src/tests/memory-edit.test.ts +177 -0
  94. package/src/tests/memory-hybrid.test.ts +212 -0
  95. package/src/tests/runner-repo-autostash.test.ts +39 -8
  96. package/src/tests/script-connections.test.ts +360 -0
  97. package/src/tests/scripts-http.test.ts +16 -10
  98. package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
  99. package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
  100. package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
  101. package/src/tools/credential-bindings/index.ts +1 -0
  102. package/src/tools/credential-bindings/tool.ts +238 -0
  103. package/src/tools/memory-edit.ts +148 -0
  104. package/src/tools/memory-search.ts +17 -10
  105. package/src/tools/schedules/delete-schedule.ts +18 -8
  106. package/src/tools/schedules/update-schedule.ts +4 -9
  107. package/src/tools/script-connections/index.ts +1 -0
  108. package/src/tools/script-connections/tool.ts +167 -0
  109. package/src/tools/swarm-config/delete-config.ts +5 -0
  110. package/src/tools/swarm-config/set-config.ts +23 -1
  111. package/src/tools/tool-config.ts +6 -1
  112. package/src/types.ts +6 -0
  113. package/src/workflows/executors/swarm-script.ts +2 -0
  114. package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
  115. package/templates/community/dora-metrics/README.md +39 -0
  116. package/templates/community/dora-metrics/lead-prompt.md +56 -0
  117. package/templates/community/dora-metrics/report.mjs +393 -0
  118. package/templates/community/dora-metrics/run.sh +113 -0
  119. package/templates/schedules/weekly-dora-metrics/config.json +13 -0
  120. package/templates/schedules/weekly-dora-metrics/content.md +61 -0
  121. package/dist/cli-2307phk1.js +0 -24223
  122. 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(source: string): ScriptTypecheckResult {
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, SCRIPT_SDK_TYPES],
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
- const stuckOfflineRows = await query(
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 = stuckOfflineRows
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
- const stuckOfflineRows = await query(
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 = stuckOfflineRows
166
+ const stuckInProgressOnOfflineAgents = stuckRows
155
167
  .filter((row) => !row.unavailable)
156
168
  .map(summarizeTask);
157
169
 
@@ -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 = await Bun.$`cd ${clonePath} && git stash list --format=%gd%x09%s`.quiet();
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 = await Bun.$`cd ${clonePath} && git status --porcelain`.quiet();
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.$`cd ${clonePath} && git stash push --include-untracked -m ${stashMessage}`.quiet();
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.$`cd ${clonePath} && git fetch origin ${fetchSpec}`.quiet();
220
- await Bun.$`cd ${clonePath} && git merge --no-edit --no-stat ${remoteRef}`.quiet();
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.$`cd ${clonePath} && git merge --abort`.quiet();
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) continue; // Session exists — worker might still be alive, skip
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);
@@ -1 +1,7 @@
1
- export { getRebootAffectedTasks, runRebootSweep, startHeartbeat, stopHeartbeat } from "./heartbeat";
1
+ export {
2
+ getBootEpochMs,
3
+ getRebootAffectedTasks,
4
+ runRebootSweep,
5
+ startHeartbeat,
6
+ stopHeartbeat,
7
+ } from "./heartbeat";
@@ -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
- // Dedup delete old chunks for this source path
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) => ({ memoryId: r.id, similarity: r.similarity })),
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;
@@ -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 { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
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
- if (parsed.body.scope === "global" && !agent.isLead) {
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, { sdkTypes: SCRIPT_SDK_TYPES, stdlibTypes: SCRIPT_STDLIB_TYPES });
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: SCRIPT_SDK_TYPES,
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,