@poncho-ai/harness 0.37.1 → 0.38.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.
package/dist/index.js CHANGED
@@ -2663,16 +2663,19 @@ var InMemoryEngine = class {
2663
2663
  get: async (conversationId) => {
2664
2664
  return this.convs.get(conversationId);
2665
2665
  },
2666
- create: async (ownerId, title, tenantId) => {
2666
+ create: async (ownerId, title, tenantId, init) => {
2667
2667
  const now2 = Date.now();
2668
2668
  const conv = {
2669
2669
  conversationId: randomUUID3(),
2670
2670
  title: normalizeTitle(title),
2671
- messages: [],
2671
+ messages: init?.messages ?? [],
2672
2672
  ownerId: ownerId ?? DEFAULT_OWNER,
2673
2673
  tenantId: tenantId === void 0 ? null : tenantId,
2674
2674
  createdAt: now2,
2675
- updatedAt: now2
2675
+ updatedAt: now2,
2676
+ ...init?.parentConversationId !== void 0 ? { parentConversationId: init.parentConversationId } : {},
2677
+ ...init?.subagentMeta !== void 0 ? { subagentMeta: init.subagentMeta } : {},
2678
+ ...init?.channelMeta !== void 0 ? { channelMeta: init.channelMeta } : {}
2676
2679
  };
2677
2680
  this.convs.set(conv.conversationId, conv);
2678
2681
  return conv;
@@ -2776,11 +2779,21 @@ var InMemoryEngine = class {
2776
2779
  createdAt: Date.now(),
2777
2780
  conversationId: input.conversationId,
2778
2781
  ownerId: input.ownerId,
2779
- tenantId: input.tenantId
2782
+ tenantId: input.tenantId,
2783
+ recurrence: input.recurrence ?? null,
2784
+ occurrenceCount: 0
2780
2785
  };
2781
2786
  this.reminderData.set(r.id, r);
2782
2787
  return r;
2783
2788
  },
2789
+ update: async (id, fields) => {
2790
+ const r = this.reminderData.get(id);
2791
+ if (!r) throw new Error(`Reminder ${id} not found`);
2792
+ if (fields.scheduledAt !== void 0) r.scheduledAt = fields.scheduledAt;
2793
+ if (fields.occurrenceCount !== void 0) r.occurrenceCount = fields.occurrenceCount;
2794
+ if (fields.status !== void 0) r.status = fields.status;
2795
+ return r;
2796
+ },
2784
2797
  cancel: async (id) => {
2785
2798
  const r = this.reminderData.get(id);
2786
2799
  if (!r) throw new Error(`Reminder ${id} not found`);
@@ -3207,6 +3220,17 @@ var migrations = [
3207
3220
  ]
3208
3221
  ];
3209
3222
  }
3223
+ },
3224
+ {
3225
+ version: 5,
3226
+ name: "add_reminder_recurrence",
3227
+ up: (d) => {
3228
+ const jsonType = d === "sqlite" ? "TEXT" : "JSONB";
3229
+ return [
3230
+ `ALTER TABLE reminders ADD COLUMN recurrence ${jsonType}`,
3231
+ `ALTER TABLE reminders ADD COLUMN occurrence_count INTEGER NOT NULL DEFAULT 0`
3232
+ ];
3233
+ }
3210
3234
  }
3211
3235
  ];
3212
3236
 
@@ -3338,19 +3362,23 @@ var SqlStorageEngine = class {
3338
3362
  }
3339
3363
  return conv;
3340
3364
  },
3341
- create: async (ownerId, title, tenantId) => {
3365
+ create: async (ownerId, title, tenantId, init) => {
3342
3366
  const id = randomUUID4();
3343
3367
  const now2 = Date.now();
3344
3368
  const conv = {
3345
3369
  conversationId: id,
3346
3370
  title: normalizeTitle2(title),
3347
- messages: [],
3371
+ messages: init?.messages ?? [],
3348
3372
  ownerId: ownerId ?? DEFAULT_OWNER2,
3349
3373
  tenantId: tenantId === void 0 ? null : tenantId,
3350
3374
  createdAt: now2,
3351
- updatedAt: now2
3375
+ updatedAt: now2,
3376
+ ...init?.parentConversationId !== void 0 ? { parentConversationId: init.parentConversationId } : {},
3377
+ ...init?.subagentMeta !== void 0 ? { subagentMeta: init.subagentMeta } : {},
3378
+ ...init?.channelMeta !== void 0 ? { channelMeta: init.channelMeta } : {}
3352
3379
  };
3353
3380
  const data = JSON.stringify(conv);
3381
+ const channelMetaJson = conv.channelMeta ? JSON.stringify(conv.channelMeta) : null;
3354
3382
  await this.executor.run(
3355
3383
  rewrite(
3356
3384
  `INSERT INTO conversations (id, agent_id, tenant_id, owner_id, title, data, message_count, created_at, updated_at,
@@ -3365,12 +3393,12 @@ var SqlStorageEngine = class {
3365
3393
  conv.ownerId,
3366
3394
  conv.title,
3367
3395
  data,
3368
- 0,
3396
+ conv.messages.length,
3369
3397
  new Date(now2).toISOString(),
3370
3398
  new Date(now2).toISOString(),
3371
- null,
3399
+ conv.parentConversationId ?? null,
3372
3400
  0,
3373
- null
3401
+ channelMetaJson
3374
3402
  ]
3375
3403
  );
3376
3404
  return conv;
@@ -3567,10 +3595,11 @@ var SqlStorageEngine = class {
3567
3595
  const id = randomUUID4();
3568
3596
  const now2 = Date.now();
3569
3597
  const tid = normalizeTenant2(input.tenantId);
3598
+ const recurrenceJson = input.recurrence ? JSON.stringify(input.recurrence) : null;
3570
3599
  await this.executor.run(
3571
3600
  rewrite(
3572
- `INSERT INTO reminders (id, agent_id, tenant_id, owner_id, conversation_id, task, status, scheduled_at, timezone, created_at)
3573
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
3601
+ `INSERT INTO reminders (id, agent_id, tenant_id, owner_id, conversation_id, task, status, scheduled_at, timezone, created_at, recurrence, occurrence_count)
3602
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
3574
3603
  this.dialect
3575
3604
  ),
3576
3605
  [
@@ -3583,7 +3612,9 @@ var SqlStorageEngine = class {
3583
3612
  "pending",
3584
3613
  input.scheduledAt,
3585
3614
  input.timezone ?? null,
3586
- new Date(now2).toISOString()
3615
+ new Date(now2).toISOString(),
3616
+ recurrenceJson,
3617
+ 0
3587
3618
  ]
3588
3619
  );
3589
3620
  return {
@@ -3595,9 +3626,52 @@ var SqlStorageEngine = class {
3595
3626
  createdAt: now2,
3596
3627
  conversationId: input.conversationId,
3597
3628
  ownerId: input.ownerId,
3598
- tenantId: input.tenantId
3629
+ tenantId: input.tenantId,
3630
+ recurrence: input.recurrence ?? null,
3631
+ occurrenceCount: 0
3599
3632
  };
3600
3633
  },
3634
+ update: async (id, fields) => {
3635
+ const setClauses = [];
3636
+ const params = [];
3637
+ let idx = 1;
3638
+ if (fields.scheduledAt !== void 0) {
3639
+ setClauses.push(`scheduled_at = $${idx++}`);
3640
+ params.push(fields.scheduledAt);
3641
+ }
3642
+ if (fields.occurrenceCount !== void 0) {
3643
+ setClauses.push(`occurrence_count = $${idx++}`);
3644
+ params.push(fields.occurrenceCount);
3645
+ }
3646
+ if (fields.status !== void 0) {
3647
+ setClauses.push(`status = $${idx++}`);
3648
+ params.push(fields.status);
3649
+ }
3650
+ if (setClauses.length === 0) {
3651
+ const row2 = await this.executor.get(
3652
+ rewrite("SELECT * FROM reminders WHERE id = $1 AND agent_id = $2", this.dialect),
3653
+ [id, this.agentId]
3654
+ );
3655
+ if (!row2) throw new Error(`Reminder ${id} not found`);
3656
+ return this.rowToReminder(row2);
3657
+ }
3658
+ params.push(id, this.agentId);
3659
+ const idIdx = idx++;
3660
+ const agentIdx = idx++;
3661
+ await this.executor.run(
3662
+ rewrite(
3663
+ `UPDATE reminders SET ${setClauses.join(", ")} WHERE id = $${idIdx} AND agent_id = $${agentIdx}`,
3664
+ this.dialect
3665
+ ),
3666
+ params
3667
+ );
3668
+ const row = await this.executor.get(
3669
+ rewrite("SELECT * FROM reminders WHERE id = $1 AND agent_id = $2", this.dialect),
3670
+ [id, this.agentId]
3671
+ );
3672
+ if (!row) throw new Error(`Reminder ${id} not found`);
3673
+ return this.rowToReminder(row);
3674
+ },
3601
3675
  cancel: async (id) => {
3602
3676
  await this.executor.run(
3603
3677
  rewrite(
@@ -3904,6 +3978,14 @@ var SqlStorageEngine = class {
3904
3978
  }
3905
3979
  rowToReminder(row) {
3906
3980
  const tid = row.tenant_id;
3981
+ let recurrence = null;
3982
+ if (row.recurrence) {
3983
+ try {
3984
+ recurrence = typeof row.recurrence === "string" ? JSON.parse(row.recurrence) : row.recurrence;
3985
+ } catch {
3986
+ recurrence = null;
3987
+ }
3988
+ }
3907
3989
  return {
3908
3990
  id: row.id,
3909
3991
  task: row.task,
@@ -3913,7 +3995,9 @@ var SqlStorageEngine = class {
3913
3995
  createdAt: new Date(row.created_at).getTime(),
3914
3996
  conversationId: row.conversation_id,
3915
3997
  ownerId: row.owner_id ?? void 0,
3916
- tenantId: tid === DEFAULT_TENANT2 ? null : tid
3998
+ tenantId: tid === DEFAULT_TENANT2 ? null : tid,
3999
+ recurrence,
4000
+ occurrenceCount: row.occurrence_count ?? 0
3917
4001
  };
3918
4002
  }
3919
4003
  toUint8Array(value) {
@@ -4170,7 +4254,7 @@ function createConversationStoreFromEngine(engine) {
4170
4254
  }),
4171
4255
  listSummaries: (ownerId, tenantId) => engine.conversations.list(ownerId, tenantId),
4172
4256
  get: (conversationId) => engine.conversations.get(conversationId),
4173
- create: (ownerId, title, tenantId) => engine.conversations.create(ownerId, title, tenantId),
4257
+ create: (ownerId, title, tenantId, init) => engine.conversations.create(ownerId, title, tenantId, init),
4174
4258
  update: (conversation) => engine.conversations.update(conversation),
4175
4259
  rename: (conversationId, title) => engine.conversations.rename(conversationId, title),
4176
4260
  delete: (conversationId) => engine.conversations.delete(conversationId),
@@ -4194,6 +4278,7 @@ function createReminderStoreFromEngine(engine) {
4194
4278
  return {
4195
4279
  list: () => engine.reminders.list(),
4196
4280
  create: (input) => engine.reminders.create(input),
4281
+ update: (id, fields) => engine.reminders.update(id, fields),
4197
4282
  cancel: (id) => engine.reminders.cancel(id),
4198
4283
  delete: (id) => engine.reminders.delete(id)
4199
4284
  };
@@ -4591,13 +4676,23 @@ var BashEnvironmentManager = class {
4591
4676
  this.bashOptions = toBashOptions(bashConfig, network);
4592
4677
  }
4593
4678
  environments = /* @__PURE__ */ new Map();
4679
+ filesystems = /* @__PURE__ */ new Map();
4594
4680
  workingDir;
4595
4681
  bashOptions;
4682
+ /** Return the combined IFileSystem (VFS + optional /project mount) for a tenant. */
4683
+ getFs(tenantId) {
4684
+ let fs = this.filesystems.get(tenantId);
4685
+ if (!fs) {
4686
+ const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
4687
+ fs = createBashFs(adapter, this.workingDir);
4688
+ this.filesystems.set(tenantId, fs);
4689
+ }
4690
+ return fs;
4691
+ }
4596
4692
  getOrCreate(tenantId) {
4597
4693
  let bash = this.environments.get(tenantId);
4598
4694
  if (!bash) {
4599
- const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
4600
- const fs = createBashFs(adapter, this.workingDir);
4695
+ const fs = this.getFs(tenantId);
4601
4696
  bash = new Bash({
4602
4697
  fs,
4603
4698
  cwd: "/",
@@ -4621,9 +4716,11 @@ var BashEnvironmentManager = class {
4621
4716
  }
4622
4717
  destroy(tenantId) {
4623
4718
  this.environments.delete(tenantId);
4719
+ this.filesystems.delete(tenantId);
4624
4720
  }
4625
4721
  destroyAll() {
4626
4722
  this.environments.clear();
4723
+ this.filesystems.clear();
4627
4724
  }
4628
4725
  };
4629
4726
 
@@ -4701,7 +4798,7 @@ var mimeFromPath = (path) => {
4701
4798
  return MIME_MAP2[path.slice(dot).toLowerCase()];
4702
4799
  };
4703
4800
  var isTextMime = (mime) => mime.startsWith("text/") || mime === "application/json" || mime === "application/xml" || mime === "application/sql" || mime === "application/javascript" || mime === "application/x-sh";
4704
- var createReadFileTool = (engine) => defineTool3({
4801
+ var createReadFileTool = (getFs) => defineTool3({
4705
4802
  name: "read_file",
4706
4803
  description: "Read a file from the virtual filesystem. Returns text content for text-based files, or sends images and PDFs directly to the model for visual analysis.",
4707
4804
  inputSchema: {
@@ -4721,23 +4818,24 @@ var createReadFileTool = (engine) => defineTool3({
4721
4818
  throw new Error("path is required");
4722
4819
  }
4723
4820
  const tenantId = context.tenantId ?? "__default__";
4724
- const stat3 = await engine.vfs.stat(tenantId, filePath);
4725
- if (!stat3) {
4821
+ const fs = getFs(tenantId);
4822
+ if (!await fs.exists(filePath)) {
4726
4823
  throw new Error(`File not found: ${filePath}`);
4727
4824
  }
4728
- if (stat3.type === "directory") {
4825
+ const stat3 = await fs.stat(filePath);
4826
+ if (stat3.isDirectory) {
4729
4827
  throw new Error(`${filePath} is a directory, not a file`);
4730
4828
  }
4731
- const mediaType = stat3.mimeType ?? mimeFromPath(filePath) ?? "application/octet-stream";
4829
+ const mediaType = mimeFromPath(filePath) ?? "application/octet-stream";
4732
4830
  const filename = filePath.split("/").pop() ?? filePath;
4733
4831
  if (isTextMime(mediaType)) {
4734
- const buf = await engine.vfs.readFile(tenantId, filePath);
4735
- const text = Buffer.from(buf).toString("utf8");
4832
+ const text = await fs.readFile(filePath);
4736
4833
  return { filename, mediaType, content: text };
4737
4834
  }
4835
+ const buf = await fs.readFileBuffer(filePath);
4738
4836
  return {
4739
4837
  type: "file",
4740
- data: `${VFS_SCHEME}${filePath}`,
4838
+ data: Buffer.from(buf).toString("base64"),
4741
4839
  mediaType,
4742
4840
  filename
4743
4841
  };
@@ -4746,7 +4844,7 @@ var createReadFileTool = (engine) => defineTool3({
4746
4844
 
4747
4845
  // src/vfs/edit-file-tool.ts
4748
4846
  import { defineTool as defineTool4 } from "@poncho-ai/sdk";
4749
- var createEditFileTool = (engine) => defineTool4({
4847
+ var createEditFileTool = (getFs) => defineTool4({
4750
4848
  name: "edit_file",
4751
4849
  description: "Edit a file by replacing an exact string match with new content. The old_str must match exactly one location in the file. Use an empty new_str to delete matched content. Use read_file first to see current content before editing.",
4752
4850
  inputSchema: {
@@ -4775,11 +4873,11 @@ var createEditFileTool = (engine) => defineTool4({
4775
4873
  if (!filePath) throw new Error("path is required");
4776
4874
  if (!oldStr) throw new Error("old_str must not be empty");
4777
4875
  const tenantId = context.tenantId ?? "__default__";
4778
- const stat3 = await engine.vfs.stat(tenantId, filePath);
4779
- if (!stat3) throw new Error(`File not found: ${filePath}`);
4780
- if (stat3.type === "directory") throw new Error(`${filePath} is a directory`);
4781
- const buf = await engine.vfs.readFile(tenantId, filePath);
4782
- const content = Buffer.from(buf).toString("utf8");
4876
+ const fs = getFs(tenantId);
4877
+ if (!await fs.exists(filePath)) throw new Error(`File not found: ${filePath}`);
4878
+ const stat3 = await fs.stat(filePath);
4879
+ if (stat3.isDirectory) throw new Error(`${filePath} is a directory`);
4880
+ const content = await fs.readFile(filePath);
4783
4881
  const first = content.indexOf(oldStr);
4784
4882
  if (first === -1) {
4785
4883
  throw new Error(
@@ -4793,14 +4891,14 @@ var createEditFileTool = (engine) => defineTool4({
4793
4891
  );
4794
4892
  }
4795
4893
  const updated = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
4796
- await engine.vfs.writeFile(tenantId, filePath, new TextEncoder().encode(updated));
4894
+ await fs.writeFile(filePath, updated);
4797
4895
  return { ok: true, path: filePath };
4798
4896
  }
4799
4897
  });
4800
4898
 
4801
4899
  // src/vfs/write-file-tool.ts
4802
4900
  import { defineTool as defineTool5 } from "@poncho-ai/sdk";
4803
- var createWriteFileTool = (engine) => defineTool5({
4901
+ var createWriteFileTool = (getFs) => defineTool5({
4804
4902
  name: "write_file",
4805
4903
  description: "Create a new file or overwrite an existing file in the virtual filesystem. Parent directories are created automatically. Prefer edit_file for targeted changes to existing files.",
4806
4904
  inputSchema: {
@@ -4823,11 +4921,12 @@ var createWriteFileTool = (engine) => defineTool5({
4823
4921
  const content = typeof input.content === "string" ? input.content : "";
4824
4922
  if (!filePath) throw new Error("path is required");
4825
4923
  const tenantId = context.tenantId ?? "__default__";
4924
+ const fs = getFs(tenantId);
4826
4925
  const dir = filePath.slice(0, filePath.lastIndexOf("/"));
4827
4926
  if (dir) {
4828
- await engine.vfs.mkdir(tenantId, dir, true);
4927
+ await fs.mkdir(dir, { recursive: true });
4829
4928
  }
4830
- await engine.vfs.writeFile(tenantId, filePath, new TextEncoder().encode(content));
4929
+ await fs.writeFile(filePath, content);
4831
4930
  return { ok: true, path: filePath };
4832
4931
  }
4833
4932
  });
@@ -5370,10 +5469,11 @@ async function resolveEnv(secretsStore, tenantId, envName) {
5370
5469
  // src/reminder-tools.ts
5371
5470
  import { defineTool as defineTool8 } from "@poncho-ai/sdk";
5372
5471
  var VALID_STATUSES2 = ["pending", "cancelled"];
5472
+ var VALID_RECURRENCE_TYPES = ["daily", "weekly", "monthly", "cron"];
5373
5473
  var createReminderTools = (store) => [
5374
5474
  defineTool8({
5375
5475
  name: "set_reminder",
5376
- description: "Set a one-time reminder that will fire at the specified date and time. Use this when the user asks to be reminded about something. The datetime must be an ISO 8601 string in the future. When the reminder fires, the task message will be delivered to the user.",
5476
+ description: "Set a reminder that will fire at the specified date and time. Use this when the user asks to be reminded about something. The datetime must be an ISO 8601 string in the future. When the reminder fires, the task message will be delivered to the user. Supports optional recurrence for recurring reminders (daily, weekly, monthly, or cron).",
5377
5477
  inputSchema: {
5378
5478
  type: "object",
5379
5479
  properties: {
@@ -5383,11 +5483,45 @@ var createReminderTools = (store) => [
5383
5483
  },
5384
5484
  datetime: {
5385
5485
  type: "string",
5386
- description: "ISO 8601 datetime for when the reminder should fire (e.g. '2026-03-23T09:00:00Z')"
5486
+ description: "ISO 8601 datetime for when the reminder should first fire (e.g. '2026-03-23T09:00:00Z')"
5387
5487
  },
5388
5488
  timezone: {
5389
5489
  type: "string",
5390
5490
  description: "IANA timezone for interpreting the datetime if it lacks an offset (e.g. 'America/New_York'). Defaults to UTC."
5491
+ },
5492
+ recurrence: {
5493
+ type: "object",
5494
+ description: "Optional. Set this to make the reminder repeat. Omit for a one-time reminder.",
5495
+ properties: {
5496
+ type: {
5497
+ type: "string",
5498
+ enum: VALID_RECURRENCE_TYPES,
5499
+ description: "How often to repeat: 'daily', 'weekly', 'monthly', or 'cron'."
5500
+ },
5501
+ interval: {
5502
+ type: "number",
5503
+ description: "Repeat every N units (e.g. 2 = every 2 days/weeks/months). Defaults to 1."
5504
+ },
5505
+ daysOfWeek: {
5506
+ type: "array",
5507
+ items: { type: "number" },
5508
+ description: "For weekly: which days to fire (0=Sunday, 1=Monday, ..., 6=Saturday)."
5509
+ },
5510
+ expression: {
5511
+ type: "string",
5512
+ description: "For type 'cron': a 5-field cron expression (e.g. '0 9 * * 1-5' for weekdays at 9am)."
5513
+ },
5514
+ endsAt: {
5515
+ type: "string",
5516
+ description: "ISO 8601 datetime after which the recurrence should stop."
5517
+ },
5518
+ maxOccurrences: {
5519
+ type: "number",
5520
+ description: "Maximum number of times the reminder should fire before stopping."
5521
+ }
5522
+ },
5523
+ required: ["type"],
5524
+ additionalProperties: false
5391
5525
  }
5392
5526
  },
5393
5527
  required: ["task", "datetime"],
@@ -5434,13 +5568,56 @@ var createReminderTools = (store) => [
5434
5568
  if (scheduledAt <= Date.now()) {
5435
5569
  throw new Error("Reminder datetime must be in the future");
5436
5570
  }
5571
+ let recurrence = null;
5572
+ if (input.recurrence && typeof input.recurrence === "object") {
5573
+ const rec = input.recurrence;
5574
+ const recType = rec.type;
5575
+ if (!VALID_RECURRENCE_TYPES.includes(recType)) {
5576
+ throw new Error(`Invalid recurrence type: "${recType}". Must be one of: ${VALID_RECURRENCE_TYPES.join(", ")}`);
5577
+ }
5578
+ recurrence = { type: recType };
5579
+ if (rec.interval !== void 0) {
5580
+ const interval = Number(rec.interval);
5581
+ if (!Number.isInteger(interval) || interval < 1) {
5582
+ throw new Error("recurrence.interval must be a positive integer");
5583
+ }
5584
+ recurrence.interval = interval;
5585
+ }
5586
+ if (rec.daysOfWeek !== void 0) {
5587
+ if (!Array.isArray(rec.daysOfWeek)) throw new Error("recurrence.daysOfWeek must be an array");
5588
+ const days = rec.daysOfWeek.map(Number);
5589
+ if (days.some((d) => !Number.isInteger(d) || d < 0 || d > 6)) {
5590
+ throw new Error("recurrence.daysOfWeek values must be integers 0-6");
5591
+ }
5592
+ recurrence.daysOfWeek = days;
5593
+ }
5594
+ if (rec.expression !== void 0) {
5595
+ if (typeof rec.expression !== "string") throw new Error("recurrence.expression must be a string");
5596
+ recurrence.expression = rec.expression;
5597
+ }
5598
+ if (rec.endsAt !== void 0) {
5599
+ const endsAtDate = new Date(rec.endsAt);
5600
+ if (isNaN(endsAtDate.getTime())) {
5601
+ throw new Error(`Invalid recurrence.endsAt: "${rec.endsAt}"`);
5602
+ }
5603
+ recurrence.endsAt = endsAtDate.getTime();
5604
+ }
5605
+ if (rec.maxOccurrences !== void 0) {
5606
+ const max = Number(rec.maxOccurrences);
5607
+ if (!Number.isInteger(max) || max < 1) {
5608
+ throw new Error("recurrence.maxOccurrences must be a positive integer");
5609
+ }
5610
+ recurrence.maxOccurrences = max;
5611
+ }
5612
+ }
5437
5613
  const conversationId = context.conversationId || context.runId;
5438
5614
  const reminder = await store.create({
5439
5615
  task,
5440
5616
  scheduledAt,
5441
5617
  timezone,
5442
5618
  conversationId,
5443
- tenantId: context.tenantId
5619
+ tenantId: context.tenantId,
5620
+ recurrence
5444
5621
  });
5445
5622
  return {
5446
5623
  ok: true,
@@ -5449,14 +5626,16 @@ var createReminderTools = (store) => [
5449
5626
  task: reminder.task,
5450
5627
  scheduledAt: new Date(reminder.scheduledAt).toISOString(),
5451
5628
  timezone: reminder.timezone ?? "UTC",
5452
- status: reminder.status
5629
+ status: reminder.status,
5630
+ recurrence: reminder.recurrence ?? void 0,
5631
+ occurrenceCount: reminder.occurrenceCount ?? 0
5453
5632
  }
5454
5633
  };
5455
5634
  }
5456
5635
  }),
5457
5636
  defineTool8({
5458
5637
  name: "list_reminders",
5459
- description: "List reminders for this agent. Returns all reminders by default; use the status filter to show only pending or cancelled ones. Fired reminders are automatically deleted after delivery.",
5638
+ description: "List reminders for this agent. Returns all reminders by default; use the status filter to show only pending or cancelled ones. Fired one-time reminders are automatically deleted after delivery. Recurring reminders stay active and show their recurrence config and fire count.",
5460
5639
  inputSchema: {
5461
5640
  type: "object",
5462
5641
  properties: {
@@ -5484,7 +5663,9 @@ var createReminderTools = (store) => [
5484
5663
  scheduledAt: new Date(r.scheduledAt).toISOString(),
5485
5664
  timezone: r.timezone ?? "UTC",
5486
5665
  status: r.status,
5487
- createdAt: new Date(r.createdAt).toISOString()
5666
+ createdAt: new Date(r.createdAt).toISOString(),
5667
+ recurrence: r.recurrence ?? void 0,
5668
+ occurrenceCount: r.occurrenceCount ?? 0
5488
5669
  })),
5489
5670
  count: reminders.length
5490
5671
  };
@@ -5492,7 +5673,7 @@ var createReminderTools = (store) => [
5492
5673
  }),
5493
5674
  defineTool8({
5494
5675
  name: "cancel_reminder",
5495
- description: "Cancel a pending reminder by its ID.",
5676
+ description: "Cancel a pending reminder by its ID. This works for both one-time and recurring reminders \u2014 cancelling a recurring reminder stops all future occurrences.",
5496
5677
  inputSchema: {
5497
5678
  type: "object",
5498
5679
  properties: {
@@ -6659,15 +6840,19 @@ function isAnthropicModel(model) {
6659
6840
  }
6660
6841
  return model.provider === "anthropic" || model.provider.includes("anthropic") || model.modelId.includes("anthropic") || model.modelId.includes("claude");
6661
6842
  }
6662
- function addPromptCacheBreakpoints(messages, model) {
6843
+ function addPromptCacheBreakpoints(messages, model, targetIndex) {
6663
6844
  if (messages.length === 0 || !isAnthropicModel(model)) {
6664
6845
  return messages;
6665
6846
  }
6847
+ const index = targetIndex ?? messages.length - 1;
6848
+ if (index < 0 || index >= messages.length) {
6849
+ return messages;
6850
+ }
6666
6851
  const cacheDirective = {
6667
6852
  anthropic: { cacheControl: { type: "ephemeral" } }
6668
6853
  };
6669
- return messages.map((message, index) => {
6670
- if (index === messages.length - 1) {
6854
+ return messages.map((message, i) => {
6855
+ if (i === index) {
6671
6856
  return {
6672
6857
  ...message,
6673
6858
  providerOptions: {
@@ -7800,6 +7985,25 @@ var hasUntruncatedToolResults = (messages) => {
7800
7985
  }
7801
7986
  return false;
7802
7987
  };
7988
+ var findLastStableCacheIndex = (messages) => {
7989
+ for (let i = 0; i < messages.length; i += 1) {
7990
+ const msg = messages[i];
7991
+ if (msg.role !== "tool") continue;
7992
+ if (!Array.isArray(msg.content)) continue;
7993
+ for (const part of msg.content) {
7994
+ if (!part || typeof part !== "object") continue;
7995
+ const p = part;
7996
+ if (p.type !== "tool-result" || !p.output) continue;
7997
+ if (p.output.type === "json") return i - 1;
7998
+ if (p.output.type === "text" && typeof p.output.value === "string") {
7999
+ if (!p.output.value.startsWith(TOOL_RESULT_TRUNCATED_PREFIX)) {
8000
+ return i - 1;
8001
+ }
8002
+ }
8003
+ }
8004
+ }
8005
+ return messages.length - 1;
8006
+ };
7803
8007
  var DEVELOPMENT_MODE_CONTEXT = `## Development Mode Context
7804
8008
 
7805
8009
  You are running locally in development mode. Treat this as an editable agent workspace.
@@ -8771,9 +8975,10 @@ var AgentHarness = class _AgentHarness {
8771
8975
  config?.network
8772
8976
  );
8773
8977
  this.registerIfMissing(createBashTool(this.bashManager));
8774
- this.registerIfMissing(createReadFileTool(engine));
8775
- this.registerIfMissing(createEditFileTool(engine));
8776
- this.registerIfMissing(createWriteFileTool(engine));
8978
+ const getFs = (tenantId) => this.bashManager.getFs(tenantId);
8979
+ this.registerIfMissing(createReadFileTool(getFs));
8980
+ this.registerIfMissing(createEditFileTool(getFs));
8981
+ this.registerIfMissing(createWriteFileTool(getFs));
8777
8982
  if (config?.isolate) {
8778
8983
  const { createRunCodeTool, buildRunCodeDescription, bundleLibraries } = await import("./isolate-TCWTUVG4.js");
8779
8984
  let libraryPreamble = null;
@@ -9072,14 +9277,13 @@ var AgentHarness = class _AgentHarness {
9072
9277
  );
9073
9278
  }
9074
9279
  const hasFullToolResults = hasUntruncatedToolResults(messages);
9075
- const enablePromptCache = !hasFullToolResults;
9076
- if (!enablePromptCache) {
9280
+ if (hasFullToolResults) {
9077
9281
  console.info(
9078
- `[poncho][cost] Prompt cache write disabled for run "${runId}" (untruncated tool results present in history).`
9282
+ `[poncho][cost] Prompt cache breakpoint will be placed before untruncated tool results for run "${runId}" (stable prefix only).`
9079
9283
  );
9080
9284
  } else {
9081
9285
  console.info(
9082
- `[poncho][cost] Prompt cache write enabled for run "${runId}" (history has no untruncated tool results).`
9286
+ `[poncho][cost] Prompt cache breakpoint will be placed at history tail for run "${runId}" (no untruncated tool results).`
9083
9287
  );
9084
9288
  }
9085
9289
  const inputMessageCount = messages.length;
@@ -9174,9 +9378,14 @@ Code is wrapped in an async IIFE \u2014 use \`return\` to return a value to the
9174
9378
  const promptWithSkills = this.skillContextWindow ? `${agentPrompt}${developmentContext}
9175
9379
 
9176
9380
  ${this.skillContextWindow}${browserContext}${fsContext}${isolateContext}` : `${agentPrompt}${developmentContext}${browserContext}${fsContext}${isolateContext}`;
9381
+ const hourlyTime = (() => {
9382
+ const d = /* @__PURE__ */ new Date();
9383
+ d.setUTCMinutes(0, 0, 0);
9384
+ return d.toISOString();
9385
+ })();
9177
9386
  const timeContext = this.reminderStore ? `
9178
9387
 
9179
- Current UTC time: ${(/* @__PURE__ */ new Date()).toISOString()}` : "";
9388
+ Current UTC time (hour precision): ${hourlyTime}` : "";
9180
9389
  return `${promptWithSkills}${memoryContext}${todoContext}${timeContext}`;
9181
9390
  };
9182
9391
  let systemPrompt = buildSystemPrompt();
@@ -9615,7 +9824,12 @@ ${textContent}` };
9615
9824
  const coreMessages = cachedCoreMessages;
9616
9825
  const temperature = agent.frontmatter.model?.temperature ?? 0.2;
9617
9826
  const maxTokens = agent.frontmatter.model?.maxTokens;
9618
- const cachedMessages = enablePromptCache ? addPromptCacheBreakpoints(coreMessages, modelInstance) : coreMessages;
9827
+ const breakpointIndex = hasFullToolResults ? findLastStableCacheIndex(coreMessages) : coreMessages.length - 1;
9828
+ const cachedMessages = addPromptCacheBreakpoints(
9829
+ coreMessages,
9830
+ modelInstance,
9831
+ breakpointIndex
9832
+ );
9619
9833
  const telemetryEnabled = this.loadedConfig?.telemetry?.enabled !== false;
9620
9834
  const result = await streamText({
9621
9835
  model: modelInstance,
@@ -10399,12 +10613,22 @@ var InMemoryReminderStore = class {
10399
10613
  createdAt: Date.now(),
10400
10614
  conversationId: input.conversationId,
10401
10615
  ownerId: input.ownerId,
10402
- tenantId: input.tenantId
10616
+ tenantId: input.tenantId,
10617
+ recurrence: input.recurrence ?? null,
10618
+ occurrenceCount: 0
10403
10619
  };
10404
10620
  this.reminders = pruneStale(this.reminders);
10405
10621
  this.reminders.push(reminder);
10406
10622
  return reminder;
10407
10623
  }
10624
+ async update(id, fields) {
10625
+ const reminder = this.reminders.find((r) => r.id === id);
10626
+ if (!reminder) throw new Error(`Reminder "${id}" not found`);
10627
+ if (fields.scheduledAt !== void 0) reminder.scheduledAt = fields.scheduledAt;
10628
+ if (fields.occurrenceCount !== void 0) reminder.occurrenceCount = fields.occurrenceCount;
10629
+ if (fields.status !== void 0) reminder.status = fields.status;
10630
+ return reminder;
10631
+ }
10408
10632
  async cancel(id) {
10409
10633
  const reminder = this.reminders.find((r) => r.id === id);
10410
10634
  if (!reminder) throw new Error(`Reminder "${id}" not found`);
@@ -10418,6 +10642,101 @@ var InMemoryReminderStore = class {
10418
10642
  this.reminders = this.reminders.filter((r) => r.id !== id);
10419
10643
  }
10420
10644
  };
10645
+ var computeNextOccurrence = (reminder) => {
10646
+ const rec = reminder.recurrence;
10647
+ if (!rec) return null;
10648
+ const fired = (reminder.occurrenceCount ?? 0) + 1;
10649
+ if (rec.maxOccurrences && fired >= rec.maxOccurrences) return null;
10650
+ const interval = rec.interval ?? 1;
10651
+ const prev = reminder.scheduledAt;
10652
+ let next;
10653
+ switch (rec.type) {
10654
+ case "daily": {
10655
+ next = prev + interval * 24 * 60 * 60 * 1e3;
10656
+ break;
10657
+ }
10658
+ case "weekly": {
10659
+ if (rec.daysOfWeek && rec.daysOfWeek.length > 0) {
10660
+ const d = new Date(prev);
10661
+ const days = [...rec.daysOfWeek].sort((a, b) => a - b);
10662
+ const currentDay = d.getUTCDay();
10663
+ let nextDay = days.find((day) => day > currentDay);
10664
+ if (nextDay !== void 0) {
10665
+ const delta = nextDay - currentDay;
10666
+ next = prev + delta * 24 * 60 * 60 * 1e3;
10667
+ } else {
10668
+ const delta = 7 * interval - currentDay + days[0];
10669
+ next = prev + delta * 24 * 60 * 60 * 1e3;
10670
+ }
10671
+ } else {
10672
+ next = prev + interval * 7 * 24 * 60 * 60 * 1e3;
10673
+ }
10674
+ break;
10675
+ }
10676
+ case "monthly": {
10677
+ const d = new Date(prev);
10678
+ d.setUTCMonth(d.getUTCMonth() + interval);
10679
+ next = d.getTime();
10680
+ break;
10681
+ }
10682
+ case "cron": {
10683
+ if (!rec.expression) return null;
10684
+ const parsed = parseCronExpression(rec.expression);
10685
+ if (!parsed) return null;
10686
+ next = nextCronOccurrence(prev, parsed);
10687
+ if (next <= prev) return null;
10688
+ break;
10689
+ }
10690
+ default:
10691
+ return null;
10692
+ }
10693
+ if (rec.endsAt && next > rec.endsAt) return null;
10694
+ return next;
10695
+ };
10696
+ var expandField = (field, min, max) => {
10697
+ const values = /* @__PURE__ */ new Set();
10698
+ for (const part of field.split(",")) {
10699
+ const stepMatch = part.match(/^(.+)\/(\d+)$/);
10700
+ const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
10701
+ const range = stepMatch ? stepMatch[1] : part;
10702
+ if (range === "*") {
10703
+ for (let i = min; i <= max; i += step) values.add(i);
10704
+ } else if (range.includes("-")) {
10705
+ const [lo, hi] = range.split("-").map(Number);
10706
+ if (isNaN(lo) || isNaN(hi)) return null;
10707
+ for (let i = lo; i <= hi; i += step) values.add(i);
10708
+ } else {
10709
+ const n = parseInt(range, 10);
10710
+ if (isNaN(n)) return null;
10711
+ values.add(n);
10712
+ }
10713
+ }
10714
+ return values;
10715
+ };
10716
+ var parseCronExpression = (expr) => {
10717
+ const parts = expr.trim().split(/\s+/);
10718
+ if (parts.length !== 5) return null;
10719
+ const minutes = expandField(parts[0], 0, 59);
10720
+ const hours = expandField(parts[1], 0, 23);
10721
+ const daysOfMonth = expandField(parts[2], 1, 31);
10722
+ const months = expandField(parts[3], 1, 12);
10723
+ const daysOfWeek = expandField(parts[4], 0, 6);
10724
+ if (!minutes || !hours || !daysOfMonth || !months || !daysOfWeek) return null;
10725
+ return { minutes, hours, daysOfMonth, months, daysOfWeek };
10726
+ };
10727
+ var nextCronOccurrence = (afterMs, fields) => {
10728
+ const d = new Date(afterMs);
10729
+ d.setUTCSeconds(0, 0);
10730
+ d.setUTCMinutes(d.getUTCMinutes() + 1);
10731
+ const limit = afterMs + 366 * 24 * 60 * 60 * 1e3;
10732
+ while (d.getTime() < limit) {
10733
+ if (fields.months.has(d.getUTCMonth() + 1) && fields.daysOfMonth.has(d.getUTCDate()) && fields.daysOfWeek.has(d.getUTCDay()) && fields.hours.has(d.getUTCHours()) && fields.minutes.has(d.getUTCMinutes())) {
10734
+ return d.getTime();
10735
+ }
10736
+ d.setUTCMinutes(d.getUTCMinutes() + 1);
10737
+ }
10738
+ return afterMs;
10739
+ };
10421
10740
  var createReminderStore = (_agentId, _config, _options) => {
10422
10741
  return new InMemoryReminderStore();
10423
10742
  };
@@ -10491,16 +10810,19 @@ var InMemoryConversationStore = class {
10491
10810
  this.purgeExpired();
10492
10811
  return this.conversations.get(conversationId);
10493
10812
  }
10494
- async create(ownerId = DEFAULT_OWNER3, title, tenantId = null) {
10813
+ async create(ownerId = DEFAULT_OWNER3, title, tenantId = null, init) {
10495
10814
  const now2 = Date.now();
10496
10815
  const conversation = {
10497
10816
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now2}-${Math.random()}`,
10498
10817
  title: normalizeTitle3(title),
10499
- messages: [],
10818
+ messages: init?.messages ?? [],
10500
10819
  ownerId,
10501
10820
  tenantId,
10502
10821
  createdAt: now2,
10503
- updatedAt: now2
10822
+ updatedAt: now2,
10823
+ ...init?.parentConversationId !== void 0 ? { parentConversationId: init.parentConversationId } : {},
10824
+ ...init?.subagentMeta !== void 0 ? { subagentMeta: init.subagentMeta } : {},
10825
+ ...init?.channelMeta !== void 0 ? { channelMeta: init.channelMeta } : {}
10504
10826
  };
10505
10827
  this.conversations.set(conversation.conversationId, conversation);
10506
10828
  return conversation;
@@ -10600,6 +10922,7 @@ export {
10600
10922
  buildSkillContextWindow,
10601
10923
  compactMessages,
10602
10924
  completeOpenAICodexDeviceAuth,
10925
+ computeNextOccurrence,
10603
10926
  createBashTool,
10604
10927
  createConversationStore,
10605
10928
  createConversationStoreFromEngine,