@poncho-ai/harness 0.22.0 → 0.23.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.22.0 build /home/runner/work/poncho-ai/poncho-ai/packages/harness
2
+ > @poncho-ai/harness@0.23.0 build /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
3
  > node scripts/embed-docs.js && tsup src/index.ts --format esm --dts
4
4
 
5
5
  [embed-docs] Generated poncho-docs.ts with 4 topics
@@ -8,8 +8,8 @@
8
8
  CLI tsup v8.5.1
9
9
  CLI Target: es2022
10
10
  ESM Build start
11
- ESM dist/index.js 257.83 KB
12
- ESM ⚡️ Build success in 146ms
11
+ ESM dist/index.js 261.68 KB
12
+ ESM ⚡️ Build success in 128ms
13
13
  DTS Build start
14
- DTS ⚡️ Build success in 7680ms
15
- DTS dist/index.d.ts 27.07 KB
14
+ DTS ⚡️ Build success in 4463ms
15
+ DTS dist/index.d.ts 27.40 KB
@@ -0,0 +1,6 @@
1
+
2
+ > @poncho-ai/harness@0.11.2 lint /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
+ > eslint src/
4
+
5
+ sh: eslint: command not found
6
+  ELIFECYCLE  Command failed.
@@ -0,0 +1,135 @@
1
+
2
+ > @poncho-ai/harness@0.16.1 test /Users/cesar/Dev/latitude/poncho-ai/packages/harness
3
+ > vitest
4
+
5
+
6
+  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/harness
7
+
8
+ ✓ test/telemetry.test.ts  (3 tests) 2ms
9
+ [event] step:completed {"type":"step:completed","step":1,"duration":1}
10
+ [event] step:started {"type":"step:started","step":2}
11
+ ✓ test/schema-converter.test.ts  (27 tests) 19ms
12
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > discovers and calls tools over streamable HTTP
13
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
14
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
15
+
16
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > selects discovered tools by requested patterns
17
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
18
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
19
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
20
+
21
+ ✓ test/agent-parser.test.ts  (10 tests) 24ms
22
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
23
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":0,"filteredByPolicyCount":0,"filteredByIntentCount":0}
24
+
25
+ stderr | test/mcp.test.ts > mcp bridge protocol transports > skips discovery when bearer token env value is missing
26
+ [poncho][mcp] {"event":"auth.token_missing","server":"remote","tokenEnv":"MISSING_TOKEN_ENV"}
27
+
28
+ stdout | test/mcp.test.ts > mcp bridge protocol transports > returns actionable errors for 403 permission failures
29
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
30
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
31
+
32
+ ✓ test/mcp.test.ts  (6 tests) 81ms
33
+ ✓ test/memory.test.ts  (4 tests) 56ms
34
+ ✓ test/state.test.ts  (5 tests) 237ms
35
+ ✓ test/model-factory.test.ts  (4 tests) 2ms
36
+ ✓ test/agent-identity.test.ts  (2 tests) 43ms
37
+ stdout | test/harness.test.ts > agent harness > registers default filesystem tools
38
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
39
+
40
+ stdout | test/harness.test.ts > agent harness > disables write_file by default in production environment
41
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
42
+
43
+ stdout | test/harness.test.ts > agent harness > allows disabling built-in tools via poncho.config.js
44
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
45
+
46
+ stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
47
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
48
+
49
+ stdout | test/harness.test.ts > agent harness > supports per-environment tool overrides
50
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
51
+
52
+ stdout | test/harness.test.ts > agent harness > does not auto-register exported tool objects from skill scripts
53
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
54
+
55
+ stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
56
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
57
+
58
+ stdout | test/harness.test.ts > agent harness > refreshes skill metadata and tools in development mode
59
+ [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
60
+ [poncho][mcp] {"event":"tools.cleared","reason":"activate:beta","requestedPatterns":[]}
61
+
62
+ stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
63
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
64
+ [poncho][mcp] {"event":"tools.cleared","reason":"activate:obsolete","requestedPatterns":[]}
65
+
66
+ stdout | test/harness.test.ts > agent harness > prunes removed active skills after refresh in development mode
67
+ [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
68
+
69
+ stdout | test/harness.test.ts > agent harness > does not refresh skills outside development mode
70
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
71
+
72
+ stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
73
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
74
+ [poncho][mcp] {"event":"tools.cleared","reason":"activate:alpha","requestedPatterns":[]}
75
+
76
+ stdout | test/harness.test.ts > agent harness > clears active skills when skill metadata changes in development mode
77
+ [poncho][mcp] {"event":"tools.cleared","reason":"skills:changed","requestedPatterns":[]}
78
+
79
+ stdout | test/harness.test.ts > agent harness > lists skill scripts through list_skill_scripts
80
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
81
+
82
+ stdout | test/harness.test.ts > agent harness > runs JavaScript/TypeScript skill scripts through run_skill_script
83
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
84
+
85
+ stdout | test/harness.test.ts > agent harness > runs AGENT-scope scripts from root scripts directory
86
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
87
+
88
+ stdout | test/harness.test.ts > agent harness > blocks path traversal in run_skill_script
89
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
90
+
91
+ stdout | test/harness.test.ts > agent harness > requires allowed-tools entries for non-standard script directories
92
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
93
+
94
+ stdout | test/harness.test.ts > agent harness > registers MCP tools dynamically for stacked active skills and supports deactivation
95
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":2}
96
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
97
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
98
+ [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-a","requestedPatterns":["remote/a"],"registeredCount":1,"activeSkills":["skill-a"]}
99
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":2,"registeredCount":2,"filteredByPolicyCount":0,"filteredByIntentCount":0}
100
+ [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-b","requestedPatterns":["remote/a","remote/b"],"registeredCount":2,"activeSkills":["skill-a","skill-b"]}
101
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":1}
102
+ [poncho][mcp] {"event":"tools.refreshed","reason":"deactivate:skill-a","requestedPatterns":["remote/b"],"registeredCount":1,"activeSkills":["skill-b"]}
103
+
104
+ stdout | test/harness.test.ts > agent harness > supports flat tool access config format
105
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
106
+
107
+ stdout | test/harness.test.ts > agent harness > flat tool access takes priority over legacy defaults
108
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
109
+
110
+ stdout | test/harness.test.ts > agent harness > byEnvironment overrides flat tool access
111
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
112
+
113
+ stdout | test/harness.test.ts > agent harness > registerTools skips tools disabled via config
114
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
115
+
116
+ stdout | test/harness.test.ts > agent harness > approval access level registers the tool but marks it for approval
117
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
118
+
119
+ stdout | test/harness.test.ts > agent harness > tools without approval config do not require approval
120
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
121
+
122
+ stdout | test/harness.test.ts > agent harness > allows in-flight MCP calls to finish after skill deactivation
123
+ [poncho][mcp] {"event":"catalog.loaded","server":"remote","discoveredCount":1}
124
+ [poncho][mcp] {"event":"tools.cleared","reason":"initialize","requestedPatterns":[]}
125
+ [poncho][mcp] {"event":"tools.selected","requestedPatternCount":1,"registeredCount":1,"filteredByPolicyCount":0,"filteredByIntentCount":0}
126
+ [poncho][mcp] {"event":"tools.refreshed","reason":"activate:skill-slow","requestedPatterns":["remote/slow"],"registeredCount":1,"activeSkills":["skill-slow"]}
127
+ [poncho][mcp] {"event":"tools.cleared","reason":"deactivate:skill-slow","requestedPatterns":[]}
128
+
129
+ ✓ test/harness.test.ts  (25 tests) 291ms
130
+
131
+  Test Files  9 passed (9)
132
+  Tests  86 passed (86)
133
+  Start at  17:47:43
134
+  Duration  1.88s (transform 684ms, setup 1ms, collect 2.34s, tests 755ms, environment 2ms, prepare 1.27s)
135
+
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @poncho-ai/harness
2
2
 
3
+ ## 0.23.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`d1e1bfb`](https://github.com/cesr/poncho-ai/commit/d1e1bfbf35b18788ab79231ca675774e949f5116) Thanks [@cesr](https://github.com/cesr)! - Add proactive scheduled messaging via channel-targeted cron jobs. Cron jobs with `channel: telegram` (or `slack`) now automatically discover known conversations and send the agent's response directly to each chat, continuing the existing conversation history.
8
+
9
+ ## 0.22.1
10
+
11
+ ### Patch Changes
12
+
13
+ - [`096953d`](https://github.com/cesr/poncho-ai/commit/096953d5a64a785950ea0a7f09e2183e481afd29) Thanks [@cesr](https://github.com/cesr)! - Improve time-to-first-token by lazy-loading the recall corpus
14
+
15
+ The recall corpus (past conversation summaries) is now fetched on-demand only when the LLM invokes the `conversation_recall` tool, instead of blocking every message with ~1.3s of upfront I/O. Also adds batch `mget` support to Upstash/Redis/DynamoDB conversation stores, parallelizes memory fetch with skill refresh, debounces skill refresh in dev mode, and caches message conversions across multi-step runs.
16
+
3
17
  ## 0.22.0
4
18
 
5
19
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -19,6 +19,7 @@ interface CronJobConfig {
19
19
  schedule: string;
20
20
  task: string;
21
21
  timezone?: string;
22
+ channel?: string;
22
23
  }
23
24
  interface AgentFrontmatter {
24
25
  name: string;
@@ -155,6 +156,11 @@ interface Conversation {
155
156
  result?: _poncho_ai_sdk.RunResult;
156
157
  error?: _poncho_ai_sdk.AgentFailure;
157
158
  };
159
+ channelMeta?: {
160
+ platform: string;
161
+ channelId: string;
162
+ platformThreadId: string;
163
+ };
158
164
  createdAt: number;
159
165
  updatedAt: number;
160
166
  }
@@ -208,6 +214,11 @@ type ConversationSummary = {
208
214
  parentConversationId?: string;
209
215
  messageCount?: number;
210
216
  hasPendingApprovals?: boolean;
217
+ channelMeta?: {
218
+ platform: string;
219
+ channelId: string;
220
+ platformThreadId: string;
221
+ };
211
222
  };
212
223
  declare const createStateStore: (config?: StateConfig, options?: {
213
224
  workingDir?: string;
@@ -562,6 +573,7 @@ declare class AgentHarness {
562
573
  private loadedConfig?;
563
574
  private loadedSkills;
564
575
  private skillFingerprint;
576
+ private lastSkillRefreshAt;
565
577
  private readonly activeSkillNames;
566
578
  private readonly registeredMcpToolNames;
567
579
  private latitudeTelemetry?;
@@ -601,6 +613,7 @@ declare class AgentHarness {
601
613
  private refreshMcpTools;
602
614
  private buildSkillFingerprint;
603
615
  private registerSkillTools;
616
+ private static readonly SKILL_REFRESH_DEBOUNCE_MS;
604
617
  private refreshSkillsIfChanged;
605
618
  initialize(): Promise<void>;
606
619
  private buildBrowserStoragePersistence;
package/dist/index.js CHANGED
@@ -112,10 +112,12 @@ var parseCronJobs = (value) => {
112
112
  if (timezone) {
113
113
  validateTimezone(timezone, path);
114
114
  }
115
+ const channel = typeof jobValue.channel === "string" && jobValue.channel.trim() ? jobValue.channel.trim() : void 0;
115
116
  jobs[jobName] = {
116
117
  schedule: jobValue.schedule.trim(),
117
118
  task: jobValue.task,
118
- timezone
119
+ timezone,
120
+ channel
119
121
  };
120
122
  }
121
123
  return jobs;
@@ -1030,6 +1032,22 @@ messaging: [
1030
1032
  ]
1031
1033
  \`\`\`
1032
1034
 
1035
+ #### Proactive scheduled messages
1036
+
1037
+ You can have the agent proactively message Telegram chats on a cron schedule. Add \`channel: telegram\` to any cron job in your \`AGENT.md\` frontmatter:
1038
+
1039
+ \`\`\`yaml
1040
+ cron:
1041
+ daily-checkin:
1042
+ schedule: "0 9 * * *"
1043
+ task: "Check in with the user about their plans for today"
1044
+ channel: telegram
1045
+ \`\`\`
1046
+
1047
+ The system auto-discovers all Telegram chats the bot has interacted with and sends the agent's response to each one. No chat IDs need to be configured -- filtering is handled by \`allowedUserIds\` if set. The agent runs with the full conversation history for each chat, so it has context from prior interactions.
1048
+
1049
+ The bot must have received at least one message from a user before it can send proactive messages to that chat (Telegram API requirement).
1050
+
1033
1051
  ### Email (Resend)
1034
1052
 
1035
1053
  #### 1. Set up Resend
@@ -2771,10 +2789,9 @@ var createMemoryTools = (store, options) => {
2771
2789
  Math.min(5, typeof input.limit === "number" ? input.limit : 3)
2772
2790
  );
2773
2791
  const excludeConversationId = typeof input.excludeConversationId === "string" ? input.excludeConversationId : "";
2774
- const corpus = asRecallCorpus(context.parameters.__conversationRecallCorpus).slice(
2775
- 0,
2776
- maxRecallConversations
2777
- );
2792
+ const rawCorpus = context.parameters.__conversationRecallCorpus;
2793
+ const resolvedCorpus = typeof rawCorpus === "function" ? await rawCorpus() : rawCorpus;
2794
+ const corpus = asRecallCorpus(resolvedCorpus).slice(0, maxRecallConversations);
2778
2795
  const results = corpus.filter(
2779
2796
  (item) => excludeConversationId ? item.conversationId !== excludeConversationId : true
2780
2797
  ).map((item) => ({
@@ -4366,6 +4383,10 @@ cron:
4366
4383
  schedule: "0 9 * * *" # Standard 5-field cron expression
4367
4384
  timezone: "America/New_York" # Optional IANA timezone (default: UTC)
4368
4385
  task: "Generate the daily sales report"
4386
+ telegram-checkin:
4387
+ schedule: "0 18 * * 1-5"
4388
+ channel: telegram # Proactive message to all known Telegram chats
4389
+ task: "Send an end-of-day summary to the user"
4369
4390
  \`\`\`
4370
4391
 
4371
4392
  - Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
@@ -4374,6 +4395,7 @@ cron:
4374
4395
  - Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
4375
4396
  - To carry context across cron runs, enable memory.
4376
4397
  - **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
4398
+ - **Proactive channel messaging**: Adding \`channel: telegram\` (or \`slack\`) makes the cron job send its response directly to all known conversations on that platform, instead of creating a standalone conversation. The agent continues the existing conversation history for context. A chat must have at least one prior user message for auto-discovery to find it.
4377
4399
 
4378
4400
  ## Messaging Integrations (Slack, Telegram, Email)
4379
4401
 
@@ -4599,7 +4621,7 @@ function extractMediaFromToolOutput(output) {
4599
4621
  const strippedOutput = walk(output);
4600
4622
  return { mediaItems, strippedOutput };
4601
4623
  }
4602
- var AgentHarness = class {
4624
+ var AgentHarness = class _AgentHarness {
4603
4625
  workingDir;
4604
4626
  environment;
4605
4627
  modelProvider;
@@ -4611,6 +4633,7 @@ var AgentHarness = class {
4611
4633
  loadedConfig;
4612
4634
  loadedSkills = [];
4613
4635
  skillFingerprint = "";
4636
+ lastSkillRefreshAt = 0;
4614
4637
  activeSkillNames = /* @__PURE__ */ new Set();
4615
4638
  registeredMcpToolNames = /* @__PURE__ */ new Set();
4616
4639
  latitudeTelemetry;
@@ -4899,10 +4922,16 @@ var AgentHarness = class {
4899
4922
  })
4900
4923
  );
4901
4924
  }
4925
+ static SKILL_REFRESH_DEBOUNCE_MS = 3e3;
4902
4926
  async refreshSkillsIfChanged() {
4903
4927
  if (this.environment !== "development") {
4904
4928
  return;
4905
4929
  }
4930
+ const elapsed = Date.now() - this.lastSkillRefreshAt;
4931
+ if (this.lastSkillRefreshAt > 0 && elapsed < _AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
4932
+ return;
4933
+ }
4934
+ this.lastSkillRefreshAt = Date.now();
4906
4935
  try {
4907
4936
  const latestSkills = await loadSkillMetadata(
4908
4937
  this.workingDir,
@@ -5228,6 +5257,7 @@ var AgentHarness = class {
5228
5257
  if (!this.parsedAgent) {
5229
5258
  await this.initialize();
5230
5259
  }
5260
+ const memoryPromise = this.memoryStore ? this.memoryStore.getMainMemory() : void 0;
5231
5261
  await this.refreshSkillsIfChanged();
5232
5262
  this._currentRunConversationId = input.conversationId;
5233
5263
  const ownerParam = input.parameters?.__ownerId;
@@ -5280,7 +5310,7 @@ Each conversation gets its own browser tab sharing a single browser instance. Ca
5280
5310
  const promptWithSkills = this.skillContextWindow ? `${systemPrompt}${developmentContext}
5281
5311
 
5282
5312
  ${this.skillContextWindow}${browserContext}` : `${systemPrompt}${developmentContext}${browserContext}`;
5283
- const mainMemory = this.memoryStore ? await this.memoryStore.getMainMemory() : void 0;
5313
+ const mainMemory = await memoryPromise;
5284
5314
  const boundedMainMemory = mainMemory && mainMemory.content.length > 4e3 ? `${mainMemory.content.slice(0, 4e3)}
5285
5315
  ...[truncated]` : mainMemory?.content;
5286
5316
  const memoryContext = boundedMainMemory && boundedMainMemory.trim().length > 0 ? `
@@ -5375,6 +5405,8 @@ ${boundedMainMemory.trim()}` : "";
5375
5405
  let totalOutputTokens = 0;
5376
5406
  let totalCachedTokens = 0;
5377
5407
  let transientStepRetryCount = 0;
5408
+ let cachedCoreMessages = [];
5409
+ let convertedUpTo = 0;
5378
5410
  for (let step = 1; step <= maxSteps; step += 1) {
5379
5411
  try {
5380
5412
  yield* drainBrowserEvents();
@@ -5651,7 +5683,15 @@ ${textContent}` };
5651
5683
  }
5652
5684
  }
5653
5685
  }
5654
- const coreMessages = (await Promise.all(messages.map(convertMessage))).flat();
5686
+ if (convertedUpTo > messages.length) {
5687
+ cachedCoreMessages = [];
5688
+ convertedUpTo = 0;
5689
+ }
5690
+ const newMessages = messages.slice(convertedUpTo);
5691
+ const newCoreMessages = newMessages.length > 0 ? (await Promise.all(newMessages.map(convertMessage))).flat() : [];
5692
+ cachedCoreMessages = [...cachedCoreMessages, ...newCoreMessages];
5693
+ convertedUpTo = messages.length;
5694
+ const coreMessages = cachedCoreMessages;
5655
5695
  const temperature = agent.frontmatter.model?.temperature ?? 0.2;
5656
5696
  const maxTokens = agent.frontmatter.model?.maxTokens;
5657
5697
  const cachedMessages = addPromptCacheBreakpoints(coreMessages, modelInstance);
@@ -6333,7 +6373,8 @@ var InMemoryConversationStore = class {
6333
6373
  ownerId: c.ownerId,
6334
6374
  parentConversationId: c.parentConversationId,
6335
6375
  messageCount: c.messages.length,
6336
- hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0
6376
+ hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
6377
+ channelMeta: c.channelMeta
6337
6378
  }));
6338
6379
  }
6339
6380
  async get(conversationId) {
@@ -6495,7 +6536,8 @@ var FileConversationStore = class {
6495
6536
  fileName,
6496
6537
  parentConversationId: conversation.parentConversationId,
6497
6538
  messageCount: conversation.messages.length,
6498
- hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0
6539
+ hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
6540
+ channelMeta: conversation.channelMeta
6499
6541
  });
6500
6542
  await this.writeIndex();
6501
6543
  });
@@ -6523,7 +6565,8 @@ var FileConversationStore = class {
6523
6565
  ownerId: c.ownerId,
6524
6566
  parentConversationId: c.parentConversationId,
6525
6567
  messageCount: c.messageCount,
6526
- hasPendingApprovals: c.hasPendingApprovals
6568
+ hasPendingApprovals: c.hasPendingApprovals,
6569
+ channelMeta: c.channelMeta
6527
6570
  }));
6528
6571
  }
6529
6572
  async get(conversationId) {
@@ -6750,12 +6793,12 @@ var KeyValueConversationStoreBase = class {
6750
6793
  return [];
6751
6794
  }
6752
6795
  const ids = await this.getOwnerConversationIds(ownerId);
6796
+ if (ids.length === 0) return [];
6797
+ const convKeys = await Promise.all(ids.map((id) => this.conversationKey(id)));
6798
+ const rawValues = await kv.mget(convKeys);
6753
6799
  const conversations = [];
6754
- for (const id of ids) {
6755
- const raw = await kv.get(await this.conversationKey(id));
6756
- if (!raw) {
6757
- continue;
6758
- }
6800
+ for (const raw of rawValues) {
6801
+ if (!raw) continue;
6759
6802
  try {
6760
6803
  conversations.push(JSON.parse(raw));
6761
6804
  } catch {
@@ -6772,20 +6815,28 @@ var KeyValueConversationStoreBase = class {
6772
6815
  return [];
6773
6816
  }
6774
6817
  const ids = await this.getOwnerConversationIds(ownerId);
6818
+ if (ids.length === 0) return [];
6819
+ const metaKeys = await Promise.all(ids.map((id) => this.conversationMetaKey(id)));
6820
+ const rawValues = await kv.mget(metaKeys);
6775
6821
  const summaries = [];
6776
- for (const id of ids) {
6777
- const meta = await this.getConversationMeta(id);
6778
- if (meta && meta.ownerId === ownerId) {
6779
- summaries.push({
6780
- conversationId: meta.conversationId,
6781
- title: meta.title,
6782
- updatedAt: meta.updatedAt,
6783
- createdAt: meta.createdAt,
6784
- ownerId: meta.ownerId,
6785
- parentConversationId: meta.parentConversationId,
6786
- messageCount: meta.messageCount,
6787
- hasPendingApprovals: meta.hasPendingApprovals
6788
- });
6822
+ for (const raw of rawValues) {
6823
+ if (!raw) continue;
6824
+ try {
6825
+ const meta = JSON.parse(raw);
6826
+ if (meta.ownerId === ownerId) {
6827
+ summaries.push({
6828
+ conversationId: meta.conversationId,
6829
+ title: meta.title,
6830
+ updatedAt: meta.updatedAt,
6831
+ createdAt: meta.createdAt,
6832
+ ownerId: meta.ownerId,
6833
+ parentConversationId: meta.parentConversationId,
6834
+ messageCount: meta.messageCount,
6835
+ hasPendingApprovals: meta.hasPendingApprovals,
6836
+ channelMeta: meta.channelMeta
6837
+ });
6838
+ }
6839
+ } catch {
6789
6840
  }
6790
6841
  }
6791
6842
  return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
@@ -6843,7 +6894,8 @@ var KeyValueConversationStoreBase = class {
6843
6894
  ownerId: nextConversation.ownerId,
6844
6895
  parentConversationId: nextConversation.parentConversationId,
6845
6896
  messageCount: nextConversation.messages.length,
6846
- hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0
6897
+ hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
6898
+ channelMeta: nextConversation.channelMeta
6847
6899
  }),
6848
6900
  this.ttl
6849
6901
  );
@@ -6923,6 +6975,19 @@ var UpstashConversationStore = class extends KeyValueConversationStoreBase {
6923
6975
  const payload = await response.json();
6924
6976
  return payload.result ?? void 0;
6925
6977
  },
6978
+ mget: async (keys) => {
6979
+ if (keys.length === 0) return [];
6980
+ const path = keys.map((k) => encodeURIComponent(k)).join("/");
6981
+ const response = await fetch(`${this.baseUrl}/mget/${path}`, {
6982
+ method: "POST",
6983
+ headers: this.headers()
6984
+ });
6985
+ if (!response.ok) {
6986
+ return keys.map(() => void 0);
6987
+ }
6988
+ const payload = await response.json();
6989
+ return (payload.result ?? []).map((v) => v ?? void 0);
6990
+ },
6926
6991
  set: async (key, value, ttl) => {
6927
6992
  const endpoint = typeof ttl === "number" ? `${this.baseUrl}/setex/${encodeURIComponent(key)}/${Math.max(
6928
6993
  1,
@@ -7015,6 +7080,11 @@ var RedisLikeConversationStore = class extends KeyValueConversationStoreBase {
7015
7080
  const value = await client.get(key);
7016
7081
  return value ?? void 0;
7017
7082
  },
7083
+ mget: async (keys) => {
7084
+ if (keys.length === 0) return [];
7085
+ const values = await client.mGet(keys);
7086
+ return values.map((v) => v ?? void 0);
7087
+ },
7018
7088
  set: async (key, value, ttl) => {
7019
7089
  if (typeof ttl === "number") {
7020
7090
  await client.set(key, value, { EX: Math.max(1, ttl) });
@@ -7149,6 +7219,18 @@ var DynamoDbConversationStore = class extends KeyValueConversationStoreBase {
7149
7219
  })
7150
7220
  );
7151
7221
  },
7222
+ mget: async (keys) => {
7223
+ if (keys.length === 0) return [];
7224
+ return Promise.all(keys.map(async (key) => {
7225
+ const result = await client.send(
7226
+ new client.GetItemCommand({
7227
+ TableName: this.table,
7228
+ Key: { runId: { S: key } }
7229
+ })
7230
+ );
7231
+ return result.Item?.value?.S;
7232
+ }));
7233
+ },
7152
7234
  del: async (key) => {
7153
7235
  await client.send(
7154
7236
  new client.DeleteItemCommand({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/harness",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Agent execution runtime - conversation loop, tool dispatch, streaming",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,6 +27,7 @@ export interface CronJobConfig {
27
27
  schedule: string;
28
28
  task: string;
29
29
  timezone?: string;
30
+ channel?: string;
30
31
  }
31
32
 
32
33
  export interface AgentFrontmatter {
@@ -138,10 +139,16 @@ const parseCronJobs = (
138
139
  validateTimezone(timezone, path);
139
140
  }
140
141
 
142
+ const channel =
143
+ typeof jobValue.channel === "string" && jobValue.channel.trim()
144
+ ? jobValue.channel.trim()
145
+ : undefined;
146
+
141
147
  jobs[jobName] = {
142
148
  schedule: jobValue.schedule.trim(),
143
149
  task: jobValue.task,
144
150
  timezone,
151
+ channel,
145
152
  };
146
153
  }
147
154
  return jobs;
package/src/harness.ts CHANGED
@@ -280,6 +280,10 @@ cron:
280
280
  schedule: "0 9 * * *" # Standard 5-field cron expression
281
281
  timezone: "America/New_York" # Optional IANA timezone (default: UTC)
282
282
  task: "Generate the daily sales report"
283
+ telegram-checkin:
284
+ schedule: "0 18 * * 1-5"
285
+ channel: telegram # Proactive message to all known Telegram chats
286
+ task: "Send an end-of-day summary to the user"
283
287
  \`\`\`
284
288
 
285
289
  - Each cron job triggers an autonomous agent run with the specified task, creating a fresh conversation.
@@ -288,6 +292,7 @@ cron:
288
292
  - Jobs can also be triggered manually: \`GET /api/cron/<jobName>\`.
289
293
  - To carry context across cron runs, enable memory.
290
294
  - **IMPORTANT**: When adding a new cron job, always PRESERVE all existing cron jobs. Never remove or overwrite existing jobs unless the user explicitly asks you to replace or delete them. Read the full current \`cron:\` block before editing, and append the new job alongside the existing ones.
295
+ - **Proactive channel messaging**: Adding \`channel: telegram\` (or \`slack\`) makes the cron job send its response directly to all known conversations on that platform, instead of creating a standalone conversation. The agent continues the existing conversation history for context. A chat must have at least one prior user message for auto-discovery to find it.
291
296
 
292
297
  ## Messaging Integrations (Slack, Telegram, Email)
293
298
 
@@ -545,6 +550,7 @@ export class AgentHarness {
545
550
  private loadedConfig?: PonchoConfig;
546
551
  private loadedSkills: SkillMetadata[] = [];
547
552
  private skillFingerprint = "";
553
+ private lastSkillRefreshAt = 0;
548
554
  private readonly activeSkillNames = new Set<string>();
549
555
  private readonly registeredMcpToolNames = new Set<string>();
550
556
  private latitudeTelemetry?: LatitudeTelemetry;
@@ -879,10 +885,17 @@ export class AgentHarness {
879
885
  );
880
886
  }
881
887
 
888
+ private static readonly SKILL_REFRESH_DEBOUNCE_MS = 3000;
889
+
882
890
  private async refreshSkillsIfChanged(): Promise<void> {
883
891
  if (this.environment !== "development") {
884
892
  return;
885
893
  }
894
+ const elapsed = Date.now() - this.lastSkillRefreshAt;
895
+ if (this.lastSkillRefreshAt > 0 && elapsed < AgentHarness.SKILL_REFRESH_DEBOUNCE_MS) {
896
+ return;
897
+ }
898
+ this.lastSkillRefreshAt = Date.now();
886
899
  try {
887
900
  const latestSkills = await loadSkillMetadata(
888
901
  this.workingDir,
@@ -1269,6 +1282,10 @@ export class AgentHarness {
1269
1282
  if (!this.parsedAgent) {
1270
1283
  await this.initialize();
1271
1284
  }
1285
+ // Start memory fetch early so it overlaps with skill refresh I/O
1286
+ const memoryPromise = this.memoryStore
1287
+ ? this.memoryStore.getMainMemory()
1288
+ : undefined;
1272
1289
  await this.refreshSkillsIfChanged();
1273
1290
 
1274
1291
  // Track which conversation/owner this run belongs to so browser & subagent tools resolve correctly
@@ -1328,9 +1345,7 @@ Each conversation gets its own browser tab sharing a single browser instance. Ca
1328
1345
  const promptWithSkills = this.skillContextWindow
1329
1346
  ? `${systemPrompt}${developmentContext}\n\n${this.skillContextWindow}${browserContext}`
1330
1347
  : `${systemPrompt}${developmentContext}${browserContext}`;
1331
- const mainMemory = this.memoryStore
1332
- ? await this.memoryStore.getMainMemory()
1333
- : undefined;
1348
+ const mainMemory = await memoryPromise;
1334
1349
  const boundedMainMemory =
1335
1350
  mainMemory && mainMemory.content.length > 4000
1336
1351
  ? `${mainMemory.content.slice(0, 4000)}\n...[truncated]`
@@ -1445,6 +1460,8 @@ ${boundedMainMemory.trim()}`
1445
1460
  let totalOutputTokens = 0;
1446
1461
  let totalCachedTokens = 0;
1447
1462
  let transientStepRetryCount = 0;
1463
+ let cachedCoreMessages: ModelMessage[] = [];
1464
+ let convertedUpTo = 0;
1448
1465
 
1449
1466
  for (let step = 1; step <= maxSteps; step += 1) {
1450
1467
  try {
@@ -1784,9 +1801,19 @@ ${boundedMainMemory.trim()}`
1784
1801
  }
1785
1802
  }
1786
1803
 
1787
- const coreMessages: ModelMessage[] = (
1788
- await Promise.all(messages.map(convertMessage))
1789
- ).flat();
1804
+ // Only convert messages added since the last step
1805
+ if (convertedUpTo > messages.length) {
1806
+ // Compaction replaced the array — invalidate cache
1807
+ cachedCoreMessages = [];
1808
+ convertedUpTo = 0;
1809
+ }
1810
+ const newMessages = messages.slice(convertedUpTo);
1811
+ const newCoreMessages: ModelMessage[] = newMessages.length > 0
1812
+ ? (await Promise.all(newMessages.map(convertMessage))).flat()
1813
+ : [];
1814
+ cachedCoreMessages = [...cachedCoreMessages, ...newCoreMessages];
1815
+ convertedUpTo = messages.length;
1816
+ const coreMessages = cachedCoreMessages;
1790
1817
 
1791
1818
  const temperature = agent.frontmatter.model?.temperature ?? 0.2;
1792
1819
  const maxTokens = agent.frontmatter.model?.maxTokens;
@@ -1794,6 +1821,7 @@ ${boundedMainMemory.trim()}`
1794
1821
 
1795
1822
  const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
1796
1823
 
1824
+
1797
1825
  const result = await streamText({
1798
1826
  model: modelInstance,
1799
1827
  system: integrityPrompt,
package/src/memory.ts CHANGED
@@ -658,10 +658,10 @@ export const createMemoryTools = (
658
658
  typeof input.excludeConversationId === "string"
659
659
  ? input.excludeConversationId
660
660
  : "";
661
- const corpus = asRecallCorpus(context.parameters.__conversationRecallCorpus).slice(
662
- 0,
663
- maxRecallConversations,
664
- );
661
+ const rawCorpus = context.parameters.__conversationRecallCorpus;
662
+ const resolvedCorpus =
663
+ typeof rawCorpus === "function" ? await (rawCorpus as () => Promise<unknown>)() : rawCorpus;
664
+ const corpus = asRecallCorpus(resolvedCorpus).slice(0, maxRecallConversations);
665
665
  const results = corpus
666
666
  .filter((item) =>
667
667
  excludeConversationId ? item.conversationId !== excludeConversationId : true,
package/src/state.ts CHANGED
@@ -50,6 +50,11 @@ export interface Conversation {
50
50
  result?: import("@poncho-ai/sdk").RunResult;
51
51
  error?: import("@poncho-ai/sdk").AgentFailure;
52
52
  };
53
+ channelMeta?: {
54
+ platform: string;
55
+ channelId: string;
56
+ platformThreadId: string;
57
+ };
53
58
  createdAt: number;
54
59
  updatedAt: number;
55
60
  }
@@ -247,6 +252,7 @@ export class InMemoryConversationStore implements ConversationStore {
247
252
  parentConversationId: c.parentConversationId,
248
253
  messageCount: c.messages.length,
249
254
  hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
255
+ channelMeta: c.channelMeta,
250
256
  }));
251
257
  }
252
258
 
@@ -305,6 +311,11 @@ export type ConversationSummary = {
305
311
  parentConversationId?: string;
306
312
  messageCount?: number;
307
313
  hasPendingApprovals?: boolean;
314
+ channelMeta?: {
315
+ platform: string;
316
+ channelId: string;
317
+ platformThreadId: string;
318
+ };
308
319
  };
309
320
 
310
321
  type ConversationStoreFile = {
@@ -319,6 +330,11 @@ type ConversationStoreFile = {
319
330
  parentConversationId?: string;
320
331
  messageCount?: number;
321
332
  hasPendingApprovals?: boolean;
333
+ channelMeta?: {
334
+ platform: string;
335
+ channelId: string;
336
+ platformThreadId: string;
337
+ };
322
338
  }>;
323
339
  };
324
340
 
@@ -451,6 +467,7 @@ class FileConversationStore implements ConversationStore {
451
467
  parentConversationId: conversation.parentConversationId,
452
468
  messageCount: conversation.messages.length,
453
469
  hasPendingApprovals: Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0,
470
+ channelMeta: conversation.channelMeta,
454
471
  });
455
472
  await this.writeIndex();
456
473
  });
@@ -486,6 +503,7 @@ class FileConversationStore implements ConversationStore {
486
503
  parentConversationId: c.parentConversationId,
487
504
  messageCount: c.messageCount,
488
505
  hasPendingApprovals: c.hasPendingApprovals,
506
+ channelMeta: c.channelMeta,
489
507
  }));
490
508
  }
491
509
 
@@ -646,6 +664,7 @@ class FileStateStore implements StateStore {
646
664
 
647
665
  interface RawKeyValueClient {
648
666
  get(key: string): Promise<string | undefined>;
667
+ mget(keys: string[]): Promise<(string | undefined)[]>;
649
668
  set(key: string, value: string, ttl?: number): Promise<void>;
650
669
  del(key: string): Promise<void>;
651
670
  }
@@ -659,6 +678,11 @@ type ConversationMeta = {
659
678
  parentConversationId?: string;
660
679
  messageCount?: number;
661
680
  hasPendingApprovals?: boolean;
681
+ channelMeta?: {
682
+ platform: string;
683
+ channelId: string;
684
+ platformThreadId: string;
685
+ };
662
686
  };
663
687
 
664
688
  abstract class KeyValueConversationStoreBase implements ConversationStore {
@@ -760,21 +784,18 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
760
784
  return await this.memoryFallback.list(ownerId);
761
785
  }
762
786
  if (!ownerId) {
763
- // KV stores index per-owner; cross-owner listing not supported
764
787
  return [];
765
788
  }
766
789
  const ids = await this.getOwnerConversationIds(ownerId);
790
+ if (ids.length === 0) return [];
791
+ const convKeys = await Promise.all(ids.map((id) => this.conversationKey(id)));
792
+ const rawValues = await kv.mget(convKeys);
767
793
  const conversations: Conversation[] = [];
768
- for (const id of ids) {
769
- const raw = await kv.get(await this.conversationKey(id));
770
- if (!raw) {
771
- continue;
772
- }
794
+ for (const raw of rawValues) {
795
+ if (!raw) continue;
773
796
  try {
774
797
  conversations.push(JSON.parse(raw) as Conversation);
775
- } catch {
776
- // Skip invalid records.
777
- }
798
+ } catch { /* skip invalid records */ }
778
799
  }
779
800
  return conversations.sort((a, b) => b.updatedAt - a.updatedAt);
780
801
  }
@@ -788,21 +809,28 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
788
809
  return [];
789
810
  }
790
811
  const ids = await this.getOwnerConversationIds(ownerId);
812
+ if (ids.length === 0) return [];
813
+ const metaKeys = await Promise.all(ids.map((id) => this.conversationMetaKey(id)));
814
+ const rawValues = await kv.mget(metaKeys);
791
815
  const summaries: ConversationSummary[] = [];
792
- for (const id of ids) {
793
- const meta = await this.getConversationMeta(id);
794
- if (meta && meta.ownerId === ownerId) {
795
- summaries.push({
796
- conversationId: meta.conversationId,
797
- title: meta.title,
798
- updatedAt: meta.updatedAt,
799
- createdAt: meta.createdAt,
800
- ownerId: meta.ownerId,
801
- parentConversationId: meta.parentConversationId,
802
- messageCount: meta.messageCount,
803
- hasPendingApprovals: meta.hasPendingApprovals,
804
- });
805
- }
816
+ for (const raw of rawValues) {
817
+ if (!raw) continue;
818
+ try {
819
+ const meta = JSON.parse(raw) as ConversationMeta;
820
+ if (meta.ownerId === ownerId) {
821
+ summaries.push({
822
+ conversationId: meta.conversationId,
823
+ title: meta.title,
824
+ updatedAt: meta.updatedAt,
825
+ createdAt: meta.createdAt,
826
+ ownerId: meta.ownerId,
827
+ parentConversationId: meta.parentConversationId,
828
+ messageCount: meta.messageCount,
829
+ hasPendingApprovals: meta.hasPendingApprovals,
830
+ channelMeta: meta.channelMeta,
831
+ });
832
+ }
833
+ } catch { /* skip invalid records */ }
806
834
  }
807
835
  return summaries.sort((a, b) => b.updatedAt - a.updatedAt);
808
836
  }
@@ -863,6 +891,7 @@ abstract class KeyValueConversationStoreBase implements ConversationStore {
863
891
  parentConversationId: nextConversation.parentConversationId,
864
892
  messageCount: nextConversation.messages.length,
865
893
  hasPendingApprovals: Array.isArray(nextConversation.pendingApprovals) && nextConversation.pendingApprovals.length > 0,
894
+ channelMeta: nextConversation.channelMeta,
866
895
  } satisfies ConversationMeta),
867
896
  this.ttl,
868
897
  );
@@ -948,6 +977,19 @@ class UpstashConversationStore extends KeyValueConversationStoreBase {
948
977
  const payload = (await response.json()) as { result?: string | null };
949
978
  return payload.result ?? undefined;
950
979
  },
980
+ mget: async (keys: string[]) => {
981
+ if (keys.length === 0) return [];
982
+ const path = keys.map((k) => encodeURIComponent(k)).join("/");
983
+ const response = await fetch(`${this.baseUrl}/mget/${path}`, {
984
+ method: "POST",
985
+ headers: this.headers(),
986
+ });
987
+ if (!response.ok) {
988
+ return keys.map(() => undefined);
989
+ }
990
+ const payload = (await response.json()) as { result?: (string | null)[] };
991
+ return (payload.result ?? []).map((v) => v ?? undefined);
992
+ },
951
993
  set: async (key: string, value: string, ttl?: number) => {
952
994
  const endpoint =
953
995
  typeof ttl === "number"
@@ -1042,6 +1084,7 @@ class RedisLikeConversationStore extends KeyValueConversationStoreBase {
1042
1084
  private readonly clientPromise: Promise<
1043
1085
  | {
1044
1086
  get: (key: string) => Promise<string | null>;
1087
+ mGet: (keys: readonly string[]) => Promise<(string | null)[]>;
1045
1088
  set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
1046
1089
  del: (key: string) => Promise<unknown>;
1047
1090
  }
@@ -1056,6 +1099,7 @@ class RedisLikeConversationStore extends KeyValueConversationStoreBase {
1056
1099
  createClient: (options: { url: string }) => {
1057
1100
  connect: () => Promise<unknown>;
1058
1101
  get: (key: string) => Promise<string | null>;
1102
+ mGet: (keys: readonly string[]) => Promise<(string | null)[]>;
1059
1103
  set: (key: string, value: string, options?: { EX?: number }) => Promise<unknown>;
1060
1104
  del: (key: string) => Promise<unknown>;
1061
1105
  };
@@ -1079,6 +1123,11 @@ class RedisLikeConversationStore extends KeyValueConversationStoreBase {
1079
1123
  const value = await client.get(key);
1080
1124
  return value ?? undefined;
1081
1125
  },
1126
+ mget: async (keys: string[]) => {
1127
+ if (keys.length === 0) return [];
1128
+ const values = await client.mGet(keys);
1129
+ return values.map((v: string | null) => v ?? undefined);
1130
+ },
1082
1131
  set: async (key: string, value: string, ttl?: number) => {
1083
1132
  if (typeof ttl === "number") {
1084
1133
  await client.set(key, value, { EX: Math.max(1, ttl) });
@@ -1261,6 +1310,18 @@ class DynamoDbConversationStore extends KeyValueConversationStoreBase {
1261
1310
  }),
1262
1311
  );
1263
1312
  },
1313
+ mget: async (keys: string[]) => {
1314
+ if (keys.length === 0) return [];
1315
+ return Promise.all(keys.map(async (key) => {
1316
+ const result = (await client.send(
1317
+ new client.GetItemCommand({
1318
+ TableName: this.table,
1319
+ Key: { runId: { S: key } },
1320
+ }),
1321
+ )) as { Item?: { value?: { S?: string } } };
1322
+ return result.Item?.value?.S;
1323
+ }));
1324
+ },
1264
1325
  del: async (key: string) => {
1265
1326
  await client.send(
1266
1327
  new client.DeleteItemCommand({