@desplega.ai/agent-swarm 1.80.0 → 1.80.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 (93) hide show
  1. package/openapi.json +399 -14
  2. package/package.json +3 -1
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +1 -1
  5. package/src/be/migrations/064_scripts.sql +39 -0
  6. package/src/be/migrations/065_script_embeddings.sql +7 -0
  7. package/src/be/scripts/db.ts +391 -0
  8. package/src/be/scripts/embeddings.ts +231 -0
  9. package/src/be/scripts/maintenance.ts +9 -0
  10. package/src/be/scripts/typecheck.ts +193 -0
  11. package/src/cli.tsx +22 -5
  12. package/src/commands/artifact.ts +3 -2
  13. package/src/commands/claude-managed-setup.ts +2 -1
  14. package/src/commands/codex-login.ts +5 -3
  15. package/src/commands/onboard.tsx +2 -1
  16. package/src/commands/runner.ts +72 -10
  17. package/src/commands/setup.tsx +5 -3
  18. package/src/hooks/hook.ts +4 -3
  19. package/src/http/index.ts +40 -29
  20. package/src/http/memory.ts +28 -0
  21. package/src/http/openapi.ts +1 -0
  22. package/src/http/page-proxy.ts +2 -1
  23. package/src/http/route-def.ts +1 -0
  24. package/src/http/schedules.ts +37 -0
  25. package/src/http/scripts.ts +381 -0
  26. package/src/linear/outbound.ts +9 -2
  27. package/src/otel.ts +5 -0
  28. package/src/providers/claude-adapter.ts +22 -1
  29. package/src/scripts-runtime/ctx.ts +23 -0
  30. package/src/scripts-runtime/eval-harness.ts +39 -0
  31. package/src/scripts-runtime/executors/native.ts +229 -0
  32. package/src/scripts-runtime/executors/registry.ts +16 -0
  33. package/src/scripts-runtime/executors/types.ts +63 -0
  34. package/src/scripts-runtime/extract-signature.ts +81 -0
  35. package/src/scripts-runtime/import-allowlist.ts +109 -0
  36. package/src/scripts-runtime/loader.ts +96 -0
  37. package/src/scripts-runtime/redacted.ts +48 -0
  38. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  39. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  40. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  41. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  42. package/src/scripts-runtime/stdlib/index.ts +16 -0
  43. package/src/scripts-runtime/stdlib/table.ts +17 -0
  44. package/src/scripts-runtime/swarm-config.ts +35 -0
  45. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  46. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  47. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  48. package/src/server.ts +12 -0
  49. package/src/tests/api-key.test.ts +33 -0
  50. package/src/tests/codex-login.test.ts +1 -1
  51. package/src/tests/linear-outbound-sync.test.ts +109 -0
  52. package/src/tests/mcp-tools.test.ts +69 -0
  53. package/src/tests/redacted.test.ts +29 -0
  54. package/src/tests/runner-tool-spans.test.ts +268 -0
  55. package/src/tests/script-executor-conformance.test.ts +142 -0
  56. package/src/tests/script-executor-registry.test.ts +17 -0
  57. package/src/tests/scripts-db.test.ts +329 -0
  58. package/src/tests/scripts-embeddings.test.ts +291 -0
  59. package/src/tests/scripts-extract-signature.test.ts +47 -0
  60. package/src/tests/scripts-http.test.ts +350 -0
  61. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  62. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  63. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  64. package/src/tests/scripts-runtime.test.ts +289 -0
  65. package/src/tests/sdk-allowlist.test.ts +59 -0
  66. package/src/tests/secret-scrubber.test.ts +35 -1
  67. package/src/tests/swarm-config.test.ts +38 -0
  68. package/src/tests/tool-annotations.test.ts +2 -2
  69. package/src/tests/tool-call-progress.test.ts +30 -0
  70. package/src/tests/workflow-e2e.test.ts +218 -0
  71. package/src/tests/workflow-executors.test.ts +32 -2
  72. package/src/tests/workflow-input-redaction.test.ts +232 -0
  73. package/src/tests/workflow-swarm-script.test.ts +273 -0
  74. package/src/tools/memory-rate.ts +2 -1
  75. package/src/tools/script-common.ts +88 -0
  76. package/src/tools/script-delete.ts +35 -0
  77. package/src/tools/script-query-types.ts +37 -0
  78. package/src/tools/script-run.ts +43 -0
  79. package/src/tools/script-search.ts +32 -0
  80. package/src/tools/script-upsert.ts +43 -0
  81. package/src/tools/tool-config.ts +7 -0
  82. package/src/types.ts +60 -1
  83. package/src/utils/api-key.ts +28 -0
  84. package/src/utils/page-session.ts +8 -6
  85. package/src/utils/secret-scrubber.ts +22 -1
  86. package/src/workflows/engine.ts +12 -4
  87. package/src/workflows/executors/index.ts +1 -0
  88. package/src/workflows/executors/registry.ts +2 -0
  89. package/src/workflows/executors/script.ts +12 -1
  90. package/src/workflows/executors/swarm-script.ts +170 -0
  91. package/src/workflows/input.ts +65 -0
  92. package/src/workflows/recovery.ts +31 -3
  93. package/src/workflows/resume.ts +43 -5
package/src/http/index.ts CHANGED
@@ -14,9 +14,10 @@ import { initGitLab } from "../gitlab";
14
14
  import { stopHeartbeat } from "../heartbeat";
15
15
  import { initJira } from "../jira";
16
16
  import { initLinear } from "../linear";
17
- import { initOtel, startSpan, withRemoteContext } from "../otel";
17
+ import { initOtel, isPollTracingEnabled, startSpan, withRemoteContext } from "../otel";
18
18
  import { startSlackApp, stopSlackApp } from "../slack";
19
19
  import { initTelemetry, telemetry } from "../telemetry";
20
+ import { getApiKey } from "../utils/api-key";
20
21
  import { initWorkflows } from "../workflows";
21
22
  import { handleActiveSessions } from "./active-sessions";
22
23
  import { handleAgentRegister, handleAgentsRest } from "./agents";
@@ -45,6 +46,7 @@ import { handlePricing } from "./pricing";
45
46
  import { handlePromptTemplates } from "./prompt-templates";
46
47
  import { handleRepos } from "./repos";
47
48
  import { handleSchedules } from "./schedules";
49
+ import { handleScripts } from "./scripts";
48
50
  import { handleSessionData } from "./session-data";
49
51
  import { handleSessions } from "./sessions";
50
52
  import { handleSkills } from "./skills";
@@ -69,7 +71,7 @@ process.on("unhandledRejection", (reason) => {
69
71
  });
70
72
 
71
73
  const port = parseInt(process.env.PORT || process.argv[2] || "3013", 10);
72
- const apiKey = process.env.API_KEY || "";
74
+ const apiKey = getApiKey();
73
75
 
74
76
  // Use globalThis to persist state across hot reloads
75
77
  const globalState = globalThis as typeof globalThis & {
@@ -116,33 +118,39 @@ const httpServer = createHttpServer(async (req, res) => {
116
118
  });
117
119
 
118
120
  await withRemoteContext(req.headers as Record<string, unknown>, async () => {
119
- const span = startSpan("http.server", {
120
- "http.request.method": req.method ?? "",
121
- "url.path": req.url?.split("?")[0] ?? "",
122
- "agent.id": req.headers["x-agent-id"] as string | undefined,
123
- "agentswarm.component": "api",
124
- });
125
-
126
- res.on("finish", () => {
127
- if (spanEnded) return;
128
- spanEnded = true;
129
- span.setAttributes({
130
- "http.response.status_code": statusCode,
131
- "agentswarm.http.duration_ms": Math.round((performance.now() - startTime) * 10) / 10,
121
+ const reqPath = req.url?.split("?")[0] ?? "";
122
+ const skipSpan = reqPath === "/api/poll" && !isPollTracingEnabled();
123
+ const span = skipSpan
124
+ ? null
125
+ : startSpan("http.server", {
126
+ "http.request.method": req.method ?? "",
127
+ "url.path": reqPath,
128
+ "agent.id": req.headers["x-agent-id"] as string | undefined,
129
+ "agentswarm.component": "api",
130
+ });
131
+
132
+ if (span) {
133
+ res.on("finish", () => {
134
+ if (spanEnded) return;
135
+ spanEnded = true;
136
+ span.setAttributes({
137
+ "http.response.status_code": statusCode,
138
+ "agentswarm.http.duration_ms": Math.round((performance.now() - startTime) * 10) / 10,
139
+ });
140
+ if (statusCode >= 500) {
141
+ span.setStatus({ code: 2, message: `HTTP ${statusCode}` });
142
+ }
143
+ span.end();
132
144
  });
133
- if (statusCode >= 500) {
134
- span.setStatus({ code: 2, message: `HTTP ${statusCode}` });
135
- }
136
- span.end();
137
- });
138
145
 
139
- res.on("error", (err) => {
140
- if (spanEnded) return;
141
- spanEnded = true;
142
- span.recordException(err);
143
- span.setStatus({ code: 2, message: err.message });
144
- span.end();
145
- });
146
+ res.on("error", (err) => {
147
+ if (spanEnded) return;
148
+ spanEnded = true;
149
+ span.recordException(err);
150
+ span.setStatus({ code: 2, message: err.message });
151
+ span.end();
152
+ });
153
+ }
146
154
 
147
155
  setCorsHeaders(req, res);
148
156
 
@@ -180,6 +188,7 @@ const httpServer = createHttpServer(async (req, res) => {
180
188
  () => handleDbQuery(req, res, pathSegments, queryParams),
181
189
  () => handleRepos(req, res, pathSegments, queryParams),
182
190
  () => handleSkills(req, res, pathSegments, queryParams, myAgentId),
191
+ () => handleScripts(req, res, pathSegments, queryParams, myAgentId),
183
192
  () => handleMcpServers(req, res, pathSegments, queryParams),
184
193
  () => handleMcpOAuth(req, res, pathSegments, queryParams),
185
194
  () => handleMemory(req, res, pathSegments, myAgentId),
@@ -205,8 +214,10 @@ const httpServer = createHttpServer(async (req, res) => {
205
214
  res.writeHead(404);
206
215
  res.end("Not Found");
207
216
  } catch (err) {
208
- span.recordException(err);
209
- span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });
217
+ if (span) {
218
+ span.recordException(err);
219
+ span.setStatus({ code: 2, message: err instanceof Error ? err.message : String(err) });
220
+ }
210
221
  const message = err instanceof Error ? err.message : String(err);
211
222
  console.error(`[HTTP] ❌ ${req.method} ${req.url} → ${message}`);
212
223
  if (!res.headersSent) {
@@ -124,6 +124,20 @@ const deleteMemoryById = route({
124
124
  },
125
125
  });
126
126
 
127
+ const getMemoryById = route({
128
+ method: "get",
129
+ path: "/api/memory/{id}",
130
+ pattern: ["api", "memory", null],
131
+ summary: "Get a single memory by ID",
132
+ tags: ["Memory"],
133
+ auth: { apiKey: true, agentId: true },
134
+ params: z.object({ id: z.string().uuid() }),
135
+ responses: {
136
+ 200: { description: "Memory details" },
137
+ 404: { description: "Memory not found" },
138
+ },
139
+ });
140
+
127
141
  // Memory rater v1.5 — worker-facing rating endpoints. Plan:
128
142
  // thoughts/taras/plans/2026-05-05-memory-rater-v1.5/step-3.md
129
143
  //
@@ -618,5 +632,19 @@ export async function handleMemory(
618
632
  return true;
619
633
  }
620
634
 
635
+ if (getMemoryById.match(req.method, pathSegments)) {
636
+ const parsed = await getMemoryById.parse(req, res, pathSegments, new URLSearchParams());
637
+ if (!parsed) return true;
638
+
639
+ const memory = getMemoryStore().get(parsed.params.id);
640
+ if (!memory) {
641
+ jsonError(res, "Memory not found", 404);
642
+ return true;
643
+ }
644
+
645
+ json(res, { memory });
646
+ return true;
647
+ }
648
+
621
649
  return false;
622
650
  }
@@ -64,6 +64,7 @@ export function generateOpenApiSpec(opts: OpenApiOptions): string {
64
64
  registry.registerPath({
65
65
  method: routeDef.method,
66
66
  path: routeDef.path,
67
+ operationId: routeDef.operationId,
67
68
  summary: routeDef.summary,
68
69
  description: routeDef.description,
69
70
  tags: routeDef.tags,
@@ -1,5 +1,6 @@
1
1
  import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  import { getPage } from "../be/db";
3
+ import { getApiKey } from "../utils/api-key";
3
4
  import { extractAndVerifyCookie } from "../utils/page-session";
4
5
  import { route } from "./route-def";
5
6
  import { jsonError } from "./utils";
@@ -147,7 +148,7 @@ export async function handlePageProxy(req: IncomingMessage, res: ServerResponse)
147
148
  const baseUrl = `http://127.0.0.1:${port}`;
148
149
  const targetUrl = `${baseUrl}${rewrittenPath}${queryPart}`;
149
150
 
150
- const apiKey = process.env.API_KEY ?? "";
151
+ const apiKey = getApiKey();
151
152
  // `X-Page-Id` is the trust anchor for page-scoped KV: only the page-proxy
152
153
  // ever sets it (any external `X-Page-Id` header is dropped because we don't
153
154
  // forward the original headers). The KV handler treats this as the highest-
@@ -20,6 +20,7 @@ export interface RouteDef<
20
20
  path: string; // OpenAPI-style: "/api/tasks/{id}"
21
21
  pattern: readonly (string | null)[]; // matchRoute-style: ["api", "tasks", null]
22
22
  exact?: boolean; // default true
23
+ operationId?: string;
23
24
  summary: string;
24
25
  description?: string;
25
26
  tags: string[];
@@ -8,6 +8,7 @@ import {
8
8
  getDb,
9
9
  getScheduledTaskById,
10
10
  getScheduledTaskByName,
11
+ getScheduledTasks,
11
12
  updateScheduledTask,
12
13
  } from "../be/db";
13
14
  import { calculateNextRun } from "../scheduler/scheduler";
@@ -64,6 +65,29 @@ const runScheduleNow = route({
64
65
  },
65
66
  });
66
67
 
68
+ const listSchedules = route({
69
+ method: "get",
70
+ path: "/api/schedules",
71
+ pattern: ["api", "schedules"],
72
+ summary: "List schedules",
73
+ tags: ["Schedules"],
74
+ query: z.object({
75
+ enabled: z
76
+ .enum(["true", "false"])
77
+ .optional()
78
+ .transform((v) => (v === undefined ? undefined : v === "true")),
79
+ name: z.string().optional(),
80
+ scheduleType: z.enum(["recurring", "one_time"]).optional(),
81
+ hideCompleted: z
82
+ .enum(["true", "false"])
83
+ .optional()
84
+ .transform((v) => (v === undefined ? undefined : v === "true")),
85
+ }),
86
+ responses: {
87
+ 200: { description: "List of schedules" },
88
+ },
89
+ });
90
+
67
91
  const getSchedule = route({
68
92
  method: "get",
69
93
  path: "/api/schedules/{id}",
@@ -129,6 +153,19 @@ export async function handleSchedules(
129
153
  queryParams: URLSearchParams,
130
154
  _myAgentId: string | undefined,
131
155
  ): Promise<boolean> {
156
+ if (listSchedules.match(req.method, pathSegments)) {
157
+ const parsed = await listSchedules.parse(req, res, pathSegments, queryParams);
158
+ if (!parsed) return true;
159
+ const schedules = getScheduledTasks({
160
+ enabled: parsed.query.enabled,
161
+ name: parsed.query.name,
162
+ scheduleType: parsed.query.scheduleType,
163
+ hideCompleted: parsed.query.hideCompleted,
164
+ });
165
+ json(res, { schedules, count: schedules.length });
166
+ return true;
167
+ }
168
+
132
169
  if (createSchedule.match(req.method, pathSegments)) {
133
170
  const parsed = await createSchedule.parse(req, res, pathSegments, queryParams);
134
171
  if (!parsed) return true;
@@ -0,0 +1,381 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ import { z } from "zod";
3
+ import { getAgentById } from "../be/db";
4
+ import { createEvent } from "../be/events";
5
+ import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
6
+ import { searchScripts } from "../be/scripts/embeddings";
7
+ import { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
8
+ import { extractScriptSignature } from "../scripts-runtime/extract-signature";
9
+ import { runScript } from "../scripts-runtime/loader";
10
+ import {
11
+ ScriptFsModeSchema,
12
+ type ScriptRecord,
13
+ type ScriptScope,
14
+ ScriptScopeSchema,
15
+ } from "../types";
16
+ import { scrubObject } from "../utils/secret-scrubber";
17
+ import { route } from "./route-def";
18
+ import { json, jsonError } from "./utils";
19
+
20
+ const scriptNameSchema = z.string().min(1).max(200);
21
+
22
+ const upsertBodySchema = z.object({
23
+ name: scriptNameSchema,
24
+ source: z.string().min(1),
25
+ description: z.string().default(""),
26
+ intent: z.string().default(""),
27
+ scope: ScriptScopeSchema.default("agent"),
28
+ fsMode: ScriptFsModeSchema.default("none"),
29
+ });
30
+
31
+ const runBodySchema = z
32
+ .object({
33
+ name: scriptNameSchema.optional(),
34
+ source: z.string().min(1).optional(),
35
+ args: z.unknown().optional(),
36
+ intent: z.string().default(""),
37
+ scope: ScriptScopeSchema.optional(),
38
+ fsMode: ScriptFsModeSchema.default("none"),
39
+ })
40
+ .refine((body) => Boolean(body.name) !== Boolean(body.source), {
41
+ message: "Provide exactly one of name or source",
42
+ });
43
+
44
+ const searchBodySchema = z.object({
45
+ query: z.string().default(""),
46
+ scope: ScriptScopeSchema.optional(),
47
+ limit: z.number().int().min(1).max(100).default(10),
48
+ });
49
+
50
+ const nameParamsSchema = z.object({ name: scriptNameSchema });
51
+ const scopeQuerySchema = z.object({ scope: ScriptScopeSchema.default("agent") });
52
+ const optionalScopeQuerySchema = z.object({ scope: ScriptScopeSchema.optional() });
53
+
54
+ const upsertRoute = route({
55
+ method: "post",
56
+ path: "/api/scripts/upsert",
57
+ pattern: ["api", "scripts", "upsert"],
58
+ operationId: "scripts_upsert",
59
+ summary: "Create or update a reusable script",
60
+ description: "Explicit script upserts run a TypeScript typecheck before writing.",
61
+ tags: ["Scripts"],
62
+ body: upsertBodySchema,
63
+ responses: {
64
+ 200: { description: "Script upserted" },
65
+ 400: { description: "Validation or typecheck failure" },
66
+ 403: { description: "Global write requires lead agent" },
67
+ },
68
+ });
69
+
70
+ const runRoute = route({
71
+ method: "post",
72
+ path: "/api/scripts/run",
73
+ pattern: ["api", "scripts", "run"],
74
+ operationId: "scripts_run",
75
+ summary: "Run a reusable or inline script",
76
+ description:
77
+ "Inline source skips typecheck and is auto-saved as a scratch script only on success.",
78
+ tags: ["Scripts"],
79
+ body: runBodySchema,
80
+ responses: {
81
+ 200: { description: "Script run completed" },
82
+ 400: { description: "Validation error" },
83
+ 404: { description: "Script not found" },
84
+ 501: { description: "workspace-rw scripts are not supported in v1" },
85
+ },
86
+ });
87
+
88
+ const searchRoute = route({
89
+ method: "post",
90
+ path: "/api/scripts/search",
91
+ pattern: ["api", "scripts", "search"],
92
+ operationId: "scripts_search",
93
+ summary: "Search reusable scripts",
94
+ description: "Phase 3 search is substring-only over script name and metadata.",
95
+ tags: ["Scripts"],
96
+ body: searchBodySchema,
97
+ responses: {
98
+ 200: { description: "Matching scripts" },
99
+ 400: { description: "Validation error" },
100
+ },
101
+ });
102
+
103
+ const deleteRoute = route({
104
+ method: "delete",
105
+ path: "/api/scripts/{name}",
106
+ pattern: ["api", "scripts", null],
107
+ operationId: "scripts_delete",
108
+ summary: "Delete a reusable script",
109
+ tags: ["Scripts"],
110
+ params: nameParamsSchema,
111
+ query: scopeQuerySchema,
112
+ responses: {
113
+ 200: { description: "Delete result" },
114
+ 400: { description: "Validation error" },
115
+ 403: { description: "Global delete requires lead agent" },
116
+ },
117
+ });
118
+
119
+ const typesRoute = route({
120
+ method: "get",
121
+ path: "/api/scripts/{name}/types",
122
+ pattern: ["api", "scripts", null, "types"],
123
+ operationId: "scripts_types",
124
+ summary: "Get script signature and authoring types",
125
+ tags: ["Scripts"],
126
+ params: nameParamsSchema,
127
+ query: optionalScopeQuerySchema,
128
+ responses: {
129
+ 200: { description: "Script signature and type blobs" },
130
+ 404: { description: "Script not found" },
131
+ },
132
+ });
133
+
134
+ function requireAgent(res: ServerResponse, agentId: string | undefined) {
135
+ if (!agentId) {
136
+ jsonError(res, "X-Agent-ID required for scripts API", 400);
137
+ return null;
138
+ }
139
+ const agent = getAgentById(agentId);
140
+ if (!agent) {
141
+ jsonError(res, "Agent not found", 404);
142
+ return null;
143
+ }
144
+ return agent;
145
+ }
146
+
147
+ function signatureJsonFor(source: string): string {
148
+ return JSON.stringify(extractScriptSignature(source));
149
+ }
150
+
151
+ function resolveScript(name: string, agentId: string, scope?: ScriptScope): ScriptRecord | null {
152
+ if (scope === "global") return getScript({ name, scope: "global" });
153
+ if (scope === "agent") return getScript({ name, scope: "agent", scopeId: agentId });
154
+ return (
155
+ getScript({ name, scope: "agent", scopeId: agentId }) ?? getScript({ name, scope: "global" })
156
+ );
157
+ }
158
+
159
+ function scratchSlug(intent: string, source: string): string {
160
+ const base = (intent || "inline-script")
161
+ .toLowerCase()
162
+ .replace(/[^a-z0-9]+/g, "-")
163
+ .replace(/^-+|-+$/g, "")
164
+ .slice(0, 48);
165
+ const hash = new Bun.CryptoHasher("sha256").update(source).digest("hex").slice(0, 8);
166
+ return `scratch-${base || "inline-script"}-${hash}`;
167
+ }
168
+
169
+ function emitGlobalUpsertEvent(args: {
170
+ agentId: string;
171
+ script: ScriptRecord;
172
+ isNew: boolean;
173
+ isPromotion: boolean;
174
+ }) {
175
+ createEvent({
176
+ category: "system",
177
+ event: "script.global_upsert",
178
+ source: "api",
179
+ agentId: args.agentId,
180
+ data: {
181
+ scriptId: args.script.id,
182
+ name: args.script.name,
183
+ version: args.script.version,
184
+ contentHash: args.script.contentHash,
185
+ changedByAgentId: args.agentId,
186
+ isNew: args.isNew,
187
+ isPromotion: args.isPromotion,
188
+ },
189
+ });
190
+ }
191
+
192
+ export async function handleScripts(
193
+ req: IncomingMessage,
194
+ res: ServerResponse,
195
+ pathSegments: string[],
196
+ queryParams: URLSearchParams,
197
+ agentId: string | undefined,
198
+ ): Promise<boolean> {
199
+ if (upsertRoute.match(req.method, pathSegments)) {
200
+ const parsed = await upsertRoute.parse(req, res, pathSegments, queryParams);
201
+ if (!parsed) return true;
202
+ const agent = requireAgent(res, agentId);
203
+ if (!agent) return true;
204
+
205
+ if (parsed.body.scope === "global" && !agent.isLead) {
206
+ jsonError(res, "Global scripts require a lead agent", 403);
207
+ return true;
208
+ }
209
+
210
+ const typecheck = typecheckScript(parsed.body.source);
211
+ if (!typecheck.ok) {
212
+ json(res, { error: "typecheck_failed", diagnostics: typecheck.diagnostics }, 400);
213
+ return true;
214
+ }
215
+
216
+ const existingAgentScript =
217
+ parsed.body.scope === "global"
218
+ ? getScript({ name: parsed.body.name, scope: "agent", scopeId: agent.id })
219
+ : null;
220
+ const result = await upsertScriptByName({
221
+ name: parsed.body.name,
222
+ scope: parsed.body.scope,
223
+ scopeId: parsed.body.scope === "agent" ? agent.id : null,
224
+ source: parsed.body.source,
225
+ description: parsed.body.description,
226
+ intent: parsed.body.intent,
227
+ signatureJson: signatureJsonFor(parsed.body.source),
228
+ fsMode: parsed.body.fsMode,
229
+ agentId: agent.id,
230
+ isScratch: false,
231
+ typeChecked: true,
232
+ });
233
+
234
+ if (parsed.body.scope === "global" && !result.contentDeduped) {
235
+ emitGlobalUpsertEvent({
236
+ agentId: agent.id,
237
+ script: result.script,
238
+ isNew: result.isNew,
239
+ isPromotion: Boolean(existingAgentScript),
240
+ });
241
+ }
242
+
243
+ json(res, {
244
+ name: result.script.name,
245
+ version: result.script.version,
246
+ contentDeduped: result.contentDeduped,
247
+ });
248
+ return true;
249
+ }
250
+
251
+ if (runRoute.match(req.method, pathSegments)) {
252
+ const parsed = await runRoute.parse(req, res, pathSegments, queryParams);
253
+ if (!parsed) return true;
254
+ const agent = requireAgent(res, agentId);
255
+ if (!agent) return true;
256
+
257
+ let source = parsed.body.source;
258
+ let fsMode = parsed.body.fsMode;
259
+ if (parsed.body.name) {
260
+ const script = resolveScript(parsed.body.name, agent.id, parsed.body.scope);
261
+ if (!script) {
262
+ jsonError(res, "Script not found", 404);
263
+ return true;
264
+ }
265
+ source = script.source;
266
+ fsMode = script.fsMode;
267
+ }
268
+
269
+ if (fsMode === "workspace-rw") {
270
+ jsonError(res, "workspace-rw scripts are not supported by /api/scripts/run in v1", 501);
271
+ return true;
272
+ }
273
+
274
+ const output = await runScript({
275
+ source: source as string,
276
+ args: parsed.body.args,
277
+ fsMode,
278
+ agentId: agent.id,
279
+ });
280
+
281
+ let autoSaved: { slug: string; reason: string } | undefined;
282
+ if (parsed.body.source && !output.error && output.exitCode === 0) {
283
+ const slug = scratchSlug(parsed.body.intent, parsed.body.source);
284
+ await upsertScriptByName({
285
+ name: slug,
286
+ scope: "agent",
287
+ scopeId: agent.id,
288
+ source: parsed.body.source,
289
+ description: `Scratch script: ${parsed.body.intent || slug}`,
290
+ intent: parsed.body.intent || "Inline script auto-saved after successful run",
291
+ signatureJson: signatureJsonFor(parsed.body.source),
292
+ fsMode: "none",
293
+ agentId: agent.id,
294
+ isScratch: true,
295
+ typeChecked: false,
296
+ changeReason: "Auto-saved successful inline run",
297
+ });
298
+ autoSaved = { slug, reason: "successful_inline_run" };
299
+ }
300
+
301
+ json(
302
+ res,
303
+ scrubObject({
304
+ result: output.result,
305
+ autoSaved,
306
+ truncated: output.truncated,
307
+ durationMs: output.durationMs,
308
+ stdout: output.stdout,
309
+ stderr: output.stderr,
310
+ exitCode: output.exitCode,
311
+ error: output.error,
312
+ }),
313
+ );
314
+ return true;
315
+ }
316
+
317
+ if (searchRoute.match(req.method, pathSegments)) {
318
+ const parsed = await searchRoute.parse(req, res, pathSegments, queryParams);
319
+ if (!parsed) return true;
320
+ const agent = requireAgent(res, agentId);
321
+ if (!agent) return true;
322
+
323
+ const matches = await searchScripts({
324
+ query: parsed.body.query,
325
+ scope: parsed.body.scope,
326
+ scopeId: agent.id,
327
+ limit: parsed.body.limit,
328
+ });
329
+
330
+ json(res, {
331
+ results: matches.map(({ script, score }) => ({
332
+ name: script.name,
333
+ signature: JSON.parse(script.signatureJson),
334
+ description: script.description,
335
+ score,
336
+ })),
337
+ });
338
+ return true;
339
+ }
340
+
341
+ if (typesRoute.match(req.method, pathSegments)) {
342
+ const parsed = await typesRoute.parse(req, res, pathSegments, queryParams);
343
+ if (!parsed) return true;
344
+ const agent = requireAgent(res, agentId);
345
+ if (!agent) return true;
346
+
347
+ const script = resolveScript(parsed.params.name, agent.id, parsed.query.scope);
348
+ if (!script) {
349
+ jsonError(res, "Script not found", 404);
350
+ return true;
351
+ }
352
+ json(res, {
353
+ signature: JSON.parse(script.signatureJson),
354
+ sdkTypes: SCRIPT_SDK_TYPES,
355
+ stdlibTypes: SCRIPT_STDLIB_TYPES,
356
+ });
357
+ return true;
358
+ }
359
+
360
+ if (deleteRoute.match(req.method, pathSegments)) {
361
+ const parsed = await deleteRoute.parse(req, res, pathSegments, queryParams);
362
+ if (!parsed) return true;
363
+ const agent = requireAgent(res, agentId);
364
+ if (!agent) return true;
365
+
366
+ if (parsed.query.scope === "global" && !agent.isLead) {
367
+ jsonError(res, "Global scripts require a lead agent", 403);
368
+ return true;
369
+ }
370
+
371
+ const deleted = deleteScript({
372
+ name: parsed.params.name,
373
+ scope: parsed.query.scope,
374
+ scopeId: parsed.query.scope === "agent" ? agent.id : null,
375
+ });
376
+ json(res, { deleted });
377
+ return true;
378
+ }
379
+
380
+ return false;
381
+ }
@@ -49,6 +49,10 @@ async function handleTaskCreated(data: unknown): Promise<void> {
49
49
  );
50
50
  }
51
51
 
52
+ // Cap parameter length to avoid oversized Linear GraphQL payloads. Linear renders this in the
53
+ // AgentSession panel; 2000 chars is plenty for a progress update.
54
+ const PROGRESS_PARAMETER_MAX = 2000;
55
+
52
56
  async function handleTaskProgress(data: unknown): Promise<void> {
53
57
  const { taskId, progress } = data as { taskId: string; progress?: string };
54
58
  if (!taskId || !progress) return;
@@ -56,8 +60,11 @@ async function handleTaskProgress(data: unknown): Promise<void> {
56
60
  const sessionId = taskSessionMap.get(taskId);
57
61
  if (!sessionId) return;
58
62
 
59
- // Use 'action' activity type — Linear renders it as a structured tool invocation card
60
- postAgentSessionAction(sessionId, progress).catch((err) => {
63
+ // Post as `action` activity (renders as a structured card in Linear's AgentSession panel).
64
+ // Per Linear's agentActivityCreate spec, `action` requires BOTH `action` AND `parameter`;
65
+ // the original bug here was passing `progress` as `action` with `parameter` undefined.
66
+ const parameter = progress.slice(0, PROGRESS_PARAMETER_MAX);
67
+ postAgentSessionAction(sessionId, "Progress update", parameter).catch((err) => {
61
68
  console.error(`[Linear Outbound] Failed to post progress action for task ${taskId}:`, err);
62
69
  });
63
70
  }
package/src/otel.ts CHANGED
@@ -48,6 +48,11 @@ export function isOtelEnabled(): boolean {
48
48
  return enabled;
49
49
  }
50
50
 
51
+ export function isPollTracingEnabled(): boolean {
52
+ const v = (process.env.OTEL_TRACE_POLL ?? "").toLowerCase();
53
+ return v === "1" || v === "true" || v === "yes" || v === "on";
54
+ }
55
+
51
56
  export async function initOtel(serviceRole = process.env.AGENT_ROLE || "api"): Promise<void> {
52
57
  if (!enabled || initialized) return;
53
58
  initialized = true;