@poncho-ai/harness 0.17.0 → 0.19.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/harness@0.17.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.19.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 196.31 KB
11
- ESM ⚡️ Build success in 155ms
10
+ ESM dist/index.js 198.85 KB
11
+ ESM ⚡️ Build success in 137ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 6569ms
14
- DTS dist/index.d.ts 23.97 KB
13
+ DTS ⚡️ Build success in 7145ms
14
+ DTS dist/index.d.ts 24.33 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`075b9ac`](https://github.com/cesr/poncho-ai/commit/075b9ac3556847af913bf2b58f030575c3b99852) Thanks [@cesr](https://github.com/cesr)! - Batch tool approvals, fix serverless session persistence and adapter init
8
+ - Batch tool approvals: all approval-requiring tool calls in a single step are now collected and presented together instead of one at a time.
9
+ - Fix messaging adapter route registration: routes are only registered after successful initialization, preventing "Adapter not initialised" errors on Vercel.
10
+ - Add stateless signed-cookie sessions so web UI auth survives serverless cold starts.
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [[`075b9ac`](https://github.com/cesr/poncho-ai/commit/075b9ac3556847af913bf2b58f030575c3b99852)]:
15
+ - @poncho-ai/sdk@1.4.0
16
+
17
+ ## 0.18.0
18
+
19
+ ### Minor Changes
20
+
21
+ - [`cd6ccd7`](https://github.com/cesr/poncho-ai/commit/cd6ccd7846e16fbaf17167617666796320ec29ce) Thanks [@cesr](https://github.com/cesr)! - Add MCP custom headers support, tool:generating streaming feedback, and cross-owner subagent recovery
22
+ - **MCP custom headers**: `poncho mcp add --header "Name: value"` and `headers` config field let servers like Arcade receive extra HTTP headers alongside bearer auth.
23
+ - **tool:generating event**: the harness now emits `tool:generating` events when the model begins writing tool-call arguments, so the web UI shows real-time "preparing <tool>" feedback instead of appearing stuck during large tool calls.
24
+ - **Subagent recovery**: `list`/`listSummaries` accept optional `ownerId` so stale-subagent recovery on server restart scans across all owners.
25
+
26
+ ### Patch Changes
27
+
28
+ - Updated dependencies [[`cd6ccd7`](https://github.com/cesr/poncho-ai/commit/cd6ccd7846e16fbaf17167617666796320ec29ce)]:
29
+ - @poncho-ai/sdk@1.3.0
30
+
3
31
  ## 0.17.0
4
32
 
5
33
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -94,6 +94,7 @@ interface Conversation {
94
94
  name: string;
95
95
  input: Record<string, unknown>;
96
96
  }>;
97
+ decision?: "approved" | "denied";
97
98
  }>;
98
99
  ownerId: string;
99
100
  tenantId: string | null;
@@ -205,6 +206,7 @@ interface RemoteMcpServerConfig {
205
206
  type: "bearer";
206
207
  tokenEnv?: string;
207
208
  };
209
+ headers?: Record<string, string>;
208
210
  timeoutMs?: number;
209
211
  reconnectAttempts?: number;
210
212
  reconnectDelayMs?: number;
@@ -267,6 +269,8 @@ type BuiltInToolToggles = {
267
269
  list_directory?: boolean;
268
270
  read_file?: boolean;
269
271
  write_file?: boolean;
272
+ delete_file?: boolean;
273
+ delete_directory?: boolean;
270
274
  };
271
275
  interface MessagingChannelConfig {
272
276
  platform: "slack" | "resend";
@@ -275,6 +279,7 @@ interface MessagingChannelConfig {
275
279
  apiKeyEnv?: string;
276
280
  webhookSecretEnv?: string;
277
281
  fromEnv?: string;
282
+ replyToEnv?: string;
278
283
  allowedSenders?: string[];
279
284
  mode?: "auto-reply" | "tool";
280
285
  allowedRecipients?: string[];
@@ -358,6 +363,8 @@ declare const loadPonchoConfig: (workingDir: string) => Promise<PonchoConfig | u
358
363
 
359
364
  declare const createDefaultTools: (workingDir: string) => ToolDefinition[];
360
365
  declare const createWriteTool: (workingDir: string) => ToolDefinition;
366
+ declare const createDeleteTool: (workingDir: string) => ToolDefinition;
367
+ declare const createDeleteDirectoryTool: (workingDir: string) => ToolDefinition;
361
368
 
362
369
  declare const PONCHO_UPLOAD_SCHEME = "poncho-upload://";
363
370
  interface UploadStore {
@@ -680,4 +687,4 @@ declare class TelemetryEmitter {
680
687
 
681
688
  declare const createSubagentTools: (manager: SubagentManager, getConversationId: () => string | undefined, getOwnerId: () => string) => ToolDefinition[];
682
689
 
683
- export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
690
+ export { type AgentFrontmatter, AgentHarness, type AgentIdentity, type AgentLimitsConfig, type AgentModelConfig, type BuiltInToolToggles, type Conversation, type ConversationState, type ConversationStore, type ConversationSummary, type CronJobConfig, type HarnessOptions, type HarnessRunOutput, InMemoryConversationStore, InMemoryStateStore, LatitudeCapture, type LatitudeCaptureConfig, LocalMcpBridge, LocalUploadStore, type MainMemory, type McpConfig, type MemoryConfig, type MemoryStore, type MessagingChannelConfig, type ModelProviderFactory, PONCHO_UPLOAD_SCHEME, type ParsedAgent, type PonchoConfig, type ProviderConfig, type RemoteMcpServerConfig, type RuntimeRenderContext, S3UploadStore, STORAGE_SCHEMA_VERSION, type SkillContextEntry, type SkillMetadata, type StateConfig, type StateProviderName, type StateStore, type StorageConfig, type SubagentManager, type SubagentResult, type SubagentSummary, type TelemetryConfig, TelemetryEmitter, type ToolAccess, type ToolCall, ToolDispatcher, type ToolExecutionResult, type UploadStore, type UploadsConfig, VercelBlobUploadStore, buildAgentDirectoryName, buildSkillContextWindow, createConversationStore, createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createMemoryStore, createMemoryTools, createModelProvider, createSkillTools, createStateStore, createSubagentTools, createUploadStore, createWriteTool, deriveUploadKey, ensureAgentIdentity, generateAgentId, getAgentStoreDirectory, getModelContextWindow, getPonchoStoreRoot, jsonSchemaToZod, loadPonchoConfig, loadSkillContext, loadSkillInstructions, loadSkillMetadata, normalizeScriptPolicyPath, parseAgentFile, parseAgentMarkdown, readSkillResource, renderAgentPrompt, resolveAgentIdentity, resolveMemoryConfig, resolveSkillDirs, resolveStateConfig, slugifyStorageComponent };
package/dist/index.js CHANGED
@@ -378,7 +378,7 @@ var loadPonchoConfig = async (workingDir) => {
378
378
  };
379
379
 
380
380
  // src/default-tools.ts
381
- import { mkdir, readdir, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
381
+ import { mkdir, readdir, readFile as readFile3, rm, unlink, writeFile as writeFile2 } from "fs/promises";
382
382
  import { dirname, resolve as resolve4, sep } from "path";
383
383
  import { defineTool } from "@poncho-ai/sdk";
384
384
  var resolveSafePath = (workingDir, inputPath) => {
@@ -463,6 +463,52 @@ var createWriteTool = (workingDir) => defineTool({
463
463
  return { path, written: true };
464
464
  }
465
465
  });
466
+ var createDeleteTool = (workingDir) => defineTool({
467
+ name: "delete_file",
468
+ description: "Delete a file at a path inside the working directory",
469
+ inputSchema: {
470
+ type: "object",
471
+ properties: {
472
+ path: {
473
+ type: "string",
474
+ description: "File path relative to working directory"
475
+ }
476
+ },
477
+ required: ["path"],
478
+ additionalProperties: false
479
+ },
480
+ handler: async (input) => {
481
+ const path = typeof input.path === "string" ? input.path : "";
482
+ const resolved = resolveSafePath(workingDir, path);
483
+ await unlink(resolved);
484
+ return { path, deleted: true };
485
+ }
486
+ });
487
+ var createDeleteDirectoryTool = (workingDir) => defineTool({
488
+ name: "delete_directory",
489
+ description: "Recursively delete a directory and all its contents inside the working directory",
490
+ inputSchema: {
491
+ type: "object",
492
+ properties: {
493
+ path: {
494
+ type: "string",
495
+ description: "Directory path relative to working directory"
496
+ }
497
+ },
498
+ required: ["path"],
499
+ additionalProperties: false
500
+ },
501
+ handler: async (input) => {
502
+ const path = typeof input.path === "string" ? input.path : "";
503
+ if (!path) throw new Error("Path must not be empty.");
504
+ const resolved = resolveSafePath(workingDir, path);
505
+ if (resolved === resolve4(workingDir)) {
506
+ throw new Error("Cannot delete the working directory root.");
507
+ }
508
+ await rm(resolved, { recursive: true });
509
+ return { path, deleted: true };
510
+ }
511
+ });
466
512
 
467
513
  // src/harness.ts
468
514
  import { randomUUID as randomUUID3 } from "crypto";
@@ -470,7 +516,7 @@ import { getTextContent as getTextContent2 } from "@poncho-ai/sdk";
470
516
 
471
517
  // src/upload-store.ts
472
518
  import { createHash as createHash2 } from "crypto";
473
- import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm } from "fs/promises";
519
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm as rm2 } from "fs/promises";
474
520
  import { createRequire } from "module";
475
521
  import { resolve as resolve5 } from "path";
476
522
  var tryImport = async (mod, workingDir) => {
@@ -577,7 +623,7 @@ var LocalUploadStore = class {
577
623
  }
578
624
  async delete(urlOrKey) {
579
625
  const key = urlOrKey.startsWith(PONCHO_UPLOAD_SCHEME) ? urlOrKey.slice(PONCHO_UPLOAD_SCHEME.length) : urlOrKey;
580
- await rm(resolve5(this.uploadsDir, key), { force: true });
626
+ await rm2(resolve5(this.uploadsDir, key), { force: true });
581
627
  }
582
628
  };
583
629
  var VercelBlobUploadStore = class {
@@ -1306,16 +1352,19 @@ var StreamableHttpMcpRpcClient = class {
1306
1352
  endpoint;
1307
1353
  timeoutMs;
1308
1354
  bearerToken;
1355
+ customHeaders;
1309
1356
  idCounter = 1;
1310
1357
  initialized = false;
1311
1358
  sessionId;
1312
- constructor(endpoint, timeoutMs = 1e4, bearerToken) {
1359
+ constructor(endpoint, timeoutMs = 1e4, bearerToken, customHeaders) {
1313
1360
  this.endpoint = endpoint;
1314
1361
  this.timeoutMs = timeoutMs;
1315
1362
  this.bearerToken = bearerToken;
1363
+ this.customHeaders = customHeaders ?? {};
1316
1364
  }
1317
1365
  buildHeaders(accept) {
1318
1366
  const headers = {
1367
+ ...this.customHeaders,
1319
1368
  "Content-Type": "application/json",
1320
1369
  Accept: accept
1321
1370
  };
@@ -1596,7 +1645,8 @@ var LocalMcpBridge = class {
1596
1645
  new StreamableHttpMcpRpcClient(
1597
1646
  server.url,
1598
1647
  server.timeoutMs ?? 1e4,
1599
- server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : void 0
1648
+ server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : void 0,
1649
+ server.headers
1600
1650
  )
1601
1651
  );
1602
1652
  }
@@ -2904,6 +2954,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
2904
2954
  RESEND_API_KEY=re_...
2905
2955
  RESEND_WEBHOOK_SECRET=whsec_...
2906
2956
  RESEND_FROM=Agent <agent@yourdomain.com>
2957
+ RESEND_REPLY_TO=support@yourdomain.com # optional
2907
2958
  \`\`\`
2908
2959
  5. Add to \`poncho.config.js\`:
2909
2960
  \`\`\`javascript
@@ -3076,7 +3127,7 @@ var AgentHarness = class {
3076
3127
  isToolEnabled(name) {
3077
3128
  const access3 = this.resolveToolAccess(name);
3078
3129
  if (access3 === false) return false;
3079
- if (name === "write_file") {
3130
+ if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
3080
3131
  return this.shouldEnableWriteTool();
3081
3132
  }
3082
3133
  return true;
@@ -3119,6 +3170,12 @@ var AgentHarness = class {
3119
3170
  if (this.isToolEnabled("write_file")) {
3120
3171
  this.registerIfMissing(createWriteTool(this.workingDir));
3121
3172
  }
3173
+ if (this.isToolEnabled("delete_file")) {
3174
+ this.registerIfMissing(createDeleteTool(this.workingDir));
3175
+ }
3176
+ if (this.isToolEnabled("delete_directory")) {
3177
+ this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
3178
+ }
3122
3179
  }
3123
3180
  shouldEnableWriteTool() {
3124
3181
  const override = process.env.PONCHO_FS_WRITE?.toLowerCase();
@@ -4063,7 +4120,7 @@ ${textContent}` };
4063
4120
  let chunkCount = 0;
4064
4121
  const hasRunTimeout = timeoutMs > 0;
4065
4122
  const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
4066
- const textIterator = result.textStream[Symbol.asyncIterator]();
4123
+ const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
4067
4124
  try {
4068
4125
  while (true) {
4069
4126
  if (isCancelled()) {
@@ -4089,20 +4146,20 @@ ${textContent}` };
4089
4146
  }
4090
4147
  const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
4091
4148
  const timeout = chunkCount === 0 ? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS) : hasRunTimeout ? remaining : 0;
4092
- let nextChunk;
4149
+ let nextPart;
4093
4150
  if (timeout <= 0 && chunkCount > 0) {
4094
- nextChunk = await textIterator.next();
4151
+ nextPart = await fullStreamIterator.next();
4095
4152
  } else {
4096
4153
  let timer;
4097
- nextChunk = await Promise.race([
4098
- textIterator.next(),
4154
+ nextPart = await Promise.race([
4155
+ fullStreamIterator.next(),
4099
4156
  new Promise((resolve10) => {
4100
4157
  timer = setTimeout(() => resolve10(null), timeout);
4101
4158
  })
4102
4159
  ]);
4103
4160
  clearTimeout(timer);
4104
4161
  }
4105
- if (nextChunk === null) {
4162
+ if (nextPart === null) {
4106
4163
  const isFirstChunk = chunkCount === 0;
4107
4164
  console.error(
4108
4165
  `[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`
@@ -4120,13 +4177,19 @@ ${textContent}` };
4120
4177
  });
4121
4178
  return;
4122
4179
  }
4123
- if (nextChunk.done) break;
4124
- chunkCount += 1;
4125
- fullText += nextChunk.value;
4126
- yield pushEvent({ type: "model:chunk", content: nextChunk.value });
4180
+ if (nextPart.done) break;
4181
+ const part = nextPart.value;
4182
+ if (part.type === "text-delta") {
4183
+ chunkCount += 1;
4184
+ fullText += part.text;
4185
+ yield pushEvent({ type: "model:chunk", content: part.text });
4186
+ } else if (part.type === "tool-input-start") {
4187
+ chunkCount += 1;
4188
+ yield pushEvent({ type: "tool:generating", tool: part.toolName, toolCallId: part.id });
4189
+ }
4127
4190
  }
4128
4191
  } finally {
4129
- textIterator.return?.(void 0)?.catch?.(() => {
4192
+ fullStreamIterator.return?.(void 0)?.catch?.(() => {
4130
4193
  });
4131
4194
  }
4132
4195
  if (isCancelled()) {
@@ -4232,6 +4295,7 @@ ${textContent}` };
4232
4295
  const toolResultsForModel = [];
4233
4296
  const richToolResults = [];
4234
4297
  const approvedCalls = [];
4298
+ const approvalNeeded = [];
4235
4299
  for (const call of toolCalls) {
4236
4300
  if (isCancelled()) {
4237
4301
  yield emitCancellation();
@@ -4239,52 +4303,60 @@ ${textContent}` };
4239
4303
  }
4240
4304
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
4241
4305
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
4242
- const requiresApproval = this.requiresApprovalForToolCall(
4243
- runtimeToolName,
4244
- call.input
4245
- );
4246
- if (requiresApproval) {
4247
- const approvalId = `approval_${randomUUID3()}`;
4248
- yield pushEvent({
4249
- type: "tool:approval:required",
4250
- tool: runtimeToolName,
4251
- input: call.input,
4252
- approvalId
4306
+ if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
4307
+ approvalNeeded.push({
4308
+ approvalId: `approval_${randomUUID3()}`,
4309
+ id: call.id,
4310
+ name: runtimeToolName,
4311
+ input: call.input
4253
4312
  });
4254
- const assistantContent2 = JSON.stringify({
4255
- text: fullText,
4256
- tool_calls: toolCalls.map((tc) => ({
4257
- id: tc.id,
4258
- name: exposedToolNames.get(tc.name) ?? tc.name,
4259
- input: tc.input
4260
- }))
4313
+ } else {
4314
+ approvedCalls.push({
4315
+ id: call.id,
4316
+ name: runtimeToolName,
4317
+ input: call.input
4261
4318
  });
4262
- const assistantMsg = {
4263
- role: "assistant",
4264
- content: assistantContent2,
4265
- metadata: { timestamp: now(), id: randomUUID3(), step }
4266
- };
4267
- const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
4319
+ }
4320
+ }
4321
+ if (approvalNeeded.length > 0) {
4322
+ for (const an of approvalNeeded) {
4268
4323
  yield pushEvent({
4269
- type: "tool:approval:checkpoint",
4270
- approvalId,
4271
- tool: runtimeToolName,
4272
- toolCallId: call.id,
4273
- input: call.input,
4274
- checkpointMessages: deltaMessages,
4275
- pendingToolCalls: toolCalls.map((tc) => ({
4276
- id: tc.id,
4277
- name: exposedToolNames.get(tc.name) ?? tc.name,
4278
- input: tc.input
4279
- }))
4324
+ type: "tool:approval:required",
4325
+ tool: an.name,
4326
+ input: an.input,
4327
+ approvalId: an.approvalId
4280
4328
  });
4281
- return;
4282
4329
  }
4283
- approvedCalls.push({
4284
- id: call.id,
4285
- name: runtimeToolName,
4286
- input: call.input
4330
+ const assistantContent2 = JSON.stringify({
4331
+ text: fullText,
4332
+ tool_calls: toolCalls.map((tc) => ({
4333
+ id: tc.id,
4334
+ name: exposedToolNames.get(tc.name) ?? tc.name,
4335
+ input: tc.input
4336
+ }))
4337
+ });
4338
+ const assistantMsg = {
4339
+ role: "assistant",
4340
+ content: assistantContent2,
4341
+ metadata: { timestamp: now(), id: randomUUID3(), step }
4342
+ };
4343
+ const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
4344
+ yield pushEvent({
4345
+ type: "tool:approval:checkpoint",
4346
+ approvals: approvalNeeded.map((an) => ({
4347
+ approvalId: an.approvalId,
4348
+ tool: an.name,
4349
+ toolCallId: an.id,
4350
+ input: an.input
4351
+ })),
4352
+ checkpointMessages: deltaMessages,
4353
+ pendingToolCalls: toolCalls.map((tc) => ({
4354
+ id: tc.id,
4355
+ name: exposedToolNames.get(tc.name) ?? tc.name,
4356
+ input: tc.input
4357
+ }))
4287
4358
  });
4359
+ return;
4288
4360
  }
4289
4361
  const batchStart = now();
4290
4362
  if (isCancelled()) {
@@ -4578,7 +4650,7 @@ var LatitudeCapture = class {
4578
4650
 
4579
4651
  // src/state.ts
4580
4652
  import { randomUUID as randomUUID4 } from "crypto";
4581
- import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm2, writeFile as writeFile5 } from "fs/promises";
4653
+ import { mkdir as mkdir4, readFile as readFile7, readdir as readdir4, rename as rename2, rm as rm3, writeFile as writeFile5 } from "fs/promises";
4582
4654
  import { dirname as dirname4, resolve as resolve9 } from "path";
4583
4655
  var DEFAULT_OWNER = "local-owner";
4584
4656
  var LOCAL_STATE_FILE = "state.json";
@@ -4693,13 +4765,13 @@ var InMemoryConversationStore = class {
4693
4765
  }
4694
4766
  }
4695
4767
  }
4696
- async list(ownerId = DEFAULT_OWNER) {
4768
+ async list(ownerId) {
4697
4769
  this.purgeExpired();
4698
- return Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
4770
+ return Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
4699
4771
  }
4700
- async listSummaries(ownerId = DEFAULT_OWNER) {
4772
+ async listSummaries(ownerId) {
4701
4773
  this.purgeExpired();
4702
- return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
4774
+ return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
4703
4775
  conversationId: c.conversationId,
4704
4776
  title: c.title,
4705
4777
  updatedAt: c.updatedAt,
@@ -4875,9 +4947,9 @@ var FileConversationStore = class {
4875
4947
  });
4876
4948
  await this.writing;
4877
4949
  }
4878
- async list(ownerId = DEFAULT_OWNER) {
4950
+ async list(ownerId) {
4879
4951
  await this.ensureLoaded();
4880
- const summaries = Array.from(this.conversations.values()).filter((conversation) => conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
4952
+ const summaries = Array.from(this.conversations.values()).filter((conversation) => !ownerId || conversation.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt);
4881
4953
  const conversations = [];
4882
4954
  for (const summary of summaries) {
4883
4955
  const loaded = await this.readConversationFile(summary.fileName);
@@ -4887,9 +4959,9 @@ var FileConversationStore = class {
4887
4959
  }
4888
4960
  return conversations;
4889
4961
  }
4890
- async listSummaries(ownerId = DEFAULT_OWNER) {
4962
+ async listSummaries(ownerId) {
4891
4963
  await this.ensureLoaded();
4892
- return Array.from(this.conversations.values()).filter((c) => c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
4964
+ return Array.from(this.conversations.values()).filter((c) => !ownerId || c.ownerId === ownerId).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
4893
4965
  conversationId: c.conversationId,
4894
4966
  title: c.title,
4895
4967
  updatedAt: c.updatedAt,
@@ -4953,7 +5025,7 @@ var FileConversationStore = class {
4953
5025
  if (removed) {
4954
5026
  this.writing = this.writing.then(async () => {
4955
5027
  if (existing) {
4956
- await rm2(resolve9(conversationsDir, existing.fileName), { force: true });
5028
+ await rm3(resolve9(conversationsDir, existing.fileName), { force: true });
4957
5029
  }
4958
5030
  await this.writeIndex();
4959
5031
  });
@@ -5115,11 +5187,14 @@ var KeyValueConversationStoreBase = class {
5115
5187
  return void 0;
5116
5188
  }
5117
5189
  }
5118
- async list(ownerId = DEFAULT_OWNER) {
5190
+ async list(ownerId) {
5119
5191
  const kv = await this.client();
5120
5192
  if (!kv) {
5121
5193
  return await this.memoryFallback.list(ownerId);
5122
5194
  }
5195
+ if (!ownerId) {
5196
+ return [];
5197
+ }
5123
5198
  const ids = await this.getOwnerConversationIds(ownerId);
5124
5199
  const conversations = [];
5125
5200
  for (const id of ids) {
@@ -5134,11 +5209,14 @@ var KeyValueConversationStoreBase = class {
5134
5209
  }
5135
5210
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
5136
5211
  }
5137
- async listSummaries(ownerId = DEFAULT_OWNER) {
5212
+ async listSummaries(ownerId) {
5138
5213
  const kv = await this.client();
5139
5214
  if (!kv) {
5140
5215
  return await this.memoryFallback.listSummaries(ownerId);
5141
5216
  }
5217
+ if (!ownerId) {
5218
+ return [];
5219
+ }
5142
5220
  const ids = await this.getOwnerConversationIds(ownerId);
5143
5221
  const summaries = [];
5144
5222
  for (const id of ids) {
@@ -5695,6 +5773,8 @@ export {
5695
5773
  buildSkillContextWindow,
5696
5774
  createConversationStore,
5697
5775
  createDefaultTools,
5776
+ createDeleteDirectoryTool,
5777
+ createDeleteTool,
5698
5778
  createMemoryStore,
5699
5779
  createMemoryTools,
5700
5780
  createModelProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "redis": "^5.10.0",
32
32
  "yaml": "^2.4.0",
33
33
  "zod": "^3.22.0",
34
- "@poncho-ai/sdk": "1.2.0"
34
+ "@poncho-ai/sdk": "1.4.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/mustache": "^4.2.6",
package/src/config.ts CHANGED
@@ -39,6 +39,8 @@ export type BuiltInToolToggles = {
39
39
  list_directory?: boolean;
40
40
  read_file?: boolean;
41
41
  write_file?: boolean;
42
+ delete_file?: boolean;
43
+ delete_directory?: boolean;
42
44
  };
43
45
 
44
46
  export interface MessagingChannelConfig {
@@ -50,6 +52,7 @@ export interface MessagingChannelConfig {
50
52
  apiKeyEnv?: string;
51
53
  webhookSecretEnv?: string;
52
54
  fromEnv?: string;
55
+ replyToEnv?: string;
53
56
  allowedSenders?: string[];
54
57
  mode?: "auto-reply" | "tool";
55
58
  allowedRecipients?: string[];
@@ -1,4 +1,4 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, rm, unlink, writeFile } from "node:fs/promises";
2
2
  import { dirname, resolve, sep } from "node:path";
3
3
  import { defineTool, type ToolDefinition } from "@poncho-ai/sdk";
4
4
 
@@ -87,3 +87,53 @@ export const createWriteTool = (workingDir: string): ToolDefinition =>
87
87
  return { path, written: true };
88
88
  },
89
89
  });
90
+
91
+ export const createDeleteTool = (workingDir: string): ToolDefinition =>
92
+ defineTool({
93
+ name: "delete_file",
94
+ description: "Delete a file at a path inside the working directory",
95
+ inputSchema: {
96
+ type: "object",
97
+ properties: {
98
+ path: {
99
+ type: "string",
100
+ description: "File path relative to working directory",
101
+ },
102
+ },
103
+ required: ["path"],
104
+ additionalProperties: false,
105
+ },
106
+ handler: async (input) => {
107
+ const path = typeof input.path === "string" ? input.path : "";
108
+ const resolved = resolveSafePath(workingDir, path);
109
+ await unlink(resolved);
110
+ return { path, deleted: true };
111
+ },
112
+ });
113
+
114
+ export const createDeleteDirectoryTool = (workingDir: string): ToolDefinition =>
115
+ defineTool({
116
+ name: "delete_directory",
117
+ description: "Recursively delete a directory and all its contents inside the working directory",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ path: {
122
+ type: "string",
123
+ description: "Directory path relative to working directory",
124
+ },
125
+ },
126
+ required: ["path"],
127
+ additionalProperties: false,
128
+ },
129
+ handler: async (input) => {
130
+ const path = typeof input.path === "string" ? input.path : "";
131
+ if (!path) throw new Error("Path must not be empty.");
132
+ const resolved = resolveSafePath(workingDir, path);
133
+ if (resolved === resolve(workingDir)) {
134
+ throw new Error("Cannot delete the working directory root.");
135
+ }
136
+ await rm(resolved, { recursive: true });
137
+ return { path, deleted: true };
138
+ },
139
+ });
package/src/harness.ts CHANGED
@@ -15,7 +15,7 @@ import type { UploadStore } from "./upload-store.js";
15
15
  import { PONCHO_UPLOAD_SCHEME, deriveUploadKey } from "./upload-store.js";
16
16
  import { parseAgentFile, renderAgentPrompt, type ParsedAgent, type AgentFrontmatter } from "./agent-parser.js";
17
17
  import { loadPonchoConfig, resolveMemoryConfig, type PonchoConfig, type ToolAccess, type BuiltInToolToggles } from "./config.js";
18
- import { createDefaultTools, createWriteTool } from "./default-tools.js";
18
+ import { createDefaultTools, createDeleteDirectoryTool, createDeleteTool, createWriteTool } from "./default-tools.js";
19
19
  import {
20
20
  createMemoryStore,
21
21
  createMemoryTools,
@@ -324,6 +324,7 @@ The agent will respond in Slack threads when @mentioned. Each Slack thread maps
324
324
  RESEND_API_KEY=re_...
325
325
  RESEND_WEBHOOK_SECRET=whsec_...
326
326
  RESEND_FROM=Agent <agent@yourdomain.com>
327
+ RESEND_REPLY_TO=support@yourdomain.com # optional
327
328
  \`\`\`
328
329
  5. Add to \`poncho.config.js\`:
329
330
  \`\`\`javascript
@@ -526,7 +527,7 @@ export class AgentHarness {
526
527
  private isToolEnabled(name: string): boolean {
527
528
  const access = this.resolveToolAccess(name);
528
529
  if (access === false) return false;
529
- if (name === "write_file") {
530
+ if (name === "write_file" || name === "delete_file" || name === "delete_directory") {
530
531
  return this.shouldEnableWriteTool();
531
532
  }
532
533
  return true;
@@ -574,6 +575,12 @@ export class AgentHarness {
574
575
  if (this.isToolEnabled("write_file")) {
575
576
  this.registerIfMissing(createWriteTool(this.workingDir));
576
577
  }
578
+ if (this.isToolEnabled("delete_file")) {
579
+ this.registerIfMissing(createDeleteTool(this.workingDir));
580
+ }
581
+ if (this.isToolEnabled("delete_directory")) {
582
+ this.registerIfMissing(createDeleteDirectoryTool(this.workingDir));
583
+ }
577
584
  }
578
585
 
579
586
  private shouldEnableWriteTool(): boolean {
@@ -1682,15 +1689,14 @@ ${boundedMainMemory.trim()}`
1682
1689
  isEnabled: telemetryEnabled && !!this.latitudeTelemetry,
1683
1690
  },
1684
1691
  });
1685
- // Stream text chunksenforce overall run timeout per chunk.
1686
- // The top-of-step timeout check cannot fire while we are
1687
- // blocked inside the textStream async iterator, so we race
1688
- // each next() call against the remaining time budget.
1692
+ // Stream full responseuse fullStream to get visibility into
1693
+ // tool-call generation (tool-input-start) in addition to text deltas.
1694
+ // Enforce overall run timeout per part.
1689
1695
  let fullText = "";
1690
1696
  let chunkCount = 0;
1691
1697
  const hasRunTimeout = timeoutMs > 0;
1692
1698
  const streamDeadline = hasRunTimeout ? start + timeoutMs : 0;
1693
- const textIterator = result.textStream[Symbol.asyncIterator]();
1699
+ const fullStreamIterator = result.fullStream[Symbol.asyncIterator]();
1694
1700
  try {
1695
1701
  while (true) {
1696
1702
  if (isCancelled()) {
@@ -1714,21 +1720,17 @@ ${boundedMainMemory.trim()}`
1714
1720
  return;
1715
1721
  }
1716
1722
  }
1717
- // Use a shorter timeout for the first chunk to detect
1718
- // non-responsive models quickly instead of waiting minutes.
1719
- // When no run timeout is set, only the first chunk is time-bounded.
1720
1723
  const remaining = hasRunTimeout ? streamDeadline - now() : Infinity;
1721
1724
  const timeout = chunkCount === 0
1722
1725
  ? Math.min(remaining, FIRST_CHUNK_TIMEOUT_MS)
1723
1726
  : hasRunTimeout ? remaining : 0;
1724
- let nextChunk: IteratorResult<string> | null;
1727
+ let nextPart: IteratorResult<(typeof result.fullStream) extends AsyncIterable<infer T> ? T : never> | null;
1725
1728
  if (timeout <= 0 && chunkCount > 0) {
1726
- // No time budget — await the stream directly (development mode, no run timeout)
1727
- nextChunk = await textIterator.next();
1729
+ nextPart = await fullStreamIterator.next();
1728
1730
  } else {
1729
1731
  let timer: ReturnType<typeof setTimeout> | undefined;
1730
- nextChunk = await Promise.race([
1731
- textIterator.next(),
1732
+ nextPart = await Promise.race([
1733
+ fullStreamIterator.next(),
1732
1734
  new Promise<null>((resolve) => {
1733
1735
  timer = setTimeout(() => resolve(null), timeout);
1734
1736
  }),
@@ -1736,16 +1738,14 @@ ${boundedMainMemory.trim()}`
1736
1738
  clearTimeout(timer);
1737
1739
  }
1738
1740
 
1739
- if (nextChunk === null) {
1741
+ if (nextPart === null) {
1740
1742
  const isFirstChunk = chunkCount === 0;
1741
1743
  console.error(
1742
1744
  `[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`,
1743
1745
  );
1744
1746
  if (isFirstChunk) {
1745
- // Throw so the step-level retry logic can handle it.
1746
1747
  throw new FirstChunkTimeoutError(modelName, FIRST_CHUNK_TIMEOUT_MS);
1747
1748
  }
1748
- // Mid-stream timeout: not retryable (partial response would be lost)
1749
1749
  yield pushEvent({
1750
1750
  type: "run:error",
1751
1751
  runId,
@@ -1757,14 +1757,20 @@ ${boundedMainMemory.trim()}`
1757
1757
  return;
1758
1758
  }
1759
1759
 
1760
- if (nextChunk.done) break;
1761
- chunkCount += 1;
1762
- fullText += nextChunk.value;
1763
- yield pushEvent({ type: "model:chunk", content: nextChunk.value });
1760
+ if (nextPart.done) break;
1761
+ const part = nextPart.value;
1762
+
1763
+ if (part.type === "text-delta") {
1764
+ chunkCount += 1;
1765
+ fullText += part.text;
1766
+ yield pushEvent({ type: "model:chunk", content: part.text });
1767
+ } else if (part.type === "tool-input-start") {
1768
+ chunkCount += 1;
1769
+ yield pushEvent({ type: "tool:generating", tool: part.toolName, toolCallId: part.id });
1770
+ }
1764
1771
  }
1765
1772
  } finally {
1766
- // Best-effort cleanup of the underlying stream/connection.
1767
- textIterator.return?.(undefined)?.catch?.(() => {});
1773
+ fullStreamIterator.return?.(undefined)?.catch?.(() => {});
1768
1774
  }
1769
1775
 
1770
1776
  if (isCancelled()) {
@@ -1773,8 +1779,6 @@ ${boundedMainMemory.trim()}`
1773
1779
  }
1774
1780
 
1775
1781
  // Check finish reason for error / abnormal completions.
1776
- // textStream silently swallows model-level errors – they only
1777
- // surface through finishReason (or fullStream, which we don't use).
1778
1782
  const finishReason = await result.finishReason;
1779
1783
 
1780
1784
  if (finishReason === "error") {
@@ -1909,7 +1913,14 @@ ${boundedMainMemory.trim()}`
1909
1913
  name: string;
1910
1914
  input: Record<string, unknown>;
1911
1915
  }> = [];
1916
+ const approvalNeeded: Array<{
1917
+ approvalId: string;
1918
+ id: string;
1919
+ name: string;
1920
+ input: Record<string, unknown>;
1921
+ }> = [];
1912
1922
 
1923
+ // Phase 1: classify all tool calls
1913
1924
  for (const call of toolCalls) {
1914
1925
  if (isCancelled()) {
1915
1926
  yield emitCancellation();
@@ -1917,54 +1928,66 @@ ${boundedMainMemory.trim()}`
1917
1928
  }
1918
1929
  const runtimeToolName = exposedToolNames.get(call.name) ?? call.name;
1919
1930
  yield pushEvent({ type: "tool:started", tool: runtimeToolName, input: call.input });
1920
- const requiresApproval = this.requiresApprovalForToolCall(
1921
- runtimeToolName,
1922
- call.input,
1923
- );
1924
- if (requiresApproval) {
1925
- const approvalId = `approval_${randomUUID()}`;
1926
- yield pushEvent({
1927
- type: "tool:approval:required",
1928
- tool: runtimeToolName,
1931
+ if (this.requiresApprovalForToolCall(runtimeToolName, call.input)) {
1932
+ approvalNeeded.push({
1933
+ approvalId: `approval_${randomUUID()}`,
1934
+ id: call.id,
1935
+ name: runtimeToolName,
1929
1936
  input: call.input,
1930
- approvalId,
1931
1937
  });
1932
-
1933
- const assistantContent = JSON.stringify({
1934
- text: fullText,
1935
- tool_calls: toolCalls.map(tc => ({
1936
- id: tc.id,
1937
- name: exposedToolNames.get(tc.name) ?? tc.name,
1938
- input: tc.input,
1939
- })),
1938
+ } else {
1939
+ approvedCalls.push({
1940
+ id: call.id,
1941
+ name: runtimeToolName,
1942
+ input: call.input,
1940
1943
  });
1941
- const assistantMsg: Message = {
1942
- role: "assistant",
1943
- content: assistantContent,
1944
- metadata: { timestamp: now(), id: randomUUID(), step },
1945
- };
1946
- const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
1944
+ }
1945
+ }
1946
+
1947
+ // Phase 2a: if any tools need approval, emit events for ALL of them and checkpoint
1948
+ if (approvalNeeded.length > 0) {
1949
+ for (const an of approvalNeeded) {
1947
1950
  yield pushEvent({
1948
- type: "tool:approval:checkpoint",
1949
- approvalId,
1950
- tool: runtimeToolName,
1951
- toolCallId: call.id,
1952
- input: call.input,
1953
- checkpointMessages: deltaMessages,
1954
- pendingToolCalls: toolCalls.map(tc => ({
1955
- id: tc.id,
1956
- name: exposedToolNames.get(tc.name) ?? tc.name,
1957
- input: tc.input,
1958
- })),
1951
+ type: "tool:approval:required",
1952
+ tool: an.name,
1953
+ input: an.input,
1954
+ approvalId: an.approvalId,
1959
1955
  });
1960
- return;
1961
1956
  }
1962
- approvedCalls.push({
1963
- id: call.id,
1964
- name: runtimeToolName,
1965
- input: call.input,
1957
+
1958
+ const assistantContent = JSON.stringify({
1959
+ text: fullText,
1960
+ tool_calls: toolCalls.map(tc => ({
1961
+ id: tc.id,
1962
+ name: exposedToolNames.get(tc.name) ?? tc.name,
1963
+ input: tc.input,
1964
+ })),
1966
1965
  });
1966
+ const assistantMsg: Message = {
1967
+ role: "assistant",
1968
+ content: assistantContent,
1969
+ metadata: { timestamp: now(), id: randomUUID(), step },
1970
+ };
1971
+ const deltaMessages = [...messages.slice(inputMessageCount), assistantMsg];
1972
+ yield pushEvent({
1973
+ type: "tool:approval:checkpoint",
1974
+ approvals: approvalNeeded.map(an => ({
1975
+ approvalId: an.approvalId,
1976
+ tool: an.name,
1977
+ toolCallId: an.id,
1978
+ input: an.input,
1979
+ })),
1980
+ checkpointMessages: deltaMessages,
1981
+ pendingToolCalls: toolCalls.map(tc => ({
1982
+ id: tc.id,
1983
+ name: exposedToolNames.get(tc.name) ?? tc.name,
1984
+ input: tc.input,
1985
+ })),
1986
+ });
1987
+ return;
1967
1988
  }
1989
+
1990
+ // Phase 2b: no approvals needed — execute all auto-approved calls
1968
1991
  const batchStart = now();
1969
1992
  if (isCancelled()) {
1970
1993
  yield emitCancellation();
package/src/mcp.ts CHANGED
@@ -12,6 +12,7 @@ export interface RemoteMcpServerConfig {
12
12
  type: "bearer";
13
13
  tokenEnv?: string;
14
14
  };
15
+ headers?: Record<string, string>;
15
16
  timeoutMs?: number;
16
17
  reconnectAttempts?: number;
17
18
  reconnectDelayMs?: number;
@@ -46,18 +47,26 @@ class StreamableHttpMcpRpcClient implements McpRpcClient {
46
47
  private readonly endpoint: string;
47
48
  private readonly timeoutMs: number;
48
49
  private readonly bearerToken?: string;
50
+ private readonly customHeaders: Record<string, string>;
49
51
  private idCounter = 1;
50
52
  private initialized = false;
51
53
  private sessionId?: string;
52
54
 
53
- constructor(endpoint: string, timeoutMs = 10_000, bearerToken?: string) {
55
+ constructor(
56
+ endpoint: string,
57
+ timeoutMs = 10_000,
58
+ bearerToken?: string,
59
+ customHeaders?: Record<string, string>,
60
+ ) {
54
61
  this.endpoint = endpoint;
55
62
  this.timeoutMs = timeoutMs;
56
63
  this.bearerToken = bearerToken;
64
+ this.customHeaders = customHeaders ?? {};
57
65
  }
58
66
 
59
67
  private buildHeaders(accept: string): Record<string, string> {
60
68
  const headers: Record<string, string> = {
69
+ ...this.customHeaders,
61
70
  "Content-Type": "application/json",
62
71
  Accept: accept,
63
72
  };
@@ -367,6 +376,7 @@ export class LocalMcpBridge {
367
376
  server.url,
368
377
  server.timeoutMs ?? 10_000,
369
378
  server.auth?.tokenEnv ? process.env[server.auth.tokenEnv] : undefined,
379
+ server.headers,
370
380
  ),
371
381
  );
372
382
  }
package/src/state.ts CHANGED
@@ -35,6 +35,7 @@ export interface Conversation {
35
35
  checkpointMessages?: Message[];
36
36
  baseMessageCount?: number;
37
37
  pendingToolCalls?: Array<{ id: string; name: string; input: Record<string, unknown> }>;
38
+ decision?: "approved" | "denied";
38
39
  }>;
39
40
  ownerId: string;
40
41
  tenantId: string | null;
@@ -223,17 +224,17 @@ export class InMemoryConversationStore implements ConversationStore {
223
224
  }
224
225
  }
225
226
 
226
- async list(ownerId = DEFAULT_OWNER): Promise<Conversation[]> {
227
+ async list(ownerId?: string): Promise<Conversation[]> {
227
228
  this.purgeExpired();
228
229
  return Array.from(this.conversations.values())
229
- .filter((conversation) => conversation.ownerId === ownerId)
230
+ .filter((conversation) => !ownerId || conversation.ownerId === ownerId)
230
231
  .sort((a, b) => b.updatedAt - a.updatedAt);
231
232
  }
232
233
 
233
- async listSummaries(ownerId = DEFAULT_OWNER): Promise<ConversationSummary[]> {
234
+ async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
234
235
  this.purgeExpired();
235
236
  return Array.from(this.conversations.values())
236
- .filter((c) => c.ownerId === ownerId)
237
+ .filter((c) => !ownerId || c.ownerId === ownerId)
237
238
  .sort((a, b) => b.updatedAt - a.updatedAt)
238
239
  .map((c) => ({
239
240
  conversationId: c.conversationId,
@@ -454,10 +455,10 @@ class FileConversationStore implements ConversationStore {
454
455
  await this.writing;
455
456
  }
456
457
 
457
- async list(ownerId = DEFAULT_OWNER): Promise<Conversation[]> {
458
+ async list(ownerId?: string): Promise<Conversation[]> {
458
459
  await this.ensureLoaded();
459
460
  const summaries = Array.from(this.conversations.values())
460
- .filter((conversation) => conversation.ownerId === ownerId)
461
+ .filter((conversation) => !ownerId || conversation.ownerId === ownerId)
461
462
  .sort((a, b) => b.updatedAt - a.updatedAt);
462
463
  const conversations: Conversation[] = [];
463
464
  for (const summary of summaries) {
@@ -469,10 +470,10 @@ class FileConversationStore implements ConversationStore {
469
470
  return conversations;
470
471
  }
471
472
 
472
- async listSummaries(ownerId = DEFAULT_OWNER): Promise<ConversationSummary[]> {
473
+ async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
473
474
  await this.ensureLoaded();
474
475
  return Array.from(this.conversations.values())
475
- .filter((c) => c.ownerId === ownerId)
476
+ .filter((c) => !ownerId || c.ownerId === ownerId)
476
477
  .sort((a, b) => b.updatedAt - a.updatedAt)
477
478
  .map((c) => ({
478
479
  conversationId: c.conversationId,
@@ -751,11 +752,15 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
751
752
  }
752
753
  }
753
754
 
754
- async list(ownerId = DEFAULT_OWNER): Promise<Conversation[]> {
755
+ async list(ownerId?: string): Promise<Conversation[]> {
755
756
  const kv = await this.client();
756
757
  if (!kv) {
757
758
  return await this.memoryFallback.list(ownerId);
758
759
  }
760
+ if (!ownerId) {
761
+ // KV stores index per-owner; cross-owner listing not supported
762
+ return [];
763
+ }
759
764
  const ids = await this.getOwnerConversationIds(ownerId);
760
765
  const conversations: Conversation[] = [];
761
766
  for (const id of ids) {
@@ -772,11 +777,14 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
772
777
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
773
778
  }
774
779
 
775
- async listSummaries(ownerId = DEFAULT_OWNER): Promise<ConversationSummary[]> {
780
+ async listSummaries(ownerId?: string): Promise<ConversationSummary[]> {
776
781
  const kv = await this.client();
777
782
  if (!kv) {
778
783
  return await this.memoryFallback.listSummaries(ownerId);
779
784
  }
785
+ if (!ownerId) {
786
+ return [];
787
+ }
780
788
  const ids = await this.getOwnerConversationIds(ownerId);
781
789
  const summaries: ConversationSummary[] = [];
782
790
  for (const id of ids) {
package/test/mcp.test.ts CHANGED
@@ -126,6 +126,89 @@ describe("mcp bridge protocol transports", () => {
126
126
  expect(deleteSeen).toBe(true);
127
127
  });
128
128
 
129
+ it("sends custom headers alongside bearer token", async () => {
130
+ process.env.LINEAR_TOKEN = "token-123";
131
+ let capturedHeaders: Record<string, string | undefined> = {};
132
+ const server = createServer(async (req, res) => {
133
+ if (req.method === "DELETE") {
134
+ res.statusCode = 200;
135
+ res.end();
136
+ return;
137
+ }
138
+ const chunks: Buffer[] = [];
139
+ for await (const chunk of req) chunks.push(Buffer.from(chunk));
140
+ const body = Buffer.concat(chunks).toString("utf8");
141
+ const payload = body.trim().length > 0 ? (JSON.parse(body) as any) : {};
142
+ capturedHeaders = {
143
+ authorization: req.headers.authorization,
144
+ "x-custom-id": req.headers["x-custom-id"] as string | undefined,
145
+ "x-another": req.headers["x-another"] as string | undefined,
146
+ };
147
+ if (payload.method === "initialize") {
148
+ res.setHeader("Content-Type", "application/json");
149
+ res.setHeader("Mcp-Session-Id", "s");
150
+ res.end(
151
+ JSON.stringify({
152
+ jsonrpc: "2.0",
153
+ id: payload.id,
154
+ result: {
155
+ protocolVersion: "2025-03-26",
156
+ capabilities: { tools: { listChanged: true } },
157
+ serverInfo: { name: "remote", version: "1.0.0" },
158
+ },
159
+ }),
160
+ );
161
+ return;
162
+ }
163
+ if (payload.method === "notifications/initialized") {
164
+ res.statusCode = 202;
165
+ res.end();
166
+ return;
167
+ }
168
+ if (payload.method === "tools/list") {
169
+ res.setHeader("Content-Type", "application/json");
170
+ res.end(
171
+ JSON.stringify({
172
+ jsonrpc: "2.0",
173
+ id: payload.id,
174
+ result: {
175
+ tools: [
176
+ { name: "ping", inputSchema: { type: "object", properties: {} } },
177
+ ],
178
+ },
179
+ }),
180
+ );
181
+ return;
182
+ }
183
+ res.statusCode = 404;
184
+ res.end();
185
+ });
186
+ await new Promise<void>((r) => server.listen(0, () => r()));
187
+ const address = server.address();
188
+ if (!address || typeof address === "string") throw new Error("Unexpected address");
189
+
190
+ const bridge = new LocalMcpBridge({
191
+ mcp: [
192
+ {
193
+ name: "custom-headers",
194
+ url: `http://127.0.0.1:${address.port}/mcp`,
195
+ auth: { type: "bearer", tokenEnv: "LINEAR_TOKEN" },
196
+ headers: { "X-Custom-Id": "user-42", "X-Another": "value" },
197
+ },
198
+ ],
199
+ });
200
+
201
+ await bridge.startLocalServers();
202
+ await bridge.discoverTools();
203
+
204
+ expect(capturedHeaders.authorization).toBe("Bearer token-123");
205
+ expect(capturedHeaders["x-custom-id"]).toBe("user-42");
206
+ expect(capturedHeaders["x-another"]).toBe("value");
207
+
208
+ await bridge.stopLocalServers();
209
+ await new Promise<void>((r) => server.close(() => r()));
210
+ });
211
+
129
212
  it("fails fast on duplicate server names", () => {
130
213
  expect(
131
214
  () =>