@desplega.ai/agent-swarm 1.131.0 → 1.131.1

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 (84) hide show
  1. package/dist/{actions-w53mbns9.js → actions-jgcy477k.js} +6 -6
  2. package/dist/{app-m9gwqsqv.js → app-5da78bd4.js} +3 -3
  3. package/dist/{assistant-bp65m6f3.js → assistant-f2vkk1hj.js} +8 -8
  4. package/dist/{boot-reembed-62p0at2k.js → boot-reembed-q7mp8hcr.js} +3 -3
  5. package/dist/{boot-reembed-xmfp6wdh.js → boot-reembed-xmw8wcx6.js} +4 -4
  6. package/dist/{boot-scrub-logs-rr8q2dms.js → boot-scrub-logs-dsqrv79n.js} +2 -2
  7. package/dist/{claude-adapter-2y47mrkd.js → claude-adapter-q6x7yspx.js} +4 -4
  8. package/dist/{claude-managed-adapter-gv19tdew.js → claude-managed-adapter-wsg37bt8.js} +1 -1
  9. package/dist/{cli-t1xze5qh.js → cli-114fvtmj.js} +1 -1
  10. package/dist/{cli-cdeg08wx.js → cli-2f07m0xr.js} +1 -1
  11. package/dist/{cli-70xqj0z3.js → cli-2rk0cx7s.js} +2 -2
  12. package/dist/{cli-nm5gtp1x.js → cli-3ty0t082.js} +1 -1
  13. package/dist/{cli-efxfnq6q.js → cli-4rkfg1wx.js} +3 -3
  14. package/dist/{cli-a4jzdpa3.js → cli-50vbt00w.js} +1 -1
  15. package/dist/{cli-pm6xbh84.js → cli-6dp3ydxa.js} +34 -8
  16. package/dist/{cli-d3x3x1c9.js → cli-6dpytdr2.js} +1 -1
  17. package/dist/{cli-e7ckcg4v.js → cli-6pcz9h15.js} +12 -12
  18. package/dist/{cli-5kx36evn.js → cli-793jvbsh.js} +1 -1
  19. package/dist/{cli-78xwvkhj.js → cli-7dm2zqy2.js} +2 -2
  20. package/dist/{cli-psh3c4va.js → cli-aca5pz9f.js} +1 -1
  21. package/dist/{cli-72q8m8gk.js → cli-ccf9vy7q.js} +2 -2
  22. package/dist/{cli-qv61hp37.js → cli-g1re8r05.js} +2 -2
  23. package/dist/{cli-d2tfkcga.js → cli-g74vpjp2.js} +3 -3
  24. package/dist/{cli-2q72qz2s.js → cli-gecy3z2s.js} +13 -4
  25. package/dist/{cli-yw8vghx2.js → cli-jbcva0xj.js} +14 -6
  26. package/dist/{cli-t76vw5db.js → cli-jevgrb4w.js} +19 -19
  27. package/dist/{cli-3kf51j15.js → cli-kd1ck4rx.js} +1 -1
  28. package/dist/{cli-g6b8x1h8.js → cli-nrkkknat.js} +1 -1
  29. package/dist/{cli-fywms69x.js → cli-pxfxkx5m.js} +1 -1
  30. package/dist/{cli-cvgjwnh1.js → cli-rgrfzg8z.js} +6 -6
  31. package/dist/{cli-rzbpmxdr.js → cli-se2p6rkb.js} +1 -1
  32. package/dist/{cli-5v4rw6yd.js → cli-sy7jk3tx.js} +1 -1
  33. package/dist/{cli-t55yw3a1.js → cli-t8d349cy.js} +2 -2
  34. package/dist/{cli-0rcyq6d9.js → cli-w2wxd56v.js} +2 -2
  35. package/dist/{cli-qmdnhpej.js → cli-wz9115x0.js} +17 -7
  36. package/dist/{cli-5ze9rv97.js → cli-ycrnynpw.js} +4 -4
  37. package/dist/{cli-a40rnf71.js → cli-ygrbmjja.js} +5 -5
  38. package/dist/cli.js +12 -12
  39. package/dist/{codex-adapter-d24q75wk.js → codex-adapter-htjkn22b.js} +2 -2
  40. package/dist/{codex-session-runner-g50vcsex.js → codex-session-runner-wjfc7smz.js} +2 -2
  41. package/dist/{commands-c3nrc5hg.js → commands-xhrfwqsk.js} +2 -2
  42. package/dist/{db-nt7fg3tw.js → db-anvwb22f.js} +2 -2
  43. package/dist/{handlers-fkr6swat.js → handlers-b9kbc621.js} +8 -8
  44. package/dist/{hook-g019yehy.js → hook-cgk4wxmx.js} +3 -3
  45. package/dist/{hook-77wpvnjc.js → hook-xkvn6c5h.js} +2 -2
  46. package/dist/{http-jewj0syk.js → http-9xh6ta4r.js} +156 -37
  47. package/dist/{index-hfvggd0z.js → index-25wjsdv2.js} +9 -9
  48. package/dist/{index-7n2xpbd4.js → index-eaxyqcmj.js} +8 -8
  49. package/dist/{index-hp1kdbjj.js → index-vg4snay4.js} +10 -10
  50. package/dist/{index-6gm5he8h.js → index-ywds4812.js} +8 -8
  51. package/dist/{keepalive-j0fjvqfd.js → keepalive-vc7s2aa6.js} +4 -4
  52. package/dist/{lead-1n8x30xe.js → lead-r2yqfmx7.js} +24 -24
  53. package/dist/{maintenance-gvev11sa.js → maintenance-mw70k468.js} +4 -4
  54. package/dist/{oauth-refresh-sweep-s9ecsvh0.js → oauth-refresh-sweep-yecgjpk4.js} +3 -3
  55. package/dist/{onboard-pvm5b38w.js → onboard-xj0f73f9.js} +2 -2
  56. package/dist/{otel-impl-p6jvgd9c.js → otel-impl-nxg0n0b3.js} +1 -1
  57. package/dist/{pi-mono-adapter-t091s27g.js → pi-mono-adapter-4g5kby31.js} +1 -1
  58. package/dist/{pricing-refresh-k49rcyed.js → pricing-refresh-2cn6effr.js} +4 -4
  59. package/dist/{rbac-roles-cr80g96t.js → rbac-roles-1hcxdtg7.js} +2 -2
  60. package/dist/{rbac-roles-30pspvy7.js → rbac-roles-vynday5g.js} +3 -3
  61. package/dist/{render-v2-xm457ng1.js → render-v2-tc0bh8kk.js} +3 -3
  62. package/dist/{seed-pricing-yhpv35fr.js → seed-pricing-8kceb43f.js} +3 -3
  63. package/dist/{setup-nytnmwwr.js → setup-46xtgq3c.js} +2 -2
  64. package/dist/{worker-msytr2cw.js → worker-qrbz6hmv.js} +24 -24
  65. package/openapi.json +1 -1
  66. package/package.json +2 -2
  67. package/src/be/migrations/130_scratch_script_retention_grace.sql +6 -0
  68. package/src/be/scripts/db.ts +43 -1
  69. package/src/be/scripts/retention.ts +164 -0
  70. package/src/be/scripts/run-saved.ts +16 -1
  71. package/src/http/index.ts +5 -0
  72. package/src/http/scripts.ts +21 -3
  73. package/src/http/x.ts +14 -0
  74. package/src/providers/claude-managed-models.ts +34 -10
  75. package/src/tests/claude-managed-adapter.test.ts +38 -7
  76. package/src/tests/sandboxed-process.test.ts +45 -0
  77. package/src/tests/script-connections.test.ts +3 -2
  78. package/src/tests/scripts-external-api.test.ts +8 -3
  79. package/src/tests/scripts-http.test.ts +12 -0
  80. package/src/tests/scripts-retention.test.ts +384 -0
  81. package/src/tests/workflow-swarm-script.test.ts +14 -5
  82. package/src/utils/sandboxed-process.ts +40 -14
  83. package/src/workflows/executors/swarm-script.ts +24 -1
  84. package/templates/schedules/daily-compounding-reflection/content.md +12 -3
@@ -5,6 +5,7 @@ import {
5
5
  getScriptMcpConnectionDescriptors,
6
6
  } from "../script-connections";
7
7
  import { buildScriptCredentialBindingsWithFailures } from "../script-credential-broker";
8
+ import { restoreScratchScriptLastUsedIfUnchanged, touchScratchScriptLastUsed } from "./db";
8
9
 
9
10
  export function getSavedScriptOwnerAgentId(script: ScriptRecord): string | null {
10
11
  return script.scopeId ?? script.createdByAgentId;
@@ -16,10 +17,15 @@ export async function runSavedScriptAsAgent(args: {
16
17
  input: unknown;
17
18
  agentId: string;
18
19
  }) {
20
+ // Touch before executing (not just after) so a scratch script that's already
21
+ // stale when a run starts can't be reaped by the retention sweep while the
22
+ // run — which may take up to the runtime's wall-clock ceiling — is in flight.
23
+ const runStartTouch = args.script.isScratch ? touchScratchScriptLastUsed(args.script.id) : null;
24
+
19
25
  const credentials = await buildScriptCredentialBindingsWithFailures({
20
26
  agentId: args.agentId,
21
27
  });
22
- return runScript({
28
+ const output = await runScript({
23
29
  source: args.script.source,
24
30
  args: args.input,
25
31
  fsMode: args.script.fsMode,
@@ -29,4 +35,13 @@ export async function runSavedScriptAsAgent(args: {
29
35
  apiConnections: getScriptApiConnectionDescriptors({ agentId: args.agentId }),
30
36
  mcpConnections: getScriptMcpConnectionDescriptors({ agentId: args.agentId }),
31
37
  });
38
+ if (output.exitCode === 0 && !output.error && !output.runtimeError) {
39
+ touchScratchScriptLastUsed(args.script.id);
40
+ } else if (runStartTouch) {
41
+ // The run-start touch didn't pan out — restore the pre-run timestamp so a
42
+ // failed invocation doesn't buy the script another retention window, as
43
+ // long as no concurrent run has touched it since.
44
+ restoreScratchScriptLastUsedIfUnchanged(args.script.id, args.script.updatedAt, runStartTouch);
45
+ }
46
+ return output;
32
47
  }
package/src/http/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  stopAuditGc,
18
18
  stopAuditWriter,
19
19
  } from "../be/rbac-audit";
20
+ import { startScratchScriptGc, stopScratchScriptGc } from "../be/scripts/retention";
20
21
  import { seedLegacyCapabilitiesConfig } from "../be/seed-capabilities";
21
22
  import { initGitHub } from "../github";
22
23
  import { initGitLab } from "../gitlab";
@@ -441,6 +442,9 @@ async function shutdown() {
441
442
  // Stop memory expired-row garbage collector
442
443
  stopMemoryGc();
443
444
 
445
+ // Stop scratch-script retention garbage collector
446
+ stopScratchScriptGc();
447
+
444
448
  // Stop RBAC audit: retention GC, flush interval, final drain, detach sink
445
449
  stopAuditGc();
446
450
  stopAuditWriter();
@@ -561,6 +565,7 @@ try {
561
565
  setAuditSink(enqueueAuditRow);
562
566
  startAuditWriter();
563
567
  startAuditGc();
568
+ startScratchScriptGc();
564
569
 
565
570
  // business-use initialization (no-op if envs not set)
566
571
  initialize();
@@ -24,7 +24,9 @@ import {
24
24
  listScriptApisForScript,
25
25
  listScripts,
26
26
  listScriptVersions,
27
+ restoreScratchScriptLastUsedIfUnchanged,
27
28
  rotateScriptApiSecret,
29
+ touchScratchScriptLastUsed,
28
30
  updateScriptApi,
29
31
  upsertScriptByName,
30
32
  } from "../be/scripts/db";
@@ -695,12 +697,14 @@ export async function handleScripts(
695
697
 
696
698
  let source = parsed.body.source;
697
699
  let fsMode = parsed.body.fsMode;
700
+ let namedScript: ScriptRecord | null = null;
698
701
  if (parsed.body.name) {
699
702
  const script = resolveScript(parsed.body.name, agent.id, parsed.body.scope);
700
703
  if (!script) {
701
704
  jsonError(res, "Script not found", 404);
702
705
  return true;
703
706
  }
707
+ namedScript = script;
704
708
  source = script.source;
705
709
  fsMode = script.fsMode;
706
710
  }
@@ -711,6 +715,11 @@ export async function handleScripts(
711
715
  }
712
716
 
713
717
  const startedAt = new Date().toISOString();
718
+ // Touch before executing so an already-stale scratch script isn't reaped
719
+ // by the retention sweep while this run is still in flight.
720
+ const runStartTouch = namedScript?.isScratch
721
+ ? touchScratchScriptLastUsed(namedScript.id)
722
+ : null;
714
723
  const credentials = await buildScriptCredentialBindingsWithFailures({ agentId: agent.id });
715
724
  const output = await runScript({
716
725
  source: source as string,
@@ -722,10 +731,20 @@ export async function handleScripts(
722
731
  apiConnections: getScriptApiConnectionDescriptors({ agentId: agent.id }),
723
732
  mcpConnections: getScriptMcpConnectionDescriptors({ agentId: agent.id }),
724
733
  });
734
+ const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
735
+
736
+ if (namedScript?.isScratch && ok) {
737
+ touchScratchScriptLastUsed(namedScript.id);
738
+ } else if (namedScript?.isScratch && runStartTouch) {
739
+ // Failed run — restore the pre-run timestamp so it doesn't buy the
740
+ // script another retention window, unless a concurrent run already
741
+ // touched it since.
742
+ restoreScratchScriptLastUsedIfUnchanged(namedScript.id, namedScript.updatedAt, runStartTouch);
743
+ }
725
744
 
726
745
  // Persist output to KV when idempotencyKey is provided and run succeeded
727
746
  let kvSaved: { namespace: string; key: string } | undefined;
728
- if (parsed.body.idempotencyKey && !output.error && output.exitCode === 0) {
747
+ if (parsed.body.idempotencyKey && ok) {
729
748
  const kvNamespace = `script:executions`;
730
749
  const kvKey = parsed.body.idempotencyKey;
731
750
  const kvValue = {
@@ -745,7 +764,7 @@ export async function handleScripts(
745
764
  }
746
765
 
747
766
  let autoSaved: { slug: string; reason: string } | undefined;
748
- if (parsed.body.source && !output.error && output.exitCode === 0) {
767
+ if (parsed.body.source && ok) {
749
768
  const slug = scratchSlug(parsed.body.intent, parsed.body.source);
750
769
  await upsertScriptByName({
751
770
  name: slug,
@@ -767,7 +786,6 @@ export async function handleScripts(
767
786
  // Persist the inline run (no journal) so one-off executions show up alongside
768
787
  // durable workflow runs in the Script Runs dashboard. Best-effort: recording
769
788
  // must never fail the actual execution.
770
- const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
771
789
  const runError = ok
772
790
  ? undefined
773
791
  : scrubSecrets(
package/src/http/x.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  getScriptApiSecret,
13
13
  getScriptById,
14
14
  recordScriptApiUsage,
15
+ restoreScratchScriptLastUsedIfUnchanged,
16
+ touchScratchScriptLastUsed,
15
17
  } from "../be/scripts/db";
16
18
  import {
17
19
  MAX_SCRIPT_WALL_CLOCK_MS,
@@ -228,6 +230,10 @@ export async function handleX(
228
230
  const timeoutMs = resolveTimeoutMs(req);
229
231
  const startedAt = new Date().toISOString();
230
232
 
233
+ // Touch before executing so an already-stale scratch script isn't reaped by
234
+ // the retention sweep while this run is still in flight.
235
+ const runStartTouch = script.isScratch ? touchScratchScriptLastUsed(script.id) : null;
236
+
231
237
  const credentials = await buildScriptCredentialBindingsWithFailures({
232
238
  agentId: endpoint.agentId,
233
239
  });
@@ -250,6 +256,14 @@ export async function handleX(
250
256
 
251
257
  const ok = output.exitCode === 0 && !output.error && !output.runtimeError;
252
258
  const error = ok ? null : buildExecutionError(output);
259
+ if (ok) {
260
+ touchScratchScriptLastUsed(script.id);
261
+ } else if (script.isScratch && runStartTouch) {
262
+ // Failed run — restore the pre-run timestamp so it doesn't buy the
263
+ // script another retention window, unless a concurrent run already
264
+ // touched it since.
265
+ restoreScratchScriptLastUsedIfUnchanged(script.id, script.updatedAt, runStartTouch);
266
+ }
253
267
 
254
268
  // Usage + observability — best-effort, must never fail the response.
255
269
  try {
@@ -38,8 +38,8 @@ export const CLAUDE_MANAGED_MODELS = [
38
38
 
39
39
  export type ClaudeManagedModel = (typeof CLAUDE_MANAGED_MODELS)[number];
40
40
 
41
- /** Pricing per million tokens (USD). */
42
- export interface ClaudeManagedModelPricing {
41
+ /** Token rates per million tokens (USD). */
42
+ export interface ClaudeManagedTokenPricing {
43
43
  /** USD per million uncached input tokens. */
44
44
  inputPerMillion: number;
45
45
  /** USD per million output tokens. */
@@ -50,6 +50,15 @@ export interface ClaudeManagedModelPricing {
50
50
  cacheWritePerMillion: number;
51
51
  }
52
52
 
53
+ /** Pricing per million tokens (USD), including an optional scheduled rate change. */
54
+ export interface ClaudeManagedModelPricing extends ClaudeManagedTokenPricing {
55
+ scheduledChange?: {
56
+ /** ISO timestamp at which the replacement rates take effect. */
57
+ effectiveAt: string;
58
+ pricing: ClaudeManagedTokenPricing;
59
+ };
60
+ }
61
+
53
62
  /**
54
63
  * Anthropic public list pricing. Source:
55
64
  * https://platform.claude.com/docs/en/about-claude/pricing
@@ -57,7 +66,7 @@ export interface ClaudeManagedModelPricing {
57
66
  * - claude-fable-5: $10 / $50 / $1.00 / $12.50 (verified 2026-06-10)
58
67
  * - claude-mythos-5: $10 / $50 / $1.00 / $12.50 (limited availability, verified 2026-06-10)
59
68
  * - claude-opus-5: $5 / $25 / $0.50 / $6.25 (verified 2026-07-25)
60
- * - claude-sonnet-5: $3 / $15 / $0.30 / $3.75 (verified 2026-06-30; intro $2/$10 through 2026-08-31)
69
+ * - claude-sonnet-5: $2 / $10 / $0.20 / $2.50 (introductory rate, verified 2026-08-11; $3/$15 from 2026-09-01)
61
70
  * - claude-sonnet-4-6: $3 / $15 / $0.30 / $3.75 (in / out / cache-read / cache-write)
62
71
  * - claude-opus-4-8: $5 / $25 / $0.50 / $6.25 (verified 2026-05-28)
63
72
  * - claude-opus-4-7: $15 / $75 / $1.50 / $18.75 (STALE — was correct at launch, Anthropic has since dropped Opus to $5/$25)
@@ -83,12 +92,21 @@ export const CLAUDE_MANAGED_MODEL_PRICING: Record<ClaudeManagedModel, ClaudeMana
83
92
  cacheReadPerMillion: 1.0,
84
93
  cacheWritePerMillion: 12.5,
85
94
  },
86
- // intro $2/$10 through 2026-08-31
95
+ // Introductory rate through 2026-08-31; changes to $3/$15 from 2026-09-01.
87
96
  "claude-sonnet-5": {
88
- inputPerMillion: 3.0,
89
- outputPerMillion: 15.0,
90
- cacheReadPerMillion: 0.3,
91
- cacheWritePerMillion: 3.75,
97
+ inputPerMillion: 2.0,
98
+ outputPerMillion: 10.0,
99
+ cacheReadPerMillion: 0.2,
100
+ cacheWritePerMillion: 2.5,
101
+ scheduledChange: {
102
+ effectiveAt: "2026-09-01T00:00:00.000Z",
103
+ pricing: {
104
+ inputPerMillion: 3.0,
105
+ outputPerMillion: 15.0,
106
+ cacheReadPerMillion: 0.3,
107
+ cacheWritePerMillion: 3.75,
108
+ },
109
+ },
92
110
  },
93
111
  "claude-sonnet-4-6": {
94
112
  inputPerMillion: 3.0,
@@ -146,9 +164,10 @@ export function computeClaudeManagedCostUsd(
146
164
  outputTokens: number,
147
165
  cacheReadTokens: number,
148
166
  cacheWriteTokens: number,
167
+ pricedAtMs: number = Date.now(),
149
168
  ): number {
150
- const pricing = CLAUDE_MANAGED_MODEL_PRICING[model as ClaudeManagedModel];
151
- if (!pricing) {
169
+ const configuredPricing = CLAUDE_MANAGED_MODEL_PRICING[model as ClaudeManagedModel];
170
+ if (!configuredPricing) {
152
171
  if (!warnedUnknownModels.has(model)) {
153
172
  warnedUnknownModels.add(model);
154
173
  console.warn(
@@ -158,6 +177,11 @@ export function computeClaudeManagedCostUsd(
158
177
  }
159
178
  return 0;
160
179
  }
180
+ const pricing =
181
+ configuredPricing.scheduledChange &&
182
+ pricedAtMs >= Date.parse(configuredPricing.scheduledChange.effectiveAt)
183
+ ? configuredPricing.scheduledChange.pricing
184
+ : configuredPricing;
161
185
  const inputCost = (inputTokens / 1_000_000) * pricing.inputPerMillion;
162
186
  const outputCost = (outputTokens / 1_000_000) * pricing.outputPerMillion;
163
187
  const cacheReadCost = (cacheReadTokens / 1_000_000) * pricing.cacheReadPerMillion;
@@ -831,11 +831,33 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
831
831
  expect(cost).toBeCloseTo(4.5, 10);
832
832
  });
833
833
 
834
- test("computeClaudeManagedCostUsd returns expected USD for sonnet-5 against list rate", () => {
834
+ test("computeClaudeManagedCostUsd uses sonnet-5 introductory rate through 2026-08-31", () => {
835
+ // 1M input tokens × $2.00/Mtok = $2.00
836
+ // 100k output tokens × $10.00/Mtok = $1.00
837
+ // total = $3.00. The introductory rate applies through 2026-08-31.
838
+ const cost = computeClaudeManagedCostUsd(
839
+ "claude-sonnet-5",
840
+ 1_000_000,
841
+ 100_000,
842
+ 0,
843
+ 0,
844
+ Date.parse("2026-08-31T23:59:59.999Z"),
845
+ );
846
+ expect(cost).toBeCloseTo(3.0, 10);
847
+ });
848
+
849
+ test("computeClaudeManagedCostUsd uses sonnet-5 standard rate from 2026-09-01", () => {
835
850
  // 1M input tokens × $3.00/Mtok = $3.00
836
851
  // 100k output tokens × $15.00/Mtok = $1.50
837
- // total = $4.50. Intro $2/$10 pricing is intentionally not used here.
838
- const cost = computeClaudeManagedCostUsd("claude-sonnet-5", 1_000_000, 100_000, 0, 0);
852
+ // total = $4.50 from the first instant of 2026-09-01 UTC.
853
+ const cost = computeClaudeManagedCostUsd(
854
+ "claude-sonnet-5",
855
+ 1_000_000,
856
+ 100_000,
857
+ 0,
858
+ 0,
859
+ Date.parse("2026-09-01T00:00:00.000Z"),
860
+ );
839
861
  expect(cost).toBeCloseTo(4.5, 10);
840
862
  });
841
863
 
@@ -882,10 +904,19 @@ describe("ClaudeManagedAdapter (Phase 4) — repo provisioning + cost data", ()
882
904
  cacheWritePerMillion: 12.5,
883
905
  });
884
906
  expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-5"]).toEqual({
885
- inputPerMillion: 3.0,
886
- outputPerMillion: 15.0,
887
- cacheReadPerMillion: 0.3,
888
- cacheWritePerMillion: 3.75,
907
+ inputPerMillion: 2.0,
908
+ outputPerMillion: 10.0,
909
+ cacheReadPerMillion: 0.2,
910
+ cacheWritePerMillion: 2.5,
911
+ scheduledChange: {
912
+ effectiveAt: "2026-09-01T00:00:00.000Z",
913
+ pricing: {
914
+ inputPerMillion: 3.0,
915
+ outputPerMillion: 15.0,
916
+ cacheReadPerMillion: 0.3,
917
+ cacheWritePerMillion: 3.75,
918
+ },
919
+ },
889
920
  });
890
921
  expect(CLAUDE_MANAGED_MODEL_PRICING["claude-sonnet-4-6"]).toBeDefined();
891
922
  expect(CLAUDE_MANAGED_MODEL_PRICING["claude-opus-4-7"]).toBeDefined();
@@ -1,12 +1,21 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
+ BUN_SANDBOX_VIRTUAL_MEMORY_MB,
3
4
  buildSandboxedCommand,
4
5
  createCappedStreamState,
6
+ DEFAULT_SANDBOX_LIMITS,
7
+ JAVASCRIPT_RUNTIME_SANDBOX_MAX_PROCS,
5
8
  readStreamCapped,
6
9
  sandboxSpawnEnv,
7
10
  snapshotCapped,
8
11
  } from "../utils/sandboxed-process";
9
12
 
13
+ const TEST_ENV = { PATH: process.env.PATH ?? "/usr/bin:/bin", HOME: "/tmp" };
14
+
15
+ function sandboxPrelude(command: readonly string[]): string {
16
+ return buildSandboxedCommand(command, TEST_ENV)[2] ?? "";
17
+ }
18
+
10
19
  /**
11
20
  * Temporarily override `process.platform` for the duration of `fn`, then
12
21
  * restore it. Used to exercise the win32 branches of `buildSandboxedCommand`
@@ -56,6 +65,42 @@ describe("sandboxSpawnEnv", () => {
56
65
  });
57
66
  });
58
67
 
68
+ describe("buildSandboxedCommand runtime-aware limits", () => {
69
+ test.each(["bun", "node", "npx"])("raises AS and nproc for direct %s commands", (runtime) => {
70
+ const command = buildSandboxedCommand([runtime, "--version"], TEST_ENV);
71
+ expect(command[0]).toBe("bash");
72
+ expect(command[2]).toContain(`ulimit -v ${BUN_SANDBOX_VIRTUAL_MEMORY_MB * 1024}`);
73
+ expect(command[2]).toContain(`ulimit -u ${JAVASCRIPT_RUNTIME_SANDBOX_MAX_PROCS}`);
74
+ });
75
+
76
+ test.each(["bash", "sh", "dash"])("propagates the runtime profile through %s -c", (shell) => {
77
+ const script = "bun /opt/meme-post.bundle.js";
78
+ const prelude = sandboxPrelude([shell, "-c", script]);
79
+ expect(prelude).toContain(`ulimit -v ${BUN_SANDBOX_VIRTUAL_MEMORY_MB * 1024}`);
80
+ expect(prelude).toContain(`ulimit -u ${JAVASCRIPT_RUNTIME_SANDBOX_MAX_PROCS}`);
81
+ });
82
+
83
+ test("keeps strict defaults for direct non-interpreter commands", () => {
84
+ const command = buildSandboxedCommand(["git", "status", "--short"], TEST_ENV);
85
+ expect(command[0]).toBe("sh");
86
+ const prelude = command[2] ?? "";
87
+ expect(prelude).toContain(`ulimit -v ${DEFAULT_SANDBOX_LIMITS.virtualMemoryMb * 1024}`);
88
+ expect(prelude).toContain(`ulimit -u ${DEFAULT_SANDBOX_LIMITS.maxProcs}`);
89
+ });
90
+
91
+ test("the raised profile starts a shell-wrapped Bun process with real nproc enforcement", () => {
92
+ const result = Bun.spawnSync(
93
+ buildSandboxedCommand(
94
+ ["bash", "-c", "bun -e 'console.log(JSON.stringify({ started: true }))'"],
95
+ TEST_ENV,
96
+ ),
97
+ { env: sandboxSpawnEnv(TEST_ENV) },
98
+ );
99
+ expect(result.exitCode).toBe(0);
100
+ expect(JSON.parse(result.stdout.toString())).toEqual({ started: true });
101
+ });
102
+ });
103
+
59
104
  // ─── readStreamCapped / snapshotCapped (Codex PRRT_kwDOQr3Tmc6XCRuy — deadline snapshot) ──
60
105
 
61
106
  describe("readStreamCapped with an external CappedStreamState", () => {
@@ -535,9 +535,10 @@ describe("script connections", () => {
535
535
  markMigrationApplied(database, "127_backfill_user_attribution.sql");
536
536
  // 128 alters session_costs, which this partial fixture does not create.
537
537
  markMigrationApplied(database, "128_session_costs_accuracy.sql");
538
- // 129 alters scripts and rebuilds asset_key_history, neither of which this
539
- // migration-112-only fixture creates.
538
+ // 129 and 130 alter scripts (and 129 rebuilds asset_key_history), none of
539
+ // which this migration-112-only fixture creates.
540
540
  markMigrationApplied(database, "129_asset_keys_apps_scripts.sql");
541
+ markMigrationApplied(database, "130_scratch_script_retention_grace.sql");
541
542
 
542
543
  const id = crypto.randomUUID();
543
544
  const now = new Date().toISOString();
@@ -8,6 +8,7 @@ import {
8
8
  createScriptApi,
9
9
  getScriptApiById,
10
10
  getScriptApiSecret,
11
+ getScriptById,
11
12
  insertScript,
12
13
  listScriptApisForScript,
13
14
  } from "../be/scripts/db";
@@ -49,9 +50,9 @@ const DOUBLER_SOURCE =
49
50
  let workerId: string;
50
51
  let savedEnv: NodeJS.ProcessEnv;
51
52
 
52
- function insertDoubler(opts: { argsJsonSchema?: string | null } = {}) {
53
+ function insertDoubler(opts: { argsJsonSchema?: string | null; isScratch?: boolean } = {}) {
53
54
  return insertScript({
54
- name: `doubler-${crypto.randomUUID().slice(0, 8)}`,
55
+ name: `${opts.isScratch ? "scratch-" : ""}doubler-${crypto.randomUUID().slice(0, 8)}`,
55
56
  scope: "agent",
56
57
  scopeId: workerId,
57
58
  source: DOUBLER_SOURCE,
@@ -61,6 +62,7 @@ function insertDoubler(opts: { argsJsonSchema?: string | null } = {}) {
61
62
  argsJsonSchema: opts.argsJsonSchema ?? null,
62
63
  agentId: workerId,
63
64
  typeChecked: true,
65
+ isScratch: opts.isScratch,
64
66
  });
65
67
  }
66
68
 
@@ -262,7 +264,9 @@ describe("management routes", () => {
262
264
 
263
265
  describe("public execution route", () => {
264
266
  test("none-mode endpoint runs and returns the wrapped envelope", async () => {
265
- const script = insertDoubler();
267
+ const script = insertDoubler({ isScratch: true });
268
+ const old = "2026-07-01T00:00:00.000Z";
269
+ getDb().prepare("UPDATE scripts SET updatedAt = ? WHERE id = ?").run(old, script.id);
266
270
  const endpoint = createScriptApi({
267
271
  scriptId: script.id,
268
272
  agentId: workerId,
@@ -285,6 +289,7 @@ describe("public execution route", () => {
285
289
  expect(body.result).toEqual({ doubled: 42 });
286
290
  expect(body.error).toBeNull();
287
291
  expect(typeof body.durationMs).toBe("number");
292
+ expect(getScriptById(script.id)?.updatedAt).not.toBe(old);
288
293
  });
289
294
 
290
295
  test("external endpoint runs receive ctx.mcp connections (parity with /api/scripts/run)", async () => {
@@ -501,6 +501,18 @@ describe("/api/scripts HTTP", () => {
501
501
  const inlineBody = await inline.json();
502
502
  expect(inlineBody.result).toEqual({ ok: "not typechecked" });
503
503
  expect(inlineBody.autoSaved.slug).toContain("scratch-inline-type-error-still-runs");
504
+ getDb()
505
+ .prepare("UPDATE scripts SET updatedAt = ? WHERE name = ? AND scopeId = ?")
506
+ .run("2026-07-01T00:00:00.000Z", inlineBody.autoSaved.slug, workerId);
507
+ const reusedScratch = await dispatch("/api/scripts/run", {
508
+ method: "POST",
509
+ agentId: workerId,
510
+ body: JSON.stringify({ name: inlineBody.autoSaved.slug }),
511
+ });
512
+ expect(reusedScratch.status).toBe(200);
513
+ expect(
514
+ getScript({ name: inlineBody.autoSaved.slug, scope: "agent", scopeId: workerId })?.updatedAt,
515
+ ).not.toBe("2026-07-01T00:00:00.000Z");
504
516
 
505
517
  const beforeFailed = listScripts({
506
518
  scope: "agent",