@poncho-ai/harness 0.37.2 → 0.39.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
@@ -2300,7 +2300,7 @@ var ponchoDocsTool = defineTool({
2300
2300
  import { randomUUID as randomUUID5 } from "crypto";
2301
2301
  import { readFile as readFile8 } from "fs/promises";
2302
2302
  import { resolve as resolve11 } from "path";
2303
- import { defineTool as defineTool12, getTextContent as getTextContent2 } from "@poncho-ai/sdk";
2303
+ import { defineTool as defineTool12, getTextContent as getTextContent2, createLogger as createLogger5, formatError as fmtErr, url as urlColor } from "@poncho-ai/sdk";
2304
2304
 
2305
2305
  // src/upload-store.ts
2306
2306
  import { createHash as createHash2 } from "crypto";
@@ -2308,6 +2308,8 @@ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3, rm as
2308
2308
  import { createRequire } from "module";
2309
2309
  import { homedir as homedir2 } from "os";
2310
2310
  import { resolve as resolve5 } from "path";
2311
+ import { createLogger } from "@poncho-ai/sdk";
2312
+ var uploadLog = createLogger("upload");
2311
2313
  var tryImport = async (mod, workingDir) => {
2312
2314
  try {
2313
2315
  return await import(
@@ -2345,7 +2347,7 @@ var CachedUploadStore = class {
2345
2347
  this.cache.set(key, { data, ts: now2 });
2346
2348
  this.evict();
2347
2349
  this.inner.put(key, data, mediaType).catch((err) => {
2348
- console.error("[poncho] background upload failed:", err instanceof Error ? err.message : err);
2350
+ uploadLog.error(`background upload failed: ${err instanceof Error ? err.message : err}`);
2349
2351
  });
2350
2352
  return ref;
2351
2353
  }
@@ -2559,7 +2561,7 @@ var S3UploadStore = class {
2559
2561
  }
2560
2562
  };
2561
2563
  var warn = (msg) => {
2562
- console.warn(`[poncho] \u26A0 ${msg}`);
2564
+ uploadLog.warn(msg);
2563
2565
  };
2564
2566
  var createUploadStore = async (config, workingDir) => {
2565
2567
  const provider = config?.provider ?? "local";
@@ -2573,7 +2575,7 @@ var createUploadStore = async (config, workingDir) => {
2573
2575
  const store = new VercelBlobUploadStore(workingDir, config?.access ?? "public");
2574
2576
  try {
2575
2577
  await store.loadSdk();
2576
- console.log("[poncho] uploads: using vercel-blob store");
2578
+ uploadLog.item("using vercel-blob store");
2577
2579
  return new CachedUploadStore(store);
2578
2580
  } catch {
2579
2581
  warn(
@@ -2599,7 +2601,7 @@ var createUploadStore = async (config, workingDir) => {
2599
2601
  );
2600
2602
  try {
2601
2603
  await store.ensureClient();
2602
- console.log(`[poncho] uploads: using s3 store (bucket: ${bucket})`);
2604
+ uploadLog.item(`using s3 store (bucket: ${bucket})`);
2603
2605
  return new CachedUploadStore(store);
2604
2606
  } catch {
2605
2607
  warn(
@@ -2663,16 +2665,41 @@ var InMemoryEngine = class {
2663
2665
  get: async (conversationId) => {
2664
2666
  return this.convs.get(conversationId);
2665
2667
  },
2666
- create: async (ownerId, title, tenantId) => {
2668
+ // In-memory storage has no separate archive blob, so both variants
2669
+ // return the same conversation object.
2670
+ getWithArchive: async (conversationId) => {
2671
+ return this.convs.get(conversationId);
2672
+ },
2673
+ getStatusSnapshot: async (conversationId) => {
2674
+ const c = this.convs.get(conversationId);
2675
+ if (!c) return void 0;
2676
+ return {
2677
+ conversationId: c.conversationId,
2678
+ updatedAt: c.updatedAt,
2679
+ messageCount: c.messages.length,
2680
+ hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
2681
+ hasContinuationMessages: Array.isArray(c._continuationMessages) && c._continuationMessages.length > 0,
2682
+ parentConversationId: c.parentConversationId ?? null,
2683
+ ownerId: c.ownerId,
2684
+ tenantId: c.tenantId,
2685
+ runStatus: c.runStatus ?? null
2686
+ };
2687
+ },
2688
+ create: async (ownerId, title, tenantId, init) => {
2667
2689
  const now2 = Date.now();
2668
2690
  const conv = {
2669
2691
  conversationId: randomUUID3(),
2670
2692
  title: normalizeTitle(title),
2671
- messages: [],
2693
+ messages: init?.messages ?? [],
2672
2694
  ownerId: ownerId ?? DEFAULT_OWNER,
2673
2695
  tenantId: tenantId === void 0 ? null : tenantId,
2674
2696
  createdAt: now2,
2675
- updatedAt: now2
2697
+ updatedAt: now2,
2698
+ ...init?.parentConversationId !== void 0 ? { parentConversationId: init.parentConversationId } : {},
2699
+ ...init?.parentMessageId !== void 0 ? { parentMessageId: init.parentMessageId } : {},
2700
+ ...init?.threadMeta !== void 0 ? { threadMeta: init.threadMeta } : {},
2701
+ ...init?.subagentMeta !== void 0 ? { subagentMeta: init.subagentMeta } : {},
2702
+ ...init?.channelMeta !== void 0 ? { channelMeta: init.channelMeta } : {}
2676
2703
  };
2677
2704
  this.convs.set(conv.conversationId, conv);
2678
2705
  return conv;
@@ -2720,6 +2747,16 @@ var InMemoryEngine = class {
2720
2747
  if (!conv) return void 0;
2721
2748
  conv.runningCallbackSince = void 0;
2722
2749
  return conv;
2750
+ },
2751
+ listThreads: async (parentConversationId) => {
2752
+ const results = [];
2753
+ for (const c of this.convs.values()) {
2754
+ if (c.parentConversationId === parentConversationId && typeof c.parentMessageId === "string") {
2755
+ results.push(this.toSummary(c));
2756
+ }
2757
+ }
2758
+ results.sort((a, b) => b.updatedAt - a.updatedAt);
2759
+ return results;
2723
2760
  }
2724
2761
  };
2725
2762
  // -----------------------------------------------------------------------
@@ -2776,11 +2813,21 @@ var InMemoryEngine = class {
2776
2813
  createdAt: Date.now(),
2777
2814
  conversationId: input.conversationId,
2778
2815
  ownerId: input.ownerId,
2779
- tenantId: input.tenantId
2816
+ tenantId: input.tenantId,
2817
+ recurrence: input.recurrence ?? null,
2818
+ occurrenceCount: 0
2780
2819
  };
2781
2820
  this.reminderData.set(r.id, r);
2782
2821
  return r;
2783
2822
  },
2823
+ update: async (id, fields) => {
2824
+ const r = this.reminderData.get(id);
2825
+ if (!r) throw new Error(`Reminder ${id} not found`);
2826
+ if (fields.scheduledAt !== void 0) r.scheduledAt = fields.scheduledAt;
2827
+ if (fields.occurrenceCount !== void 0) r.occurrenceCount = fields.occurrenceCount;
2828
+ if (fields.status !== void 0) r.status = fields.status;
2829
+ return r;
2830
+ },
2784
2831
  cancel: async (id) => {
2785
2832
  const r = this.reminderData.get(id);
2786
2833
  if (!r) throw new Error(`Reminder ${id} not found`);
@@ -3013,6 +3060,7 @@ var InMemoryEngine = class {
3013
3060
  messageCount: c.messages.length,
3014
3061
  hasPendingApprovals: (c.pendingApprovals?.length ?? 0) > 0,
3015
3062
  parentConversationId: c.parentConversationId,
3063
+ parentMessageId: c.parentMessageId,
3016
3064
  channelMeta: c.channelMeta
3017
3065
  };
3018
3066
  }
@@ -3057,6 +3105,7 @@ import { dirname as dirname2, resolve as resolve6 } from "path";
3057
3105
 
3058
3106
  // src/storage/sql-dialect.ts
3059
3107
  import { randomUUID as randomUUID4 } from "crypto";
3108
+ import { createLogger as createLogger2 } from "@poncho-ai/sdk";
3060
3109
 
3061
3110
  // src/storage/schema.ts
3062
3111
  var migrations = [
@@ -3207,10 +3256,32 @@ var migrations = [
3207
3256
  ]
3208
3257
  ];
3209
3258
  }
3259
+ },
3260
+ {
3261
+ version: 5,
3262
+ name: "add_reminder_recurrence",
3263
+ up: (d) => {
3264
+ const jsonType = d === "sqlite" ? "TEXT" : "JSONB";
3265
+ return [
3266
+ `ALTER TABLE reminders ADD COLUMN recurrence ${jsonType}`,
3267
+ `ALTER TABLE reminders ADD COLUMN occurrence_count INTEGER NOT NULL DEFAULT 0`
3268
+ ];
3269
+ }
3270
+ },
3271
+ {
3272
+ version: 6,
3273
+ name: "add_thread_anchor",
3274
+ up: () => [
3275
+ `ALTER TABLE conversations ADD COLUMN parent_message_id TEXT`,
3276
+ `CREATE INDEX IF NOT EXISTS idx_conversations_thread_anchor
3277
+ ON conversations (parent_conversation_id, parent_message_id)
3278
+ WHERE parent_message_id IS NOT NULL`
3279
+ ]
3210
3280
  }
3211
3281
  ];
3212
3282
 
3213
3283
  // src/storage/sql-dialect.ts
3284
+ var egressLog = createLogger2("egress");
3214
3285
  var sqliteDialect = {
3215
3286
  tag: "sqlite",
3216
3287
  param: () => "?",
@@ -3241,9 +3312,68 @@ var rewrite = (sql, dialect) => {
3241
3312
  }
3242
3313
  return sql;
3243
3314
  };
3315
+ var ConversationEgressMeter = class {
3316
+ read = {};
3317
+ write = {};
3318
+ lastLogAt = Date.now();
3319
+ logIntervalMs;
3320
+ enabled;
3321
+ constructor(logIntervalMs = 6e4) {
3322
+ this.logIntervalMs = logIntervalMs;
3323
+ this.enabled = process.env.PONCHO_LOG_EGRESS === "1";
3324
+ }
3325
+ trackRead(method, bytes) {
3326
+ const b = this.read[method] ??= { calls: 0, bytes: 0 };
3327
+ b.calls += 1;
3328
+ b.bytes += bytes;
3329
+ this.maybeLog();
3330
+ }
3331
+ trackWrite(method, bytes) {
3332
+ const b = this.write[method] ??= { calls: 0, bytes: 0 };
3333
+ b.calls += 1;
3334
+ b.bytes += bytes;
3335
+ this.maybeLog();
3336
+ }
3337
+ maybeLog() {
3338
+ if (!this.enabled) return;
3339
+ const now2 = Date.now();
3340
+ if (now2 - this.lastLogAt < this.logIntervalMs) return;
3341
+ this.lastLogAt = now2;
3342
+ this.flush();
3343
+ }
3344
+ flush() {
3345
+ if (!this.enabled) return;
3346
+ const fmt = (buckets) => Object.entries(buckets).filter(([, b]) => b.calls > 0).map(([m, b]) => `${m}=${b.calls}calls/${fmtBytes(b.bytes)}`).join(", ");
3347
+ const r = fmt(this.read);
3348
+ const w = fmt(this.write);
3349
+ if (r || w) {
3350
+ egressLog.debug(`read: ${r || "(none)"} | write: ${w || "(none)"}`);
3351
+ }
3352
+ for (const b of Object.values(this.read)) {
3353
+ b.calls = 0;
3354
+ b.bytes = 0;
3355
+ }
3356
+ for (const b of Object.values(this.write)) {
3357
+ b.calls = 0;
3358
+ b.bytes = 0;
3359
+ }
3360
+ }
3361
+ };
3362
+ var fmtBytes = (n) => {
3363
+ if (n < 1024) return `${n}B`;
3364
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`;
3365
+ return `${(n / (1024 * 1024)).toFixed(2)}MB`;
3366
+ };
3367
+ var colBytes = (v) => {
3368
+ if (v == null) return 0;
3369
+ if (typeof v === "string") return v.length;
3370
+ if (Buffer.isBuffer(v)) return v.byteLength;
3371
+ return JSON.stringify(v).length;
3372
+ };
3244
3373
  var SqlStorageEngine = class {
3245
3374
  dialect;
3246
3375
  agentId;
3376
+ egressMeter = new ConversationEgressMeter();
3247
3377
  constructor(dialect, agentId) {
3248
3378
  this.dialect = dialect;
3249
3379
  this.agentId = agentId;
@@ -3255,6 +3385,10 @@ var SqlStorageEngine = class {
3255
3385
  await this.onBeforeInit();
3256
3386
  await this.runMigrations();
3257
3387
  }
3388
+ /** Flush egress stats before shutdown. Subclasses should call super. */
3389
+ flushEgressStats() {
3390
+ this.egressMeter.flush();
3391
+ }
3258
3392
  /** Hook for subclass-specific setup (e.g. WAL mode). */
3259
3393
  async onBeforeInit() {
3260
3394
  }
@@ -3305,7 +3439,7 @@ var SqlStorageEngine = class {
3305
3439
  const filterTenant = tenantId !== void 0;
3306
3440
  const params = [this.agentId];
3307
3441
  let sql = `SELECT id, title, updated_at, created_at, owner_id, tenant_id,
3308
- message_count, parent_conversation_id,
3442
+ message_count, parent_conversation_id, parent_message_id,
3309
3443
  has_pending_approvals, channel_meta
3310
3444
  FROM conversations WHERE agent_id = $1`;
3311
3445
  if (filterTenant) {
@@ -3321,11 +3455,62 @@ var SqlStorageEngine = class {
3321
3455
  return rows.map((r) => this.rowToSummary(r));
3322
3456
  },
3323
3457
  get: async (conversationId) => {
3458
+ const row = await this.executor.get(
3459
+ rewrite("SELECT data, harness_messages, continuation_messages FROM conversations WHERE id = $1 AND agent_id = $2", this.dialect),
3460
+ [conversationId, this.agentId]
3461
+ );
3462
+ if (!row) return void 0;
3463
+ this.egressMeter.trackRead(
3464
+ "get",
3465
+ colBytes(row.data) + colBytes(row.harness_messages) + colBytes(row.continuation_messages)
3466
+ );
3467
+ const conv = this.parseConversation(row.data);
3468
+ if (row.harness_messages) {
3469
+ conv._harnessMessages = typeof row.harness_messages === "string" ? JSON.parse(row.harness_messages) : row.harness_messages;
3470
+ }
3471
+ if (row.continuation_messages) {
3472
+ conv._continuationMessages = typeof row.continuation_messages === "string" ? JSON.parse(row.continuation_messages) : row.continuation_messages;
3473
+ }
3474
+ return conv;
3475
+ },
3476
+ getStatusSnapshot: async (conversationId) => {
3477
+ const runStatusExpr = this.dialect.tag === "sqlite" ? "json_extract(data, '$.runStatus')" : "data->>'runStatus'";
3478
+ const row = await this.executor.get(
3479
+ rewrite(
3480
+ `SELECT updated_at, message_count, has_pending_approvals,
3481
+ parent_conversation_id, owner_id, tenant_id,
3482
+ (continuation_messages IS NOT NULL) AS has_continuation_messages,
3483
+ ${runStatusExpr} AS run_status
3484
+ FROM conversations WHERE id = $1 AND agent_id = $2`,
3485
+ this.dialect
3486
+ ),
3487
+ [conversationId, this.agentId]
3488
+ );
3489
+ if (!row) return void 0;
3490
+ this.egressMeter.trackRead("getStatusSnapshot", 200);
3491
+ const runStatus = row.run_status === "running" || row.run_status === "idle" ? row.run_status : null;
3492
+ return {
3493
+ conversationId,
3494
+ updatedAt: new Date(row.updated_at).getTime(),
3495
+ messageCount: row.message_count ?? 0,
3496
+ hasPendingApprovals: !!row.has_pending_approvals,
3497
+ hasContinuationMessages: !!row.has_continuation_messages,
3498
+ parentConversationId: row.parent_conversation_id,
3499
+ ownerId: row.owner_id,
3500
+ tenantId: row.tenant_id === "__default__" ? null : row.tenant_id,
3501
+ runStatus
3502
+ };
3503
+ },
3504
+ getWithArchive: async (conversationId) => {
3324
3505
  const row = await this.executor.get(
3325
3506
  rewrite("SELECT data, tool_result_archive, harness_messages, continuation_messages FROM conversations WHERE id = $1 AND agent_id = $2", this.dialect),
3326
3507
  [conversationId, this.agentId]
3327
3508
  );
3328
3509
  if (!row) return void 0;
3510
+ this.egressMeter.trackRead(
3511
+ "getWithArchive",
3512
+ colBytes(row.data) + colBytes(row.tool_result_archive) + colBytes(row.harness_messages) + colBytes(row.continuation_messages)
3513
+ );
3329
3514
  const conv = this.parseConversation(row.data);
3330
3515
  if (row.tool_result_archive) {
3331
3516
  conv._toolResultArchive = typeof row.tool_result_archive === "string" ? JSON.parse(row.tool_result_archive) : row.tool_result_archive;
@@ -3338,24 +3523,30 @@ var SqlStorageEngine = class {
3338
3523
  }
3339
3524
  return conv;
3340
3525
  },
3341
- create: async (ownerId, title, tenantId) => {
3526
+ create: async (ownerId, title, tenantId, init) => {
3342
3527
  const id = randomUUID4();
3343
3528
  const now2 = Date.now();
3344
3529
  const conv = {
3345
3530
  conversationId: id,
3346
3531
  title: normalizeTitle2(title),
3347
- messages: [],
3532
+ messages: init?.messages ?? [],
3348
3533
  ownerId: ownerId ?? DEFAULT_OWNER2,
3349
3534
  tenantId: tenantId === void 0 ? null : tenantId,
3350
3535
  createdAt: now2,
3351
- updatedAt: now2
3536
+ updatedAt: now2,
3537
+ ...init?.parentConversationId !== void 0 ? { parentConversationId: init.parentConversationId } : {},
3538
+ ...init?.parentMessageId !== void 0 ? { parentMessageId: init.parentMessageId } : {},
3539
+ ...init?.subagentMeta !== void 0 ? { subagentMeta: init.subagentMeta } : {},
3540
+ ...init?.threadMeta !== void 0 ? { threadMeta: init.threadMeta } : {},
3541
+ ...init?.channelMeta !== void 0 ? { channelMeta: init.channelMeta } : {}
3352
3542
  };
3353
3543
  const data = JSON.stringify(conv);
3544
+ const channelMetaJson = conv.channelMeta ? JSON.stringify(conv.channelMeta) : null;
3354
3545
  await this.executor.run(
3355
3546
  rewrite(
3356
3547
  `INSERT INTO conversations (id, agent_id, tenant_id, owner_id, title, data, message_count, created_at, updated_at,
3357
- parent_conversation_id, has_pending_approvals, channel_meta)
3358
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
3548
+ parent_conversation_id, parent_message_id, has_pending_approvals, channel_meta)
3549
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
3359
3550
  this.dialect
3360
3551
  ),
3361
3552
  [
@@ -3365,12 +3556,13 @@ var SqlStorageEngine = class {
3365
3556
  conv.ownerId,
3366
3557
  conv.title,
3367
3558
  data,
3368
- 0,
3559
+ conv.messages.length,
3369
3560
  new Date(now2).toISOString(),
3370
3561
  new Date(now2).toISOString(),
3371
- null,
3562
+ conv.parentConversationId ?? null,
3563
+ conv.parentMessageId ?? null,
3372
3564
  0,
3373
- null
3565
+ channelMetaJson
3374
3566
  ]
3375
3567
  );
3376
3568
  return conv;
@@ -3389,25 +3581,36 @@ var SqlStorageEngine = class {
3389
3581
  const archiveJson = archive ? JSON.stringify(archive) : null;
3390
3582
  const harnessJson = harnessMessages ? JSON.stringify(harnessMessages) : null;
3391
3583
  const continuationJson = continuationMessages ? JSON.stringify(continuationMessages) : null;
3584
+ this.egressMeter.trackWrite(
3585
+ "update",
3586
+ data.length + (archiveJson?.length ?? 0) + (harnessJson?.length ?? 0) + (continuationJson?.length ?? 0)
3587
+ );
3392
3588
  const msgCount = conversation.messages?.length ?? 0;
3393
3589
  const tid = normalizeTenant2(conversation.tenantId);
3394
3590
  const now2 = new Date(conversation.updatedAt).toISOString();
3395
3591
  const created = new Date(conversation.createdAt).toISOString();
3396
3592
  const parentConvId = conversation.parentConversationId ?? null;
3593
+ const parentMsgId = conversation.parentMessageId ?? null;
3397
3594
  const hasPendingApprovals = (conversation.pendingApprovals?.length ?? 0) > 0 ? 1 : 0;
3398
3595
  const channelMetaJson = conversation.channelMeta ? JSON.stringify(conversation.channelMeta) : null;
3399
3596
  await this.executor.run(
3400
3597
  rewrite(
3401
3598
  `INSERT INTO conversations (id, agent_id, tenant_id, owner_id, title, data, message_count, created_at, updated_at,
3402
3599
  tool_result_archive, harness_messages, continuation_messages,
3403
- parent_conversation_id, has_pending_approvals, channel_meta)
3404
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
3600
+ parent_conversation_id, parent_message_id, has_pending_approvals, channel_meta)
3601
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
3405
3602
  ${this.dialect.upsert(["id"])}
3406
3603
  data = excluded.data, title = excluded.title, message_count = excluded.message_count,
3407
3604
  updated_at = excluded.updated_at, tenant_id = excluded.tenant_id, owner_id = excluded.owner_id,
3408
- tool_result_archive = excluded.tool_result_archive, harness_messages = excluded.harness_messages,
3605
+ -- tool_result_archive is append-only from the caller's side; if the
3606
+ -- in-memory conversation was loaded via the light get() variant it
3607
+ -- won't have _toolResultArchive attached, so we must preserve the
3608
+ -- existing column instead of clobbering it with NULL.
3609
+ tool_result_archive = COALESCE(excluded.tool_result_archive, conversations.tool_result_archive),
3610
+ harness_messages = excluded.harness_messages,
3409
3611
  continuation_messages = excluded.continuation_messages,
3410
3612
  parent_conversation_id = excluded.parent_conversation_id,
3613
+ parent_message_id = excluded.parent_message_id,
3411
3614
  has_pending_approvals = excluded.has_pending_approvals,
3412
3615
  channel_meta = excluded.channel_meta`,
3413
3616
  this.dialect
@@ -3426,6 +3629,7 @@ var SqlStorageEngine = class {
3426
3629
  harnessJson,
3427
3630
  continuationJson,
3428
3631
  parentConvId,
3632
+ parentMsgId,
3429
3633
  hasPendingApprovals,
3430
3634
  channelMetaJson
3431
3635
  ]
@@ -3456,7 +3660,7 @@ var SqlStorageEngine = class {
3456
3660
  const pattern = `%${query}%`;
3457
3661
  const params = [this.agentId, pattern, pattern];
3458
3662
  let sql = `SELECT id, title, updated_at, created_at, owner_id, tenant_id,
3459
- message_count, parent_conversation_id,
3663
+ message_count, parent_conversation_id, parent_message_id,
3460
3664
  has_pending_approvals, channel_meta
3461
3665
  FROM conversations
3462
3666
  WHERE agent_id = $1 AND (title LIKE $2 OR data LIKE $3)`;
@@ -3480,6 +3684,21 @@ var SqlStorageEngine = class {
3480
3684
  conv.runningCallbackSince = void 0;
3481
3685
  await this.conversations.update(conv);
3482
3686
  return conv;
3687
+ },
3688
+ listThreads: async (parentConversationId) => {
3689
+ const sql = `SELECT id, title, updated_at, created_at, owner_id, tenant_id,
3690
+ message_count, parent_conversation_id, parent_message_id,
3691
+ has_pending_approvals, channel_meta
3692
+ FROM conversations
3693
+ WHERE agent_id = $1
3694
+ AND parent_conversation_id = $2
3695
+ AND parent_message_id IS NOT NULL
3696
+ ORDER BY updated_at DESC`;
3697
+ const rows = await this.executor.all(rewrite(sql, this.dialect), [
3698
+ this.agentId,
3699
+ parentConversationId
3700
+ ]);
3701
+ return rows.map((r) => this.rowToSummary(r));
3483
3702
  }
3484
3703
  };
3485
3704
  // -----------------------------------------------------------------------
@@ -3567,10 +3786,11 @@ var SqlStorageEngine = class {
3567
3786
  const id = randomUUID4();
3568
3787
  const now2 = Date.now();
3569
3788
  const tid = normalizeTenant2(input.tenantId);
3789
+ const recurrenceJson = input.recurrence ? JSON.stringify(input.recurrence) : null;
3570
3790
  await this.executor.run(
3571
3791
  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)`,
3792
+ `INSERT INTO reminders (id, agent_id, tenant_id, owner_id, conversation_id, task, status, scheduled_at, timezone, created_at, recurrence, occurrence_count)
3793
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
3574
3794
  this.dialect
3575
3795
  ),
3576
3796
  [
@@ -3583,7 +3803,9 @@ var SqlStorageEngine = class {
3583
3803
  "pending",
3584
3804
  input.scheduledAt,
3585
3805
  input.timezone ?? null,
3586
- new Date(now2).toISOString()
3806
+ new Date(now2).toISOString(),
3807
+ recurrenceJson,
3808
+ 0
3587
3809
  ]
3588
3810
  );
3589
3811
  return {
@@ -3595,9 +3817,52 @@ var SqlStorageEngine = class {
3595
3817
  createdAt: now2,
3596
3818
  conversationId: input.conversationId,
3597
3819
  ownerId: input.ownerId,
3598
- tenantId: input.tenantId
3820
+ tenantId: input.tenantId,
3821
+ recurrence: input.recurrence ?? null,
3822
+ occurrenceCount: 0
3599
3823
  };
3600
3824
  },
3825
+ update: async (id, fields) => {
3826
+ const setClauses = [];
3827
+ const params = [];
3828
+ let idx = 1;
3829
+ if (fields.scheduledAt !== void 0) {
3830
+ setClauses.push(`scheduled_at = $${idx++}`);
3831
+ params.push(fields.scheduledAt);
3832
+ }
3833
+ if (fields.occurrenceCount !== void 0) {
3834
+ setClauses.push(`occurrence_count = $${idx++}`);
3835
+ params.push(fields.occurrenceCount);
3836
+ }
3837
+ if (fields.status !== void 0) {
3838
+ setClauses.push(`status = $${idx++}`);
3839
+ params.push(fields.status);
3840
+ }
3841
+ if (setClauses.length === 0) {
3842
+ const row2 = await this.executor.get(
3843
+ rewrite("SELECT * FROM reminders WHERE id = $1 AND agent_id = $2", this.dialect),
3844
+ [id, this.agentId]
3845
+ );
3846
+ if (!row2) throw new Error(`Reminder ${id} not found`);
3847
+ return this.rowToReminder(row2);
3848
+ }
3849
+ params.push(id, this.agentId);
3850
+ const idIdx = idx++;
3851
+ const agentIdx = idx++;
3852
+ await this.executor.run(
3853
+ rewrite(
3854
+ `UPDATE reminders SET ${setClauses.join(", ")} WHERE id = $${idIdx} AND agent_id = $${agentIdx}`,
3855
+ this.dialect
3856
+ ),
3857
+ params
3858
+ );
3859
+ const row = await this.executor.get(
3860
+ rewrite("SELECT * FROM reminders WHERE id = $1 AND agent_id = $2", this.dialect),
3861
+ [id, this.agentId]
3862
+ );
3863
+ if (!row) throw new Error(`Reminder ${id} not found`);
3864
+ return this.rowToReminder(row);
3865
+ },
3601
3866
  cancel: async (id) => {
3602
3867
  await this.executor.run(
3603
3868
  rewrite(
@@ -3899,11 +4164,20 @@ var SqlStorageEngine = class {
3899
4164
  messageCount: row.message_count,
3900
4165
  hasPendingApprovals: !!row.has_pending_approvals,
3901
4166
  parentConversationId: row.parent_conversation_id || void 0,
4167
+ parentMessageId: row.parent_message_id || void 0,
3902
4168
  channelMeta: rawChannelMeta ? typeof rawChannelMeta === "string" ? JSON.parse(rawChannelMeta) : rawChannelMeta : void 0
3903
4169
  };
3904
4170
  }
3905
4171
  rowToReminder(row) {
3906
4172
  const tid = row.tenant_id;
4173
+ let recurrence = null;
4174
+ if (row.recurrence) {
4175
+ try {
4176
+ recurrence = typeof row.recurrence === "string" ? JSON.parse(row.recurrence) : row.recurrence;
4177
+ } catch {
4178
+ recurrence = null;
4179
+ }
4180
+ }
3907
4181
  return {
3908
4182
  id: row.id,
3909
4183
  task: row.task,
@@ -3913,7 +4187,9 @@ var SqlStorageEngine = class {
3913
4187
  createdAt: new Date(row.created_at).getTime(),
3914
4188
  conversationId: row.conversation_id,
3915
4189
  ownerId: row.owner_id ?? void 0,
3916
- tenantId: tid === DEFAULT_TENANT2 ? null : tid
4190
+ tenantId: tid === DEFAULT_TENANT2 ? null : tid,
4191
+ recurrence,
4192
+ occurrenceCount: row.occurrence_count ?? 0
3917
4193
  };
3918
4194
  }
3919
4195
  toUint8Array(value) {
@@ -4019,6 +4295,7 @@ var SqliteEngine = class extends SqlStorageEngine {
4019
4295
  };
4020
4296
  }
4021
4297
  async close() {
4298
+ this.flushEgressStats();
4022
4299
  this.db?.close();
4023
4300
  }
4024
4301
  };
@@ -4075,6 +4352,7 @@ var PostgresEngine = class extends SqlStorageEngine {
4075
4352
  this.patchVfs();
4076
4353
  }
4077
4354
  async close() {
4355
+ this.flushEgressStats();
4078
4356
  await this.sql?.end();
4079
4357
  }
4080
4358
  /** Refresh the path cache for a tenant (call before bash.exec()). */
@@ -4170,12 +4448,15 @@ function createConversationStoreFromEngine(engine) {
4170
4448
  }),
4171
4449
  listSummaries: (ownerId, tenantId) => engine.conversations.list(ownerId, tenantId),
4172
4450
  get: (conversationId) => engine.conversations.get(conversationId),
4173
- create: (ownerId, title, tenantId) => engine.conversations.create(ownerId, title, tenantId),
4451
+ getWithArchive: (conversationId) => engine.conversations.getWithArchive(conversationId),
4452
+ getStatusSnapshot: (conversationId) => engine.conversations.getStatusSnapshot(conversationId),
4453
+ create: (ownerId, title, tenantId, init) => engine.conversations.create(ownerId, title, tenantId, init),
4174
4454
  update: (conversation) => engine.conversations.update(conversation),
4175
4455
  rename: (conversationId, title) => engine.conversations.rename(conversationId, title),
4176
4456
  delete: (conversationId) => engine.conversations.delete(conversationId),
4177
4457
  appendSubagentResult: (conversationId, result) => engine.conversations.appendSubagentResult(conversationId, result),
4178
- clearCallbackLock: (conversationId) => engine.conversations.clearCallbackLock(conversationId)
4458
+ clearCallbackLock: (conversationId) => engine.conversations.clearCallbackLock(conversationId),
4459
+ listThreads: (parentConversationId) => engine.conversations.listThreads(parentConversationId)
4179
4460
  };
4180
4461
  }
4181
4462
  function createMemoryStoreFromEngine(engine, tenantId) {
@@ -4194,6 +4475,7 @@ function createReminderStoreFromEngine(engine) {
4194
4475
  return {
4195
4476
  list: () => engine.reminders.list(),
4196
4477
  create: (input) => engine.reminders.create(input),
4478
+ update: (id, fields) => engine.reminders.update(id, fields),
4197
4479
  cancel: (id) => engine.reminders.cancel(id),
4198
4480
  delete: (id) => engine.reminders.delete(id)
4199
4481
  };
@@ -4591,13 +4873,23 @@ var BashEnvironmentManager = class {
4591
4873
  this.bashOptions = toBashOptions(bashConfig, network);
4592
4874
  }
4593
4875
  environments = /* @__PURE__ */ new Map();
4876
+ filesystems = /* @__PURE__ */ new Map();
4594
4877
  workingDir;
4595
4878
  bashOptions;
4879
+ /** Return the combined IFileSystem (VFS + optional /project mount) for a tenant. */
4880
+ getFs(tenantId) {
4881
+ let fs = this.filesystems.get(tenantId);
4882
+ if (!fs) {
4883
+ const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
4884
+ fs = createBashFs(adapter, this.workingDir);
4885
+ this.filesystems.set(tenantId, fs);
4886
+ }
4887
+ return fs;
4888
+ }
4596
4889
  getOrCreate(tenantId) {
4597
4890
  let bash = this.environments.get(tenantId);
4598
4891
  if (!bash) {
4599
- const adapter = new PonchoFsAdapter(this.engine, tenantId, this.limits);
4600
- const fs = createBashFs(adapter, this.workingDir);
4892
+ const fs = this.getFs(tenantId);
4601
4893
  bash = new Bash({
4602
4894
  fs,
4603
4895
  cwd: "/",
@@ -4621,9 +4913,11 @@ var BashEnvironmentManager = class {
4621
4913
  }
4622
4914
  destroy(tenantId) {
4623
4915
  this.environments.delete(tenantId);
4916
+ this.filesystems.delete(tenantId);
4624
4917
  }
4625
4918
  destroyAll() {
4626
4919
  this.environments.clear();
4920
+ this.filesystems.clear();
4627
4921
  }
4628
4922
  };
4629
4923
 
@@ -4701,7 +4995,7 @@ var mimeFromPath = (path) => {
4701
4995
  return MIME_MAP2[path.slice(dot).toLowerCase()];
4702
4996
  };
4703
4997
  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({
4998
+ var createReadFileTool = (getFs) => defineTool3({
4705
4999
  name: "read_file",
4706
5000
  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
5001
  inputSchema: {
@@ -4721,23 +5015,24 @@ var createReadFileTool = (engine) => defineTool3({
4721
5015
  throw new Error("path is required");
4722
5016
  }
4723
5017
  const tenantId = context.tenantId ?? "__default__";
4724
- const stat3 = await engine.vfs.stat(tenantId, filePath);
4725
- if (!stat3) {
5018
+ const fs = getFs(tenantId);
5019
+ if (!await fs.exists(filePath)) {
4726
5020
  throw new Error(`File not found: ${filePath}`);
4727
5021
  }
4728
- if (stat3.type === "directory") {
5022
+ const stat3 = await fs.stat(filePath);
5023
+ if (stat3.isDirectory) {
4729
5024
  throw new Error(`${filePath} is a directory, not a file`);
4730
5025
  }
4731
- const mediaType = stat3.mimeType ?? mimeFromPath(filePath) ?? "application/octet-stream";
5026
+ const mediaType = mimeFromPath(filePath) ?? "application/octet-stream";
4732
5027
  const filename = filePath.split("/").pop() ?? filePath;
4733
5028
  if (isTextMime(mediaType)) {
4734
- const buf = await engine.vfs.readFile(tenantId, filePath);
4735
- const text = Buffer.from(buf).toString("utf8");
5029
+ const text = await fs.readFile(filePath);
4736
5030
  return { filename, mediaType, content: text };
4737
5031
  }
5032
+ const buf = await fs.readFileBuffer(filePath);
4738
5033
  return {
4739
5034
  type: "file",
4740
- data: `${VFS_SCHEME}${filePath}`,
5035
+ data: Buffer.from(buf).toString("base64"),
4741
5036
  mediaType,
4742
5037
  filename
4743
5038
  };
@@ -4746,7 +5041,7 @@ var createReadFileTool = (engine) => defineTool3({
4746
5041
 
4747
5042
  // src/vfs/edit-file-tool.ts
4748
5043
  import { defineTool as defineTool4 } from "@poncho-ai/sdk";
4749
- var createEditFileTool = (engine) => defineTool4({
5044
+ var createEditFileTool = (getFs) => defineTool4({
4750
5045
  name: "edit_file",
4751
5046
  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
5047
  inputSchema: {
@@ -4775,11 +5070,11 @@ var createEditFileTool = (engine) => defineTool4({
4775
5070
  if (!filePath) throw new Error("path is required");
4776
5071
  if (!oldStr) throw new Error("old_str must not be empty");
4777
5072
  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");
5073
+ const fs = getFs(tenantId);
5074
+ if (!await fs.exists(filePath)) throw new Error(`File not found: ${filePath}`);
5075
+ const stat3 = await fs.stat(filePath);
5076
+ if (stat3.isDirectory) throw new Error(`${filePath} is a directory`);
5077
+ const content = await fs.readFile(filePath);
4783
5078
  const first = content.indexOf(oldStr);
4784
5079
  if (first === -1) {
4785
5080
  throw new Error(
@@ -4793,14 +5088,14 @@ var createEditFileTool = (engine) => defineTool4({
4793
5088
  );
4794
5089
  }
4795
5090
  const updated = content.slice(0, first) + newStr + content.slice(first + oldStr.length);
4796
- await engine.vfs.writeFile(tenantId, filePath, new TextEncoder().encode(updated));
5091
+ await fs.writeFile(filePath, updated);
4797
5092
  return { ok: true, path: filePath };
4798
5093
  }
4799
5094
  });
4800
5095
 
4801
5096
  // src/vfs/write-file-tool.ts
4802
5097
  import { defineTool as defineTool5 } from "@poncho-ai/sdk";
4803
- var createWriteFileTool = (engine) => defineTool5({
5098
+ var createWriteFileTool = (getFs) => defineTool5({
4804
5099
  name: "write_file",
4805
5100
  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
5101
  inputSchema: {
@@ -4823,11 +5118,12 @@ var createWriteFileTool = (engine) => defineTool5({
4823
5118
  const content = typeof input.content === "string" ? input.content : "";
4824
5119
  if (!filePath) throw new Error("path is required");
4825
5120
  const tenantId = context.tenantId ?? "__default__";
5121
+ const fs = getFs(tenantId);
4826
5122
  const dir = filePath.slice(0, filePath.lastIndexOf("/"));
4827
5123
  if (dir) {
4828
- await engine.vfs.mkdir(tenantId, dir, true);
5124
+ await fs.mkdir(dir, { recursive: true });
4829
5125
  }
4830
- await engine.vfs.writeFile(tenantId, filePath, new TextEncoder().encode(content));
5126
+ await fs.writeFile(filePath, content);
4831
5127
  return { ok: true, path: filePath };
4832
5128
  }
4833
5129
  });
@@ -5370,10 +5666,11 @@ async function resolveEnv(secretsStore, tenantId, envName) {
5370
5666
  // src/reminder-tools.ts
5371
5667
  import { defineTool as defineTool8 } from "@poncho-ai/sdk";
5372
5668
  var VALID_STATUSES2 = ["pending", "cancelled"];
5669
+ var VALID_RECURRENCE_TYPES = ["daily", "weekly", "monthly", "cron"];
5373
5670
  var createReminderTools = (store) => [
5374
5671
  defineTool8({
5375
5672
  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.",
5673
+ 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
5674
  inputSchema: {
5378
5675
  type: "object",
5379
5676
  properties: {
@@ -5383,11 +5680,45 @@ var createReminderTools = (store) => [
5383
5680
  },
5384
5681
  datetime: {
5385
5682
  type: "string",
5386
- description: "ISO 8601 datetime for when the reminder should fire (e.g. '2026-03-23T09:00:00Z')"
5683
+ description: "ISO 8601 datetime for when the reminder should first fire (e.g. '2026-03-23T09:00:00Z')"
5387
5684
  },
5388
5685
  timezone: {
5389
5686
  type: "string",
5390
5687
  description: "IANA timezone for interpreting the datetime if it lacks an offset (e.g. 'America/New_York'). Defaults to UTC."
5688
+ },
5689
+ recurrence: {
5690
+ type: "object",
5691
+ description: "Optional. Set this to make the reminder repeat. Omit for a one-time reminder.",
5692
+ properties: {
5693
+ type: {
5694
+ type: "string",
5695
+ enum: VALID_RECURRENCE_TYPES,
5696
+ description: "How often to repeat: 'daily', 'weekly', 'monthly', or 'cron'."
5697
+ },
5698
+ interval: {
5699
+ type: "number",
5700
+ description: "Repeat every N units (e.g. 2 = every 2 days/weeks/months). Defaults to 1."
5701
+ },
5702
+ daysOfWeek: {
5703
+ type: "array",
5704
+ items: { type: "number" },
5705
+ description: "For weekly: which days to fire (0=Sunday, 1=Monday, ..., 6=Saturday)."
5706
+ },
5707
+ expression: {
5708
+ type: "string",
5709
+ description: "For type 'cron': a 5-field cron expression (e.g. '0 9 * * 1-5' for weekdays at 9am)."
5710
+ },
5711
+ endsAt: {
5712
+ type: "string",
5713
+ description: "ISO 8601 datetime after which the recurrence should stop."
5714
+ },
5715
+ maxOccurrences: {
5716
+ type: "number",
5717
+ description: "Maximum number of times the reminder should fire before stopping."
5718
+ }
5719
+ },
5720
+ required: ["type"],
5721
+ additionalProperties: false
5391
5722
  }
5392
5723
  },
5393
5724
  required: ["task", "datetime"],
@@ -5434,13 +5765,56 @@ var createReminderTools = (store) => [
5434
5765
  if (scheduledAt <= Date.now()) {
5435
5766
  throw new Error("Reminder datetime must be in the future");
5436
5767
  }
5768
+ let recurrence = null;
5769
+ if (input.recurrence && typeof input.recurrence === "object") {
5770
+ const rec = input.recurrence;
5771
+ const recType = rec.type;
5772
+ if (!VALID_RECURRENCE_TYPES.includes(recType)) {
5773
+ throw new Error(`Invalid recurrence type: "${recType}". Must be one of: ${VALID_RECURRENCE_TYPES.join(", ")}`);
5774
+ }
5775
+ recurrence = { type: recType };
5776
+ if (rec.interval !== void 0) {
5777
+ const interval = Number(rec.interval);
5778
+ if (!Number.isInteger(interval) || interval < 1) {
5779
+ throw new Error("recurrence.interval must be a positive integer");
5780
+ }
5781
+ recurrence.interval = interval;
5782
+ }
5783
+ if (rec.daysOfWeek !== void 0) {
5784
+ if (!Array.isArray(rec.daysOfWeek)) throw new Error("recurrence.daysOfWeek must be an array");
5785
+ const days = rec.daysOfWeek.map(Number);
5786
+ if (days.some((d) => !Number.isInteger(d) || d < 0 || d > 6)) {
5787
+ throw new Error("recurrence.daysOfWeek values must be integers 0-6");
5788
+ }
5789
+ recurrence.daysOfWeek = days;
5790
+ }
5791
+ if (rec.expression !== void 0) {
5792
+ if (typeof rec.expression !== "string") throw new Error("recurrence.expression must be a string");
5793
+ recurrence.expression = rec.expression;
5794
+ }
5795
+ if (rec.endsAt !== void 0) {
5796
+ const endsAtDate = new Date(rec.endsAt);
5797
+ if (isNaN(endsAtDate.getTime())) {
5798
+ throw new Error(`Invalid recurrence.endsAt: "${rec.endsAt}"`);
5799
+ }
5800
+ recurrence.endsAt = endsAtDate.getTime();
5801
+ }
5802
+ if (rec.maxOccurrences !== void 0) {
5803
+ const max = Number(rec.maxOccurrences);
5804
+ if (!Number.isInteger(max) || max < 1) {
5805
+ throw new Error("recurrence.maxOccurrences must be a positive integer");
5806
+ }
5807
+ recurrence.maxOccurrences = max;
5808
+ }
5809
+ }
5437
5810
  const conversationId = context.conversationId || context.runId;
5438
5811
  const reminder = await store.create({
5439
5812
  task,
5440
5813
  scheduledAt,
5441
5814
  timezone,
5442
5815
  conversationId,
5443
- tenantId: context.tenantId
5816
+ tenantId: context.tenantId,
5817
+ recurrence
5444
5818
  });
5445
5819
  return {
5446
5820
  ok: true,
@@ -5449,14 +5823,16 @@ var createReminderTools = (store) => [
5449
5823
  task: reminder.task,
5450
5824
  scheduledAt: new Date(reminder.scheduledAt).toISOString(),
5451
5825
  timezone: reminder.timezone ?? "UTC",
5452
- status: reminder.status
5826
+ status: reminder.status,
5827
+ recurrence: reminder.recurrence ?? void 0,
5828
+ occurrenceCount: reminder.occurrenceCount ?? 0
5453
5829
  }
5454
5830
  };
5455
5831
  }
5456
5832
  }),
5457
5833
  defineTool8({
5458
5834
  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.",
5835
+ 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
5836
  inputSchema: {
5461
5837
  type: "object",
5462
5838
  properties: {
@@ -5484,7 +5860,9 @@ var createReminderTools = (store) => [
5484
5860
  scheduledAt: new Date(r.scheduledAt).toISOString(),
5485
5861
  timezone: r.timezone ?? "UTC",
5486
5862
  status: r.status,
5487
- createdAt: new Date(r.createdAt).toISOString()
5863
+ createdAt: new Date(r.createdAt).toISOString(),
5864
+ recurrence: r.recurrence ?? void 0,
5865
+ occurrenceCount: r.occurrenceCount ?? 0
5488
5866
  })),
5489
5867
  count: reminders.length
5490
5868
  };
@@ -5492,7 +5870,7 @@ var createReminderTools = (store) => [
5492
5870
  }),
5493
5871
  defineTool8({
5494
5872
  name: "cancel_reminder",
5495
- description: "Cancel a pending reminder by its ID.",
5873
+ 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
5874
  inputSchema: {
5497
5875
  type: "object",
5498
5876
  properties: {
@@ -5529,6 +5907,8 @@ var createReminderTools = (store) => [
5529
5907
  ];
5530
5908
 
5531
5909
  // src/mcp.ts
5910
+ import { createLogger as createLogger3 } from "@poncho-ai/sdk";
5911
+ var mcpLog = createLogger3("mcp");
5532
5912
  var McpHttpError = class extends Error {
5533
5913
  status;
5534
5914
  constructor(status, message) {
@@ -5776,12 +6156,10 @@ var LocalMcpBridge = class {
5776
6156
  return server.name ?? server.url;
5777
6157
  }
5778
6158
  log(level, event, payload) {
5779
- const line = JSON.stringify({ event, ...payload });
5780
- if (level === "warn") {
5781
- console.warn(`[poncho][mcp] ${line}`);
5782
- return;
5783
- }
5784
- console.info(`[poncho][mcp] ${line}`);
6159
+ const summary = Object.entries(payload).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
6160
+ const msg = summary ? `${event} ${summary}` : event;
6161
+ if (level === "warn") mcpLog.warn(msg);
6162
+ else mcpLog.debug(msg);
5785
6163
  }
5786
6164
  /** Set of servers where discovery was deferred (no default token, has env resolver). */
5787
6165
  deferredDiscoveryServers = /* @__PURE__ */ new Set();
@@ -7450,6 +7828,9 @@ import { NodeTracerProvider, BatchSpanProcessor } from "@opentelemetry/sdk-trace
7450
7828
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
7451
7829
 
7452
7830
  // src/telemetry.ts
7831
+ import { createLogger as createLogger4 } from "@poncho-ai/sdk";
7832
+ var eventLog = createLogger4("event");
7833
+ var telemetryLog = createLogger4("telemetry");
7453
7834
  var MAX_FIELD_LENGTH = 200;
7454
7835
  var OMIT_FROM_LOG = /* @__PURE__ */ new Set(["continuationMessages", "_harnessMessages", "messages", "compactedHistory"]);
7455
7836
  function sanitizeEventForLog(event) {
@@ -7490,11 +7871,10 @@ var TelemetryEmitter = class {
7490
7871
  if (otlp) {
7491
7872
  await this.sendOtlp(event, otlp);
7492
7873
  }
7493
- if (event.type === "model:chunk") {
7874
+ if (event.type === "model:chunk" || event.type === "browser:frame") {
7494
7875
  return;
7495
7876
  }
7496
- process.stdout.write(`[event] ${event.type} ${sanitizeEventForLog(event)}
7497
- `);
7877
+ eventLog.debug(`${event.type} ${sanitizeEventForLog(event)}`);
7498
7878
  }
7499
7879
  async sendOtlp(event, otlp) {
7500
7880
  try {
@@ -7526,8 +7906,8 @@ var TelemetryEmitter = class {
7526
7906
  })
7527
7907
  });
7528
7908
  } catch (err) {
7529
- console.warn(
7530
- `[poncho][telemetry] OTLP log delivery failed: ${err instanceof Error ? err.message : String(err)}`
7909
+ telemetryLog.warn(
7910
+ `OTLP log delivery failed: ${err instanceof Error ? err.message : String(err)}`
7531
7911
  );
7532
7912
  }
7533
7913
  }
@@ -7621,6 +8001,11 @@ var ToolDispatcher = class {
7621
8001
  };
7622
8002
 
7623
8003
  // src/harness.ts
8004
+ var harnessLog = createLogger5("harness");
8005
+ var telemetryLog2 = createLogger5("telemetry");
8006
+ var costLog = createLogger5("cost");
8007
+ var mcpLog2 = createLogger5("mcp");
8008
+ var modelLog = createLogger5("model");
7624
8009
  function formatOtlpError(err) {
7625
8010
  if (!(err instanceof Error)) return String(err);
7626
8011
  const parts = [];
@@ -8228,8 +8613,8 @@ var AgentHarness = class _AgentHarness {
8228
8613
  const toolResultId = typeof input.toolResultId === "string" ? input.toolResultId : "";
8229
8614
  const record = archive[toolResultId];
8230
8615
  if (!record) {
8231
- console.info(
8232
- `[poncho][cost] Archived tool result lookup miss: id="${toolResultId}" conversation="${conversationId}"`
8616
+ costLog.debug(
8617
+ `archived tool result miss: id="${toolResultId}" conv=${conversationId.slice(0, 8)}`
8233
8618
  );
8234
8619
  return {
8235
8620
  error: `No archived tool result found for id "${toolResultId}" in this conversation.`
@@ -8239,8 +8624,8 @@ var AgentHarness = class _AgentHarness {
8239
8624
  const limit = Math.min(Math.max(Number(input.limit) || 6e3, 1), 2e4);
8240
8625
  const end = Math.min(record.payload.length, offset + limit);
8241
8626
  const chunk = record.payload.slice(offset, end);
8242
- console.info(
8243
- `[poncho][cost] Archived tool result lookup hit: id="${toolResultId}" conversation="${conversationId}" offset=${offset} returned=${chunk.length} total=${record.payload.length}`
8627
+ costLog.debug(
8628
+ `archived tool result hit: id="${toolResultId}" conv=${conversationId.slice(0, 8)} offset=${offset} returned=${chunk.length} total=${record.payload.length}`
8244
8629
  );
8245
8630
  return {
8246
8631
  toolResultId: record.toolResultId,
@@ -8612,9 +8997,7 @@ var AgentHarness = class _AgentHarness {
8612
8997
  this.dispatcher.unregisterMany(this.registeredMcpToolNames);
8613
8998
  this.registeredMcpToolNames.clear();
8614
8999
  if (requestedPatterns.length === 0) {
8615
- console.info(
8616
- `[poncho][mcp] ${JSON.stringify({ event: "tools.cleared", reason, requestedPatterns })}`
8617
- );
9000
+ mcpLog2.debug(`tools cleared (reason=${reason})`);
8618
9001
  return;
8619
9002
  }
8620
9003
  const tools = await this.mcpBridge.loadTools(requestedPatterns);
@@ -8622,14 +9005,8 @@ var AgentHarness = class _AgentHarness {
8622
9005
  for (const tool of tools) {
8623
9006
  this.registeredMcpToolNames.add(tool.name);
8624
9007
  }
8625
- console.info(
8626
- `[poncho][mcp] ${JSON.stringify({
8627
- event: "tools.refreshed",
8628
- reason,
8629
- requestedPatterns,
8630
- registeredCount: tools.length,
8631
- activeSkills: this.listActiveSkills()
8632
- })}`
9008
+ mcpLog2.debug(
9009
+ `tools refreshed (reason=${reason}, registered=${tools.length}, patterns=${requestedPatterns.length})`
8633
9010
  );
8634
9011
  }
8635
9012
  buildSkillFingerprint(skills) {
@@ -8692,9 +9069,7 @@ var AgentHarness = class _AgentHarness {
8692
9069
  this.agentFileFingerprint = rawContent;
8693
9070
  return true;
8694
9071
  } catch (error) {
8695
- console.warn(
8696
- `[poncho][agent] Failed to refresh AGENT.md in development mode: ${error instanceof Error ? error.message : String(error)}`
8697
- );
9072
+ createLogger5("agent").warn(`failed to refresh AGENT.md in dev: ${fmtErr(error)}`);
8698
9073
  return false;
8699
9074
  }
8700
9075
  }
@@ -8742,9 +9117,7 @@ var AgentHarness = class _AgentHarness {
8742
9117
  await this.refreshMcpTools("skills:changed");
8743
9118
  return true;
8744
9119
  } catch (error) {
8745
- console.warn(
8746
- `[poncho][skills] Failed to refresh skills in development mode: ${error instanceof Error ? error.message : String(error)}`
8747
- );
9120
+ createLogger5("skills").warn(`failed to refresh skills in dev: ${fmtErr(error)}`);
8748
9121
  return false;
8749
9122
  }
8750
9123
  }
@@ -8794,9 +9167,10 @@ var AgentHarness = class _AgentHarness {
8794
9167
  config?.network
8795
9168
  );
8796
9169
  this.registerIfMissing(createBashTool(this.bashManager));
8797
- this.registerIfMissing(createReadFileTool(engine));
8798
- this.registerIfMissing(createEditFileTool(engine));
8799
- this.registerIfMissing(createWriteFileTool(engine));
9170
+ const getFs = (tenantId) => this.bashManager.getFs(tenantId);
9171
+ this.registerIfMissing(createReadFileTool(getFs));
9172
+ this.registerIfMissing(createEditFileTool(getFs));
9173
+ this.registerIfMissing(createWriteFileTool(getFs));
8800
9174
  if (config?.isolate) {
8801
9175
  const { createRunCodeTool, buildRunCodeDescription, bundleLibraries } = await import("./isolate-TCWTUVG4.js");
8802
9176
  let libraryPreamble = null;
@@ -8838,9 +9212,7 @@ var AgentHarness = class _AgentHarness {
8838
9212
  }
8839
9213
  if (config?.browser) {
8840
9214
  await this.initBrowserTools(config).catch((e) => {
8841
- console.warn(
8842
- `[poncho][browser] Failed to load browser tools: ${e instanceof Error ? e.message : String(e)}`
8843
- );
9215
+ createLogger5("browser").warn(`failed to load browser tools: ${fmtErr(e)}`);
8844
9216
  });
8845
9217
  }
8846
9218
  const stateConfig = resolveStateConfig(config);
@@ -8871,7 +9243,7 @@ var AgentHarness = class _AgentHarness {
8871
9243
  provider2.register();
8872
9244
  this.otlpTracerProvider = provider2;
8873
9245
  this.hasOtlpExporter = true;
8874
- console.info(`[poncho][telemetry] OTLP trace exporter active \u2192 ${otlpConfig.url}`);
9246
+ telemetryLog2.item(`OTLP trace exporter active \u2192 ${urlColor(otlpConfig.url)}`);
8875
9247
  }
8876
9248
  }
8877
9249
  async buildBrowserStoragePersistence(config, sessionId) {
@@ -8986,13 +9358,13 @@ var AgentHarness = class _AgentHarness {
8986
9358
  await this.mcpBridge?.stopLocalServers();
8987
9359
  if (this.otlpSpanProcessor) {
8988
9360
  await this.otlpSpanProcessor.shutdown().catch((err) => {
8989
- console.warn(`[poncho][telemetry] OTLP span processor shutdown error: ${formatOtlpError(err)}`);
9361
+ telemetryLog2.warn(`OTLP span processor shutdown error: ${formatOtlpError(err)}`);
8990
9362
  });
8991
9363
  this.otlpSpanProcessor = void 0;
8992
9364
  }
8993
9365
  if (this.otlpTracerProvider) {
8994
9366
  await this.otlpTracerProvider.shutdown().catch((err) => {
8995
- console.warn(`[poncho][telemetry] OTLP tracer provider shutdown error: ${formatOtlpError(err)}`);
9367
+ telemetryLog2.warn(`OTLP tracer provider shutdown error: ${formatOtlpError(err)}`);
8996
9368
  });
8997
9369
  this.otlpTracerProvider = void 0;
8998
9370
  }
@@ -9044,7 +9416,7 @@ var AgentHarness = class _AgentHarness {
9044
9416
  await this.otlpSpanProcessor?.forceFlush();
9045
9417
  } catch (err) {
9046
9418
  const detail = formatOtlpError(err);
9047
- console.warn(`[poncho][telemetry] OTLP span flush failed: ${detail}`);
9419
+ telemetryLog2.warn(`OTLP span flush failed: ${detail}`);
9048
9420
  }
9049
9421
  }
9050
9422
  } else {
@@ -9090,19 +9462,15 @@ var AgentHarness = class _AgentHarness {
9090
9462
  this.seedToolResultArchive(conversationId, input.parameters);
9091
9463
  const truncationSummary = this.truncateHistoricalToolResults(messages, conversationId);
9092
9464
  if (truncationSummary.changed) {
9093
- console.info(
9094
- `[poncho][cost] Truncated ${truncationSummary.truncatedCount} historical tool result(s) (archived_new=${truncationSummary.archivedCount}, omitted_chars=${truncationSummary.omittedChars}) for conversation="${conversationId}"`
9465
+ costLog.debug(
9466
+ `truncated ${truncationSummary.truncatedCount} historical tool result(s) (archived=${truncationSummary.archivedCount}, omitted=${truncationSummary.omittedChars} chars) conv=${conversationId.slice(0, 8)}`
9095
9467
  );
9096
9468
  }
9097
9469
  const hasFullToolResults = hasUntruncatedToolResults(messages);
9098
9470
  if (hasFullToolResults) {
9099
- console.info(
9100
- `[poncho][cost] Prompt cache breakpoint will be placed before untruncated tool results for run "${runId}" (stable prefix only).`
9101
- );
9471
+ costLog.debug(`cache breakpoint before untruncated tool results (run=${runId.slice(0, 12)})`);
9102
9472
  } else {
9103
- console.info(
9104
- `[poncho][cost] Prompt cache breakpoint will be placed at history tail for run "${runId}" (no untruncated tool results).`
9105
- );
9473
+ costLog.debug(`cache breakpoint at history tail (run=${runId.slice(0, 12)})`);
9106
9474
  }
9107
9475
  const inputMessageCount = messages.length;
9108
9476
  const events = [];
@@ -9590,7 +9958,7 @@ ${textContent}` };
9590
9958
  };
9591
9959
  const modelName = agent.frontmatter.model?.name ?? "claude-opus-4-5";
9592
9960
  if (step === 1) {
9593
- console.info(`[poncho] model="${modelName}" provider="${agent.frontmatter.model?.provider ?? "anthropic"}"`);
9961
+ modelLog.item(`${modelName} (provider=${agent.frontmatter.model?.provider ?? "anthropic"})`);
9594
9962
  }
9595
9963
  const modelInstance = this.modelProvider(modelName);
9596
9964
  const compactionConfig = resolveCompactionConfig(agent.frontmatter.compaction);
@@ -9688,8 +10056,8 @@ ${textContent}` };
9688
10056
  message: `Model "${modelName}" did not respond within the ${Math.floor(timeoutMs / 1e3)}s run timeout.`
9689
10057
  }
9690
10058
  });
9691
- console.error(
9692
- `[poncho][harness] Stream timeout: model="${modelName}", step=${step}, elapsed=${now() - start}ms`
10059
+ harnessLog.error(
10060
+ `stream timeout: model=${modelName} step=${step} elapsed=${now() - start}ms`
9693
10061
  );
9694
10062
  return;
9695
10063
  }
@@ -9722,8 +10090,8 @@ ${textContent}` };
9722
10090
  break;
9723
10091
  }
9724
10092
  const isFirstChunk = chunkCount === 0;
9725
- console.error(
9726
- `[poncho][harness] Stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model="${modelName}", step=${step}, chunks=${chunkCount}, elapsed=${now() - start}ms`
10093
+ harnessLog.error(
10094
+ `stream timeout waiting for ${isFirstChunk ? "first" : "next"} chunk: model=${modelName} step=${step} chunks=${chunkCount} elapsed=${now() - start}ms`
9727
10095
  );
9728
10096
  if (isFirstChunk) {
9729
10097
  throw new FirstChunkTimeoutError(modelName, FIRST_CHUNK_TIMEOUT_MS);
@@ -9778,7 +10146,7 @@ ${textContent}` };
9778
10146
  contextTokens: latestContextTokens + toolOutputEstimateSinceModel,
9779
10147
  contextWindow
9780
10148
  };
9781
- console.info(`[poncho][harness] Soft deadline fired mid-stream at step ${step} (${(now() - start).toFixed(0)}ms). Checkpointing with ${fullText.length} chars of partial text.`);
10149
+ harnessLog.info(`soft deadline fired mid-stream at step ${step} (${(now() - start).toFixed(0)}ms); checkpointing with ${fullText.length} chars of partial text`);
9782
10150
  yield pushEvent({ type: "run:completed", runId, result: result_ });
9783
10151
  return;
9784
10152
  }
@@ -9824,9 +10192,7 @@ ${textContent}` };
9824
10192
  message: `Model "${modelName}" returned an error. This may indicate the model is not supported by the current provider SDK version, or the API returned an error response.`
9825
10193
  }
9826
10194
  });
9827
- console.error(
9828
- `[poncho][harness] Model error: finishReason="error", model="${modelName}", step=${step}`
9829
- );
10195
+ harnessLog.error(`model error: finishReason="error" model=${modelName} step=${step}`);
9830
10196
  return;
9831
10197
  }
9832
10198
  if (finishReason === "content-filter") {
@@ -9862,8 +10228,8 @@ ${textContent}` };
9862
10228
  cacheWrite: stepCacheWriteTokens
9863
10229
  }
9864
10230
  });
9865
- console.info(
9866
- `[poncho][cost] model="${modelName}" step=${step} input=${stepInputTokens} output=${usage.outputTokens ?? 0} cached=${stepCachedTokens} cacheWrite=${stepCacheWriteTokens} totals(input=${totalInputTokens}, output=${totalOutputTokens}, cached=${totalCachedTokens}, cacheWrite=${totalCacheWriteTokens})`
10231
+ costLog.debug(
10232
+ `step=${step} in=${stepInputTokens} out=${usage.outputTokens ?? 0} cached=${stepCachedTokens} cw=${stepCacheWriteTokens} totals(in=${totalInputTokens} out=${totalOutputTokens} cached=${totalCachedTokens} cw=${totalCacheWriteTokens})`
9867
10233
  );
9868
10234
  const toolCalls = toolCallsResult.map((tc) => ({
9869
10235
  id: tc.toolCallId,
@@ -9882,14 +10248,10 @@ ${textContent}` };
9882
10248
  message: `Model "${modelName}" returned no content (finish reason: ${finishReason}). The model may not be supported by the current provider SDK version.`
9883
10249
  }
9884
10250
  });
9885
- console.error(
9886
- `[poncho][harness] Empty response: finishReason="${finishReason}", model="${modelName}", step=${step}`
9887
- );
10251
+ harnessLog.error(`empty response: finishReason="${finishReason}" model=${modelName} step=${step}`);
9888
10252
  return;
9889
10253
  }
9890
- console.warn(
9891
- `[poncho][harness] Model "${modelName}" returned an empty response with finishReason="stop" on step ${step}.`
9892
- );
10254
+ harnessLog.warn(`model "${modelName}" returned empty response with finishReason="stop" on step ${step}`);
9893
10255
  }
9894
10256
  if (fullText.length > 0) {
9895
10257
  messages.push({
@@ -10255,8 +10617,8 @@ ${this.skillFingerprint}`;
10255
10617
  if (isRetryableModelError(error) && transientStepRetryCount < MAX_TRANSIENT_STEP_RETRIES) {
10256
10618
  transientStepRetryCount += 1;
10257
10619
  const statusCode = getErrorStatusCode(error);
10258
- console.warn(
10259
- `[poncho][harness] Retrying step ${step} after transient model error (attempt ${transientStepRetryCount}/${MAX_TRANSIENT_STEP_RETRIES})${typeof statusCode === "number" ? ` status=${statusCode}` : ""}: ${error instanceof Error ? error.message : String(error)}`
10620
+ harnessLog.warn(
10621
+ `retrying step ${step} after transient model error (attempt ${transientStepRetryCount}/${MAX_TRANSIENT_STEP_RETRIES})${typeof statusCode === "number" ? ` status=${statusCode}` : ""}: ${fmtErr(error)}`
10260
10622
  );
10261
10623
  step -= 1;
10262
10624
  continue;
@@ -10267,7 +10629,7 @@ ${this.skillFingerprint}`;
10267
10629
  runId,
10268
10630
  error: runError
10269
10631
  });
10270
- console.error(`[poncho][harness] Step ${step} error:`, error);
10632
+ harnessLog.error(`step ${step} error: ${fmtErr(error)}`);
10271
10633
  return;
10272
10634
  }
10273
10635
  }
@@ -10431,12 +10793,22 @@ var InMemoryReminderStore = class {
10431
10793
  createdAt: Date.now(),
10432
10794
  conversationId: input.conversationId,
10433
10795
  ownerId: input.ownerId,
10434
- tenantId: input.tenantId
10796
+ tenantId: input.tenantId,
10797
+ recurrence: input.recurrence ?? null,
10798
+ occurrenceCount: 0
10435
10799
  };
10436
10800
  this.reminders = pruneStale(this.reminders);
10437
10801
  this.reminders.push(reminder);
10438
10802
  return reminder;
10439
10803
  }
10804
+ async update(id, fields) {
10805
+ const reminder = this.reminders.find((r) => r.id === id);
10806
+ if (!reminder) throw new Error(`Reminder "${id}" not found`);
10807
+ if (fields.scheduledAt !== void 0) reminder.scheduledAt = fields.scheduledAt;
10808
+ if (fields.occurrenceCount !== void 0) reminder.occurrenceCount = fields.occurrenceCount;
10809
+ if (fields.status !== void 0) reminder.status = fields.status;
10810
+ return reminder;
10811
+ }
10440
10812
  async cancel(id) {
10441
10813
  const reminder = this.reminders.find((r) => r.id === id);
10442
10814
  if (!reminder) throw new Error(`Reminder "${id}" not found`);
@@ -10450,6 +10822,101 @@ var InMemoryReminderStore = class {
10450
10822
  this.reminders = this.reminders.filter((r) => r.id !== id);
10451
10823
  }
10452
10824
  };
10825
+ var computeNextOccurrence = (reminder) => {
10826
+ const rec = reminder.recurrence;
10827
+ if (!rec) return null;
10828
+ const fired = (reminder.occurrenceCount ?? 0) + 1;
10829
+ if (rec.maxOccurrences && fired >= rec.maxOccurrences) return null;
10830
+ const interval = rec.interval ?? 1;
10831
+ const prev = reminder.scheduledAt;
10832
+ let next;
10833
+ switch (rec.type) {
10834
+ case "daily": {
10835
+ next = prev + interval * 24 * 60 * 60 * 1e3;
10836
+ break;
10837
+ }
10838
+ case "weekly": {
10839
+ if (rec.daysOfWeek && rec.daysOfWeek.length > 0) {
10840
+ const d = new Date(prev);
10841
+ const days = [...rec.daysOfWeek].sort((a, b) => a - b);
10842
+ const currentDay = d.getUTCDay();
10843
+ let nextDay = days.find((day) => day > currentDay);
10844
+ if (nextDay !== void 0) {
10845
+ const delta = nextDay - currentDay;
10846
+ next = prev + delta * 24 * 60 * 60 * 1e3;
10847
+ } else {
10848
+ const delta = 7 * interval - currentDay + days[0];
10849
+ next = prev + delta * 24 * 60 * 60 * 1e3;
10850
+ }
10851
+ } else {
10852
+ next = prev + interval * 7 * 24 * 60 * 60 * 1e3;
10853
+ }
10854
+ break;
10855
+ }
10856
+ case "monthly": {
10857
+ const d = new Date(prev);
10858
+ d.setUTCMonth(d.getUTCMonth() + interval);
10859
+ next = d.getTime();
10860
+ break;
10861
+ }
10862
+ case "cron": {
10863
+ if (!rec.expression) return null;
10864
+ const parsed = parseCronExpression(rec.expression);
10865
+ if (!parsed) return null;
10866
+ next = nextCronOccurrence(prev, parsed);
10867
+ if (next <= prev) return null;
10868
+ break;
10869
+ }
10870
+ default:
10871
+ return null;
10872
+ }
10873
+ if (rec.endsAt && next > rec.endsAt) return null;
10874
+ return next;
10875
+ };
10876
+ var expandField = (field, min, max) => {
10877
+ const values = /* @__PURE__ */ new Set();
10878
+ for (const part of field.split(",")) {
10879
+ const stepMatch = part.match(/^(.+)\/(\d+)$/);
10880
+ const step = stepMatch ? parseInt(stepMatch[2], 10) : 1;
10881
+ const range = stepMatch ? stepMatch[1] : part;
10882
+ if (range === "*") {
10883
+ for (let i = min; i <= max; i += step) values.add(i);
10884
+ } else if (range.includes("-")) {
10885
+ const [lo, hi] = range.split("-").map(Number);
10886
+ if (isNaN(lo) || isNaN(hi)) return null;
10887
+ for (let i = lo; i <= hi; i += step) values.add(i);
10888
+ } else {
10889
+ const n = parseInt(range, 10);
10890
+ if (isNaN(n)) return null;
10891
+ values.add(n);
10892
+ }
10893
+ }
10894
+ return values;
10895
+ };
10896
+ var parseCronExpression = (expr) => {
10897
+ const parts = expr.trim().split(/\s+/);
10898
+ if (parts.length !== 5) return null;
10899
+ const minutes = expandField(parts[0], 0, 59);
10900
+ const hours = expandField(parts[1], 0, 23);
10901
+ const daysOfMonth = expandField(parts[2], 1, 31);
10902
+ const months = expandField(parts[3], 1, 12);
10903
+ const daysOfWeek = expandField(parts[4], 0, 6);
10904
+ if (!minutes || !hours || !daysOfMonth || !months || !daysOfWeek) return null;
10905
+ return { minutes, hours, daysOfMonth, months, daysOfWeek };
10906
+ };
10907
+ var nextCronOccurrence = (afterMs, fields) => {
10908
+ const d = new Date(afterMs);
10909
+ d.setUTCSeconds(0, 0);
10910
+ d.setUTCMinutes(d.getUTCMinutes() + 1);
10911
+ const limit = afterMs + 366 * 24 * 60 * 60 * 1e3;
10912
+ while (d.getTime() < limit) {
10913
+ 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())) {
10914
+ return d.getTime();
10915
+ }
10916
+ d.setUTCMinutes(d.getUTCMinutes() + 1);
10917
+ }
10918
+ return afterMs;
10919
+ };
10453
10920
  var createReminderStore = (_agentId, _config, _options) => {
10454
10921
  return new InMemoryReminderStore();
10455
10922
  };
@@ -10514,6 +10981,7 @@ var InMemoryConversationStore = class {
10514
10981
  ownerId: c.ownerId,
10515
10982
  tenantId: c.tenantId,
10516
10983
  parentConversationId: c.parentConversationId,
10984
+ parentMessageId: c.parentMessageId,
10517
10985
  messageCount: c.messages.length,
10518
10986
  hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
10519
10987
  channelMeta: c.channelMeta
@@ -10523,16 +10991,41 @@ var InMemoryConversationStore = class {
10523
10991
  this.purgeExpired();
10524
10992
  return this.conversations.get(conversationId);
10525
10993
  }
10526
- async create(ownerId = DEFAULT_OWNER3, title, tenantId = null) {
10994
+ // In-memory stores already hold the full conversation object, so there's
10995
+ // no separate archive blob to load. Both variants return the same data.
10996
+ async getWithArchive(conversationId) {
10997
+ return this.get(conversationId);
10998
+ }
10999
+ async getStatusSnapshot(conversationId) {
11000
+ const c = await this.get(conversationId);
11001
+ if (!c) return void 0;
11002
+ return {
11003
+ conversationId: c.conversationId,
11004
+ updatedAt: c.updatedAt,
11005
+ messageCount: c.messages.length,
11006
+ hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
11007
+ hasContinuationMessages: Array.isArray(c._continuationMessages) && c._continuationMessages.length > 0,
11008
+ parentConversationId: c.parentConversationId ?? null,
11009
+ ownerId: c.ownerId,
11010
+ tenantId: c.tenantId,
11011
+ runStatus: c.runStatus ?? null
11012
+ };
11013
+ }
11014
+ async create(ownerId = DEFAULT_OWNER3, title, tenantId = null, init) {
10527
11015
  const now2 = Date.now();
10528
11016
  const conversation = {
10529
11017
  conversationId: globalThis.crypto?.randomUUID?.() ?? `${now2}-${Math.random()}`,
10530
11018
  title: normalizeTitle3(title),
10531
- messages: [],
11019
+ messages: init?.messages ?? [],
10532
11020
  ownerId,
10533
11021
  tenantId,
10534
11022
  createdAt: now2,
10535
- updatedAt: now2
11023
+ updatedAt: now2,
11024
+ ...init?.parentConversationId !== void 0 ? { parentConversationId: init.parentConversationId } : {},
11025
+ ...init?.parentMessageId !== void 0 ? { parentMessageId: init.parentMessageId } : {},
11026
+ ...init?.threadMeta !== void 0 ? { threadMeta: init.threadMeta } : {},
11027
+ ...init?.subagentMeta !== void 0 ? { subagentMeta: init.subagentMeta } : {},
11028
+ ...init?.channelMeta !== void 0 ? { channelMeta: init.channelMeta } : {}
10536
11029
  };
10537
11030
  this.conversations.set(conversation.conversationId, conversation);
10538
11031
  return conversation;
@@ -10571,6 +11064,24 @@ var InMemoryConversationStore = class {
10571
11064
  conversation.updatedAt = Date.now();
10572
11065
  return conversation;
10573
11066
  }
11067
+ async listThreads(parentConversationId) {
11068
+ this.purgeExpired();
11069
+ return Array.from(this.conversations.values()).filter(
11070
+ (c) => c.parentConversationId === parentConversationId && typeof c.parentMessageId === "string"
11071
+ ).sort((a, b) => b.updatedAt - a.updatedAt).map((c) => ({
11072
+ conversationId: c.conversationId,
11073
+ title: c.title,
11074
+ updatedAt: c.updatedAt,
11075
+ createdAt: c.createdAt,
11076
+ ownerId: c.ownerId,
11077
+ tenantId: c.tenantId,
11078
+ parentConversationId: c.parentConversationId,
11079
+ parentMessageId: c.parentMessageId,
11080
+ messageCount: c.messages.length,
11081
+ hasPendingApprovals: Array.isArray(c.pendingApprovals) && c.pendingApprovals.length > 0,
11082
+ channelMeta: c.channelMeta
11083
+ }));
11084
+ }
10574
11085
  };
10575
11086
  var createStateStore = (config, _options) => {
10576
11087
  const ttl = config?.ttl;
@@ -10607,31 +11118,1608 @@ function extractMetadata(payload) {
10607
11118
  return void 0;
10608
11119
  }
10609
11120
 
11121
+ // src/orchestrator/history.ts
11122
+ var isMessageArray = (value) => Array.isArray(value) && value.every((entry) => {
11123
+ if (!entry || typeof entry !== "object") return false;
11124
+ const row = entry;
11125
+ const role = row.role;
11126
+ const content = row.content;
11127
+ const roleOk = role === "system" || role === "user" || role === "assistant" || role === "tool";
11128
+ const contentOk = typeof content === "string" || Array.isArray(content);
11129
+ return roleOk && contentOk;
11130
+ });
11131
+ var loadCanonicalHistory = (conversation) => {
11132
+ if (isMessageArray(conversation._harnessMessages) && conversation._harnessMessages.length > 0) {
11133
+ return { messages: [...conversation._harnessMessages], source: "harness" };
11134
+ }
11135
+ return { messages: [...conversation.messages], source: "messages" };
11136
+ };
11137
+ var loadRunHistory = (conversation, options) => {
11138
+ if (options?.preferContinuation && isMessageArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0) {
11139
+ return {
11140
+ messages: [...conversation._continuationMessages],
11141
+ source: "continuation",
11142
+ shouldRebuildCanonical: !isMessageArray(conversation._harnessMessages) || conversation._harnessMessages.length === 0
11143
+ };
11144
+ }
11145
+ const canonical = loadCanonicalHistory(conversation);
11146
+ return {
11147
+ ...canonical,
11148
+ shouldRebuildCanonical: canonical.source !== "harness"
11149
+ };
11150
+ };
11151
+ var resolveRunRequest = (conversation, request) => {
11152
+ const resolved = loadRunHistory(conversation, {
11153
+ preferContinuation: request.preferContinuation
11154
+ });
11155
+ return {
11156
+ source: resolved.source,
11157
+ shouldRebuildCanonical: resolved.shouldRebuildCanonical,
11158
+ messages: resolved.messages.length > 0 ? resolved.messages : request.messages
11159
+ };
11160
+ };
11161
+
11162
+ // src/orchestrator/turn.ts
11163
+ var createTurnDraftState = () => ({
11164
+ assistantResponse: "",
11165
+ toolTimeline: [],
11166
+ sections: [],
11167
+ currentTools: [],
11168
+ currentText: ""
11169
+ });
11170
+ var cloneSections = (sections) => sections.map((section) => ({
11171
+ type: section.type,
11172
+ content: Array.isArray(section.content) ? [...section.content] : section.content
11173
+ }));
11174
+ var flushTurnDraft = (draft) => {
11175
+ if (draft.currentTools.length > 0) {
11176
+ draft.sections.push({ type: "tools", content: draft.currentTools });
11177
+ draft.currentTools = [];
11178
+ }
11179
+ if (draft.currentText.length > 0) {
11180
+ draft.sections.push({ type: "text", content: draft.currentText });
11181
+ draft.currentText = "";
11182
+ }
11183
+ };
11184
+ var buildToolCompletedText = (event) => {
11185
+ const input = event.input;
11186
+ const output = event.output;
11187
+ const meta = [`${event.duration}ms`];
11188
+ let detail = "";
11189
+ if (event.tool === "bash" && input && typeof input.command === "string") {
11190
+ detail = input.command;
11191
+ } else if (event.tool === "web_search") {
11192
+ const q = input?.query || output?.query || "";
11193
+ if (q) detail = `"${q.length > 60 ? q.slice(0, 57) + "..." : q}"`;
11194
+ } else if (event.tool === "web_fetch") {
11195
+ const u = input?.url || output?.url || "";
11196
+ if (u) detail = u;
11197
+ } else if (event.tool === "spawn_subagent") {
11198
+ if (input && typeof input.task === "string") detail = input.task;
11199
+ } else if (input) {
11200
+ for (const [, v] of Object.entries(input)) {
11201
+ if (typeof v === "string" && v.length > 0) {
11202
+ detail = v.length > 80 ? v.slice(0, 77) + "..." : v;
11203
+ break;
11204
+ }
11205
+ }
11206
+ }
11207
+ if (detail) {
11208
+ detail = detail.replace(/\n/g, " ");
11209
+ meta.push(detail);
11210
+ }
11211
+ let text = `- done \`${event.tool}\`` + (meta.length > 0 ? ` (${meta.join(", ")})` : "");
11212
+ if (event.tool === "spawn_subagent" && output?.subagentId) {
11213
+ text += ` [subagent:${output.subagentId}]`;
11214
+ }
11215
+ if (event.tool === "bash" && typeof output?.exitCode === "number" && output.exitCode !== 0) {
11216
+ text += ` \u2014 exit ${output.exitCode}`;
11217
+ }
11218
+ if (event.tool === "web_search" && Array.isArray(output?.results)) {
11219
+ text += ` \u2014 ${output.results.length} result${output.results.length !== 1 ? "s" : ""}`;
11220
+ }
11221
+ return text;
11222
+ };
11223
+ var recordStandardTurnEvent = (draft, event) => {
11224
+ if (event.type === "model:chunk") {
11225
+ if (draft.currentTools.length > 0) {
11226
+ draft.sections.push({ type: "tools", content: draft.currentTools });
11227
+ draft.currentTools = [];
11228
+ if (draft.assistantResponse.length > 0 && !/\s$/.test(draft.assistantResponse)) {
11229
+ draft.assistantResponse += " ";
11230
+ }
11231
+ }
11232
+ draft.assistantResponse += event.content;
11233
+ draft.currentText += event.content;
11234
+ return;
11235
+ }
11236
+ if (event.type === "tool:started") {
11237
+ if (draft.currentText.length > 0) {
11238
+ draft.sections.push({ type: "text", content: draft.currentText });
11239
+ draft.currentText = "";
11240
+ }
11241
+ const input = event.input;
11242
+ let startDetail = "";
11243
+ if (event.tool === "bash" && input && typeof input.command === "string") {
11244
+ startDetail = input.command;
11245
+ } else if (event.tool === "web_fetch" && input && typeof input.url === "string") {
11246
+ startDetail = input.url;
11247
+ } else if (event.tool === "web_search" && input && typeof input.query === "string") {
11248
+ startDetail = `"${input.query}"`;
11249
+ } else if (input) {
11250
+ for (const [, v] of Object.entries(input)) {
11251
+ if (typeof v === "string" && v.length > 0) {
11252
+ startDetail = v.length > 80 ? v.slice(0, 77) + "..." : v;
11253
+ break;
11254
+ }
11255
+ }
11256
+ }
11257
+ if (startDetail) startDetail = startDetail.replace(/\n/g, " ");
11258
+ const toolText = `- start \`${event.tool}\`` + (startDetail ? ` (${startDetail})` : "");
11259
+ draft.toolTimeline.push(toolText);
11260
+ draft.currentTools.push(toolText);
11261
+ return;
11262
+ }
11263
+ if (event.type === "tool:completed") {
11264
+ const toolText = buildToolCompletedText(event);
11265
+ draft.toolTimeline.push(toolText);
11266
+ draft.currentTools.push(toolText);
11267
+ return;
11268
+ }
11269
+ if (event.type === "tool:error") {
11270
+ const toolText = `- error \`${event.tool}\`: ${event.error}`;
11271
+ draft.toolTimeline.push(toolText);
11272
+ draft.currentTools.push(toolText);
11273
+ }
11274
+ };
11275
+ var buildAssistantMetadata = (draft, sectionsOverride, opts) => {
11276
+ const sections = sectionsOverride ?? cloneSections(draft.sections);
11277
+ const hasContent = draft.toolTimeline.length > 0 || sections.length > 0;
11278
+ if (!hasContent && !opts?.id) return void 0;
11279
+ const meta = {};
11280
+ if (opts?.id) meta.id = opts.id;
11281
+ if (opts?.timestamp) meta.timestamp = opts.timestamp;
11282
+ if (draft.toolTimeline.length > 0) meta.toolActivity = [...draft.toolTimeline];
11283
+ if (sections.length > 0) meta.sections = sections;
11284
+ return meta;
11285
+ };
11286
+ var executeConversationTurn = async ({
11287
+ harness,
11288
+ runInput,
11289
+ events,
11290
+ initialContextTokens = 0,
11291
+ initialContextWindow = 0,
11292
+ onEvent
11293
+ }) => {
11294
+ const draft = createTurnDraftState();
11295
+ let latestRunId = "";
11296
+ let runCancelled = false;
11297
+ let runContinuation = false;
11298
+ let runContinuationMessages;
11299
+ let runHarnessMessages;
11300
+ let runContextTokens = initialContextTokens;
11301
+ let runContextWindow = initialContextWindow;
11302
+ let runSteps = 0;
11303
+ let runMaxSteps;
11304
+ const source = events ?? harness.runWithTelemetry(runInput);
11305
+ for await (const event of source) {
11306
+ recordStandardTurnEvent(draft, event);
11307
+ if (event.type === "run:started") {
11308
+ latestRunId = event.runId;
11309
+ }
11310
+ if (event.type === "run:cancelled") {
11311
+ runCancelled = true;
11312
+ }
11313
+ if (event.type === "run:completed") {
11314
+ runContinuation = event.result.continuation === true;
11315
+ runContinuationMessages = event.result.continuationMessages;
11316
+ runHarnessMessages = event.result.continuationMessages;
11317
+ runContextTokens = event.result.contextTokens ?? runContextTokens;
11318
+ runContextWindow = event.result.contextWindow ?? runContextWindow;
11319
+ runSteps = event.result.steps;
11320
+ if (typeof event.result.maxSteps === "number") {
11321
+ runMaxSteps = event.result.maxSteps;
11322
+ }
11323
+ if (draft.assistantResponse.length === 0 && event.result.response) {
11324
+ draft.assistantResponse = event.result.response;
11325
+ }
11326
+ }
11327
+ if (event.type === "run:error") {
11328
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
11329
+ }
11330
+ if (onEvent) {
11331
+ await onEvent(event, draft);
11332
+ }
11333
+ }
11334
+ return {
11335
+ latestRunId,
11336
+ runCancelled,
11337
+ runContinuation,
11338
+ runContinuationMessages,
11339
+ runHarnessMessages,
11340
+ runContextTokens,
11341
+ runContextWindow,
11342
+ runSteps,
11343
+ runMaxSteps,
11344
+ draft
11345
+ };
11346
+ };
11347
+ var normalizePendingToolCalls = (value) => {
11348
+ if (!Array.isArray(value)) return [];
11349
+ return value.filter((entry) => {
11350
+ if (!entry || typeof entry !== "object") return false;
11351
+ const row = entry;
11352
+ return typeof row.id === "string" && typeof row.name === "string" && typeof row.input === "object" && row.input !== null;
11353
+ }).map((entry) => ({ id: entry.id, name: entry.name, input: entry.input }));
11354
+ };
11355
+ var normalizeApprovalCheckpoint = (approval, fallbackMessages) => ({
11356
+ ...approval,
11357
+ checkpointMessages: isMessageArray(approval.checkpointMessages) ? approval.checkpointMessages : [...fallbackMessages],
11358
+ baseMessageCount: typeof approval.baseMessageCount === "number" && approval.baseMessageCount >= 0 ? approval.baseMessageCount : 0,
11359
+ pendingToolCalls: normalizePendingToolCalls(approval.pendingToolCalls)
11360
+ });
11361
+ var buildApprovalCheckpoints = ({
11362
+ approvals,
11363
+ runId,
11364
+ checkpointMessages,
11365
+ baseMessageCount,
11366
+ pendingToolCalls
11367
+ }) => approvals.map((approval) => ({
11368
+ approvalId: approval.approvalId,
11369
+ runId,
11370
+ tool: approval.tool,
11371
+ toolCallId: approval.toolCallId,
11372
+ input: approval.input,
11373
+ checkpointMessages,
11374
+ baseMessageCount,
11375
+ pendingToolCalls
11376
+ }));
11377
+ var applyTurnMetadata = (conv, meta, opts = {}) => {
11378
+ const {
11379
+ clearContinuation = true,
11380
+ clearApprovals = true,
11381
+ setIdle = true,
11382
+ shouldRebuildCanonical = false
11383
+ } = opts;
11384
+ if (meta.continuation && meta.continuationMessages) {
11385
+ conv._continuationMessages = meta.continuationMessages;
11386
+ } else if (clearContinuation) {
11387
+ conv._continuationMessages = void 0;
11388
+ conv._continuationCount = void 0;
11389
+ }
11390
+ if (meta.harnessMessages) {
11391
+ conv._harnessMessages = meta.harnessMessages;
11392
+ } else if (shouldRebuildCanonical) {
11393
+ conv._harnessMessages = conv.messages;
11394
+ }
11395
+ if (meta.toolResultArchive !== void 0) {
11396
+ conv._toolResultArchive = meta.toolResultArchive;
11397
+ }
11398
+ conv.runtimeRunId = meta.latestRunId || conv.runtimeRunId;
11399
+ if (clearApprovals) conv.pendingApprovals = [];
11400
+ if (setIdle) conv.runStatus = "idle";
11401
+ if (meta.contextTokens > 0) conv.contextTokens = meta.contextTokens;
11402
+ if (meta.contextWindow > 0) conv.contextWindow = meta.contextWindow;
11403
+ conv.updatedAt = Date.now();
11404
+ };
11405
+
11406
+ // src/orchestrator/continuation.ts
11407
+ var TOOL_RESULT_ARCHIVE_PARAM2 = "__toolResultArchive";
11408
+ var withToolResultArchiveParam = (parameters, conversation) => ({
11409
+ ...parameters ?? {},
11410
+ [TOOL_RESULT_ARCHIVE_PARAM2]: conversation._toolResultArchive ?? {}
11411
+ });
11412
+ var MAX_CONTINUATION_COUNT = 20;
11413
+
11414
+ // src/orchestrator/subagents.ts
11415
+ var MAX_SUBAGENT_NESTING = 3;
11416
+ var MAX_CONCURRENT_SUBAGENTS = 2;
11417
+ var MAX_SUBAGENT_CALLBACK_COUNT = 20;
11418
+ var CALLBACK_LOCK_STALE_MS = 5 * 60 * 1e3;
11419
+ var STALE_SUBAGENT_THRESHOLD_MS = 5 * 60 * 1e3;
11420
+
11421
+ // src/orchestrator/orchestrator.ts
11422
+ var AgentOrchestrator = class {
11423
+ harness;
11424
+ conversationStore;
11425
+ eventSink;
11426
+ telemetry;
11427
+ /** @internal */
11428
+ hooks;
11429
+ agentId;
11430
+ workingDir;
11431
+ // ── Runtime state (conversation runs) ──
11432
+ activeConversationRuns = /* @__PURE__ */ new Map();
11433
+ runOwners = /* @__PURE__ */ new Map();
11434
+ runConversations = /* @__PURE__ */ new Map();
11435
+ approvalDecisionTracker = /* @__PURE__ */ new Map();
11436
+ // ── Runtime state (subagents) ──
11437
+ activeSubagentRuns = /* @__PURE__ */ new Map();
11438
+ recentlySpawnedParents = /* @__PURE__ */ new Map();
11439
+ pendingSubagentApprovals = /* @__PURE__ */ new Map();
11440
+ pendingCallbackNeeded = /* @__PURE__ */ new Set();
11441
+ constructor(options) {
11442
+ this.harness = options.harness;
11443
+ this.conversationStore = options.conversationStore;
11444
+ this.eventSink = options.eventSink;
11445
+ this.telemetry = options.telemetry;
11446
+ this.hooks = options.hooks ?? options.continuationHooks;
11447
+ this.agentId = options.agentId ?? options.harness.frontmatter?.id ?? "";
11448
+ this.workingDir = options.workingDir ?? "";
11449
+ }
11450
+ setHooks(hooks) {
11451
+ this.hooks = hooks;
11452
+ }
11453
+ /** @deprecated Use setHooks instead */
11454
+ setContinuationHooks(hooks) {
11455
+ this.hooks = hooks;
11456
+ }
11457
+ get isServerless() {
11458
+ return typeof this.hooks?.dispatchBackground === "function";
11459
+ }
11460
+ // ══════════════════════════════════════════════════════════════════════════
11461
+ // Continuation
11462
+ // ══════════════════════════════════════════════════════════════════════════
11463
+ async runContinuation(conversationId, onYield) {
11464
+ const conversation = await this.conversationStore.getWithArchive(conversationId);
11465
+ if (!conversation) return;
11466
+ if (Array.isArray(conversation.pendingApprovals) && conversation.pendingApprovals.length > 0) return;
11467
+ if (!conversation._continuationMessages?.length) return;
11468
+ if (conversation.runStatus === "running") return;
11469
+ const count = (conversation._continuationCount ?? 0) + 1;
11470
+ if (count > MAX_CONTINUATION_COUNT) {
11471
+ console.warn(`[poncho][continuation] Max continuation count (${MAX_CONTINUATION_COUNT}) reached for ${conversationId}`);
11472
+ conversation._continuationMessages = void 0;
11473
+ conversation._continuationCount = void 0;
11474
+ await this.conversationStore.update(conversation);
11475
+ return;
11476
+ }
11477
+ const continuationMessages = [...conversation._continuationMessages];
11478
+ conversation._continuationMessages = void 0;
11479
+ conversation._continuationCount = count;
11480
+ conversation.runStatus = "running";
11481
+ await this.conversationStore.update(conversation);
11482
+ const abortController = new AbortController();
11483
+ this.activeConversationRuns.set(conversationId, {
11484
+ ownerId: conversation.ownerId,
11485
+ abortController,
11486
+ runId: null
11487
+ });
11488
+ this.hooks?.onContinuationStart?.(conversationId);
11489
+ try {
11490
+ if (conversation.parentConversationId) {
11491
+ for await (const event of this.runSubagentContinuation(conversationId, conversation, continuationMessages)) {
11492
+ if (onYield) await onYield(event);
11493
+ }
11494
+ } else {
11495
+ await this.runChatContinuation(conversationId, conversation, continuationMessages, onYield);
11496
+ }
11497
+ } finally {
11498
+ this.activeConversationRuns.delete(conversationId);
11499
+ this.hooks?.onContinuationEnd?.(conversationId);
11500
+ }
11501
+ }
11502
+ async runChatContinuation(conversationId, conversation, continuationMessages, onYield) {
11503
+ const execution = await executeConversationTurn({
11504
+ harness: this.harness,
11505
+ runInput: {
11506
+ conversationId,
11507
+ tenantId: conversation.tenantId ?? void 0,
11508
+ parameters: withToolResultArchiveParam({
11509
+ __activeConversationId: conversationId,
11510
+ __ownerId: conversation.ownerId
11511
+ }, conversation),
11512
+ messages: continuationMessages,
11513
+ abortSignal: this.activeConversationRuns.get(conversationId)?.abortController.signal
11514
+ },
11515
+ initialContextTokens: conversation.contextTokens ?? 0,
11516
+ initialContextWindow: conversation.contextWindow ?? 0,
11517
+ onEvent: async (event) => {
11518
+ if (event.type === "run:started") {
11519
+ this.runOwners.set(event.runId, conversation.ownerId);
11520
+ this.runConversations.set(event.runId, conversationId);
11521
+ const active = this.activeConversationRuns.get(conversationId);
11522
+ if (active) active.runId = event.runId;
11523
+ }
11524
+ if (this.telemetry) await this.telemetry.emit(event);
11525
+ await this.eventSink(conversationId, event);
11526
+ if (onYield) await onYield(event);
11527
+ }
11528
+ });
11529
+ flushTurnDraft(execution.draft);
11530
+ const freshConv = await this.conversationStore.get(conversationId);
11531
+ if (!freshConv) return;
11532
+ const hasContent = execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0;
11533
+ if (hasContent) {
11534
+ freshConv.messages = [
11535
+ ...freshConv.messages,
11536
+ {
11537
+ role: "assistant",
11538
+ content: execution.draft.assistantResponse,
11539
+ metadata: buildAssistantMetadata(execution.draft)
11540
+ }
11541
+ ];
11542
+ }
11543
+ applyTurnMetadata(freshConv, {
11544
+ latestRunId: execution.latestRunId,
11545
+ contextTokens: execution.runContextTokens,
11546
+ contextWindow: execution.runContextWindow,
11547
+ continuation: execution.runContinuation,
11548
+ continuationMessages: execution.runContinuationMessages,
11549
+ harnessMessages: execution.runHarnessMessages,
11550
+ toolResultArchive: this.harness.getToolResultArchive(conversationId)
11551
+ }, { shouldRebuildCanonical: true });
11552
+ if (execution.runContinuation) {
11553
+ freshConv._continuationCount = conversation._continuationCount;
11554
+ }
11555
+ await this.conversationStore.update(freshConv);
11556
+ }
11557
+ // ══════════════════════════════════════════════════════════════════════════
11558
+ // Approval checkpoint management
11559
+ // ══════════════════════════════════════════════════════════════════════════
11560
+ async findPendingApproval(approvalId, owner) {
11561
+ const searchedConversationIds = /* @__PURE__ */ new Set();
11562
+ const scan = async (conversations) => {
11563
+ for (const conv of conversations) {
11564
+ if (searchedConversationIds.has(conv.conversationId)) continue;
11565
+ searchedConversationIds.add(conv.conversationId);
11566
+ if (!Array.isArray(conv.pendingApprovals)) continue;
11567
+ const match = conv.pendingApprovals.find((a) => a.approvalId === approvalId);
11568
+ if (match) {
11569
+ return { conversation: conv, approval: match };
11570
+ }
11571
+ }
11572
+ return void 0;
11573
+ };
11574
+ const ownerScoped = await scan(await this.conversationStore.list(owner));
11575
+ if (ownerScoped) return ownerScoped;
11576
+ if (owner === "local-owner") {
11577
+ return await scan(await this.conversationStore.list());
11578
+ }
11579
+ return void 0;
11580
+ }
11581
+ async resumeRunFromCheckpoint(conversationId, conversation, checkpoint, toolResults) {
11582
+ const abortController = new AbortController();
11583
+ this.activeConversationRuns.set(conversationId, {
11584
+ ownerId: conversation.ownerId,
11585
+ abortController,
11586
+ runId: null
11587
+ });
11588
+ let latestRunId = conversation.runtimeRunId ?? "";
11589
+ let checkpointedRun = false;
11590
+ const normalizedCheckpoint = normalizeApprovalCheckpoint(checkpoint, conversation.messages);
11591
+ const baseMessages = normalizedCheckpoint.baseMessageCount != null ? conversation.messages.slice(0, normalizedCheckpoint.baseMessageCount) : [];
11592
+ const fullCheckpointMessages = [...baseMessages, ...normalizedCheckpoint.checkpointMessages];
11593
+ let resumeToolResultMsg;
11594
+ const lastCpMsg = fullCheckpointMessages[fullCheckpointMessages.length - 1];
11595
+ if (lastCpMsg?.role === "assistant") {
11596
+ try {
11597
+ const parsed = JSON.parse(typeof lastCpMsg.content === "string" ? lastCpMsg.content : "");
11598
+ const cpToolCalls = parsed.tool_calls ?? [];
11599
+ if (cpToolCalls.length > 0) {
11600
+ const providedMap = new Map(toolResults.map((r) => [r.callId, r]));
11601
+ resumeToolResultMsg = {
11602
+ role: "tool",
11603
+ content: JSON.stringify(cpToolCalls.map((tc) => {
11604
+ const provided = providedMap.get(tc.id);
11605
+ return {
11606
+ type: "tool_result",
11607
+ tool_use_id: tc.id,
11608
+ tool_name: provided?.toolName ?? tc.name,
11609
+ content: provided ? provided.error ? `Tool error: ${provided.error}` : JSON.stringify(provided.result ?? null) : "Tool error: Tool execution deferred (pending approval checkpoint)"
11610
+ };
11611
+ })),
11612
+ metadata: { timestamp: Date.now() }
11613
+ };
11614
+ }
11615
+ } catch {
11616
+ }
11617
+ }
11618
+ const fullCheckpointWithResults = resumeToolResultMsg ? [...fullCheckpointMessages, resumeToolResultMsg] : fullCheckpointMessages;
11619
+ let draftRef;
11620
+ let execution;
11621
+ try {
11622
+ execution = await executeConversationTurn({
11623
+ harness: this.harness,
11624
+ events: this.harness.continueFromToolResult({
11625
+ messages: fullCheckpointMessages,
11626
+ toolResults,
11627
+ conversationId,
11628
+ abortSignal: abortController.signal
11629
+ }),
11630
+ initialContextTokens: conversation.contextTokens ?? 0,
11631
+ initialContextWindow: conversation.contextWindow ?? 0,
11632
+ onEvent: async (event, draft2) => {
11633
+ draftRef = draft2;
11634
+ if (event.type === "run:started") {
11635
+ latestRunId = event.runId;
11636
+ this.runOwners.set(event.runId, conversation.ownerId);
11637
+ this.runConversations.set(event.runId, conversationId);
11638
+ const active = this.activeConversationRuns.get(conversationId);
11639
+ if (active && active.abortController === abortController) {
11640
+ active.runId = event.runId;
11641
+ }
11642
+ }
11643
+ if (event.type === "tool:approval:required") {
11644
+ const toolText = `- approval required \`${event.tool}\``;
11645
+ draft2.toolTimeline.push(toolText);
11646
+ draft2.currentTools.push(toolText);
11647
+ }
11648
+ if (event.type === "tool:approval:checkpoint") {
11649
+ const cpEvent = event;
11650
+ const conv = await this.conversationStore.get(conversationId);
11651
+ if (conv) {
11652
+ conv.pendingApprovals = buildApprovalCheckpoints({
11653
+ approvals: cpEvent.approvals,
11654
+ runId: latestRunId,
11655
+ checkpointMessages: [...fullCheckpointWithResults, ...cpEvent.checkpointMessages],
11656
+ baseMessageCount: 0,
11657
+ pendingToolCalls: cpEvent.pendingToolCalls
11658
+ });
11659
+ conv.updatedAt = Date.now();
11660
+ await this.conversationStore.update(conv);
11661
+ this.hooks?.onApprovalCheckpoint?.(
11662
+ conversationId,
11663
+ cpEvent.approvals.map((a) => ({ approvalId: a.approvalId, tool: a.tool, input: a.input }))
11664
+ );
11665
+ }
11666
+ checkpointedRun = true;
11667
+ }
11668
+ if (this.telemetry) await this.telemetry.emit(event);
11669
+ await this.eventSink(conversationId, event);
11670
+ }
11671
+ });
11672
+ flushTurnDraft(execution.draft);
11673
+ latestRunId = execution.latestRunId || latestRunId;
11674
+ } catch (err) {
11675
+ console.error("[resume-run] error:", err instanceof Error ? err.message : err);
11676
+ if (draftRef) {
11677
+ draftRef.assistantResponse = draftRef.assistantResponse || `[Error: ${err instanceof Error ? err.message : "Unknown error"}]`;
11678
+ flushTurnDraft(draftRef);
11679
+ }
11680
+ }
11681
+ const draft = execution?.draft ?? draftRef ?? createTurnDraftState();
11682
+ if (!checkpointedRun) {
11683
+ const conv = await this.conversationStore.get(conversationId);
11684
+ if (conv) {
11685
+ const hasAssistantContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0;
11686
+ if (hasAssistantContent) {
11687
+ const prevMessages = conv.messages;
11688
+ const lastMsg = prevMessages[prevMessages.length - 1];
11689
+ if (lastMsg && lastMsg.role === "assistant" && lastMsg.metadata) {
11690
+ const existingToolActivity = lastMsg.metadata.toolActivity;
11691
+ const existingSections = lastMsg.metadata.sections;
11692
+ const mergedTimeline = [
11693
+ ...Array.isArray(existingToolActivity) ? existingToolActivity : [],
11694
+ ...draft.toolTimeline
11695
+ ];
11696
+ const mergedSections = [
11697
+ ...Array.isArray(existingSections) ? existingSections : [],
11698
+ ...draft.sections
11699
+ ];
11700
+ const mergedText = (typeof lastMsg.content === "string" ? lastMsg.content : "") + draft.assistantResponse;
11701
+ conv.messages = [
11702
+ ...prevMessages.slice(0, -1),
11703
+ {
11704
+ role: "assistant",
11705
+ content: mergedText,
11706
+ metadata: {
11707
+ toolActivity: mergedTimeline,
11708
+ sections: mergedSections.length > 0 ? mergedSections : void 0
11709
+ }
11710
+ }
11711
+ ];
11712
+ } else {
11713
+ conv.messages = [
11714
+ ...prevMessages,
11715
+ {
11716
+ role: "assistant",
11717
+ content: draft.assistantResponse,
11718
+ metadata: buildAssistantMetadata(draft)
11719
+ }
11720
+ ];
11721
+ }
11722
+ }
11723
+ applyTurnMetadata(conv, {
11724
+ latestRunId,
11725
+ contextTokens: execution?.runContextTokens ?? 0,
11726
+ contextWindow: execution?.runContextWindow ?? 0,
11727
+ harnessMessages: execution?.runHarnessMessages
11728
+ }, { shouldRebuildCanonical: true });
11729
+ await this.conversationStore.update(conv);
11730
+ }
11731
+ } else {
11732
+ const conv = await this.conversationStore.get(conversationId);
11733
+ if (conv) {
11734
+ conv.runStatus = "idle";
11735
+ conv.updatedAt = Date.now();
11736
+ await this.conversationStore.update(conv);
11737
+ }
11738
+ }
11739
+ this.activeConversationRuns.delete(conversationId);
11740
+ if (latestRunId) {
11741
+ this.runOwners.delete(latestRunId);
11742
+ this.runConversations.delete(latestRunId);
11743
+ }
11744
+ console.log("[resume-run] complete for", conversationId);
11745
+ if (this.hooks?.onResumeComplete) {
11746
+ this.hooks.onResumeComplete(conversationId, checkpointedRun);
11747
+ } else {
11748
+ await this._handlePostResumeWork(conversationId);
11749
+ }
11750
+ }
11751
+ /** After a resume completes, check for deferred subagent callbacks. */
11752
+ async _handlePostResumeWork(conversationId) {
11753
+ const hadDeferred = this.pendingCallbackNeeded.delete(conversationId);
11754
+ const postConv = await this.conversationStore.get(conversationId);
11755
+ const needsCallback = hadDeferred || !!postConv?.pendingSubagentResults?.length;
11756
+ const hasRunningChildren = this.hasRunningSubagentsForParent(conversationId);
11757
+ if (!needsCallback && !hasRunningChildren) {
11758
+ this.hooks?.onStreamEnd?.(conversationId);
11759
+ }
11760
+ if (needsCallback) {
11761
+ this.processSubagentCallback(conversationId, true).catch(
11762
+ (err) => console.error(`[poncho][subagent-callback] Post-resume callback failed:`, err instanceof Error ? err.message : err)
11763
+ );
11764
+ }
11765
+ }
11766
+ // ══════════════════════════════════════════════════════════════════════════
11767
+ // Subagent lifecycle
11768
+ // ══════════════════════════════════════════════════════════════════════════
11769
+ // ── Helpers ──
11770
+ hasRunningSubagentsForParent(parentConversationId) {
11771
+ for (const run of this.activeSubagentRuns.values()) {
11772
+ if (run.parentConversationId === parentConversationId) return true;
11773
+ }
11774
+ return false;
11775
+ }
11776
+ getRunningSubagentCountForParent(parentId) {
11777
+ let count = 0;
11778
+ for (const run of this.activeSubagentRuns.values()) {
11779
+ if (run.parentConversationId === parentId) count += 1;
11780
+ }
11781
+ return count;
11782
+ }
11783
+ async getSubagentDepth(conversationId) {
11784
+ let depth = 0;
11785
+ let current = await this.conversationStore.get(conversationId);
11786
+ while (current?.parentConversationId) {
11787
+ depth += 1;
11788
+ current = await this.conversationStore.get(current.parentConversationId);
11789
+ }
11790
+ return depth;
11791
+ }
11792
+ async hasPendingSubagentWorkForParent(parentConversationId, _owner) {
11793
+ if (this.hasRunningSubagentsForParent(parentConversationId)) return true;
11794
+ if (this.recentlySpawnedParents.has(parentConversationId)) return true;
11795
+ if (this.pendingCallbackNeeded.has(parentConversationId)) return true;
11796
+ const parentConversation = await this.conversationStore.get(parentConversationId);
11797
+ if (!parentConversation) return false;
11798
+ if (Array.isArray(parentConversation.pendingSubagentResults) && parentConversation.pendingSubagentResults.length > 0) return true;
11799
+ if (typeof parentConversation.runningCallbackSince === "number" && parentConversation.runningCallbackSince > 0) return true;
11800
+ return false;
11801
+ }
11802
+ // ── Subagent approval decision ──
11803
+ /**
11804
+ * Submit an approval decision for a pending subagent approval.
11805
+ * Returns whether the approval was found, the child conversation, and whether all approvals are now decided.
11806
+ */
11807
+ async submitSubagentApprovalDecision(approvalId, approved) {
11808
+ const pending = this.pendingSubagentApprovals.get(approvalId);
11809
+ if (!pending) return { found: false, allDecided: false };
11810
+ const decision = approved ? "approved" : "denied";
11811
+ pending.checkpoint.decision = decision;
11812
+ await this.eventSink(
11813
+ pending.childConversationId,
11814
+ approved ? { type: "tool:approval:granted", approvalId } : { type: "tool:approval:denied", approvalId }
11815
+ );
11816
+ const childConv = await this.conversationStore.get(pending.childConversationId);
11817
+ if (childConv && Array.isArray(childConv.pendingApprovals)) {
11818
+ childConv.pendingApprovals = childConv.pendingApprovals.map(
11819
+ (pa) => pa.approvalId === approvalId ? { ...pa, decision } : pa
11820
+ );
11821
+ await this.conversationStore.update(childConv);
11822
+ }
11823
+ const allApprovals = childConv?.pendingApprovals ?? [];
11824
+ const allDecided = allApprovals.length > 0 && allApprovals.every((pa) => pa.decision != null);
11825
+ if (allDecided) {
11826
+ for (const pa of allApprovals) this.pendingSubagentApprovals.delete(pa.approvalId);
11827
+ if (childConv) {
11828
+ childConv.pendingApprovals = [];
11829
+ await this.conversationStore.update(childConv);
11830
+ }
11831
+ pending.resolve(allApprovals);
11832
+ }
11833
+ return { found: true, childConversationId: pending.childConversationId, allDecided };
11834
+ }
11835
+ // ── Completion + checkpoint resume ──
11836
+ async handleSubagentCompletion(subagentId) {
11837
+ const conv = await this.conversationStore.get(subagentId);
11838
+ if (!conv || !conv.parentConversationId) return;
11839
+ if (conv.subagentMeta?.status === "completed" || conv.subagentMeta?.status === "error") return;
11840
+ conv.subagentMeta = { ...conv.subagentMeta, status: "completed" };
11841
+ conv.updatedAt = Date.now();
11842
+ await this.conversationStore.update(conv);
11843
+ const lastMsg = conv.messages[conv.messages.length - 1];
11844
+ const responseText = lastMsg?.role === "assistant" && typeof lastMsg.content === "string" ? lastMsg.content : "";
11845
+ const pendingResult = {
11846
+ subagentId,
11847
+ task: conv.subagentMeta?.task ?? conv.title,
11848
+ status: "completed",
11849
+ result: { status: "completed", response: responseText, steps: 0, tokens: { input: 0, output: 0, cached: 0 }, duration: 0 },
11850
+ timestamp: Date.now()
11851
+ };
11852
+ await this.conversationStore.appendSubagentResult(conv.parentConversationId, pendingResult);
11853
+ await this.eventSink(conv.parentConversationId, {
11854
+ type: "subagent:completed",
11855
+ subagentId,
11856
+ conversationId: subagentId
11857
+ });
11858
+ await this.triggerParentCallback(conv.parentConversationId);
11859
+ }
11860
+ async resumeSubagentFromCheckpoint(subagentId) {
11861
+ const conv = await this.conversationStore.get(subagentId);
11862
+ if (!conv || !conv.parentConversationId) return;
11863
+ const allApprovals = (conv.pendingApprovals ?? []).map(
11864
+ (approval) => normalizeApprovalCheckpoint(approval, conv.messages)
11865
+ );
11866
+ if (allApprovals.length === 0) return;
11867
+ const allDecided = allApprovals.every((a) => a.decision != null);
11868
+ if (!allDecided) return;
11869
+ conv.pendingApprovals = [];
11870
+ conv.subagentMeta = { ...conv.subagentMeta, status: "running" };
11871
+ await this.conversationStore.update(conv);
11872
+ const checkpointRef = allApprovals[0];
11873
+ const toolContext = {
11874
+ runId: checkpointRef.runId,
11875
+ agentId: this.agentId,
11876
+ step: 0,
11877
+ workingDir: this.workingDir,
11878
+ parameters: {},
11879
+ conversationId: subagentId
11880
+ };
11881
+ const approvalToolCallIds = new Set(allApprovals.map((a) => a.toolCallId));
11882
+ const callsToExecute = [];
11883
+ const deniedResults = [];
11884
+ for (const a of allApprovals) {
11885
+ if (a.decision === "approved" && a.toolCallId) {
11886
+ callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
11887
+ } else if (a.decision === "denied" && a.toolCallId) {
11888
+ deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
11889
+ }
11890
+ }
11891
+ const pendingToolCalls = checkpointRef.pendingToolCalls ?? [];
11892
+ for (const tc of pendingToolCalls) {
11893
+ if (!approvalToolCallIds.has(tc.id)) callsToExecute.push(tc);
11894
+ }
11895
+ let toolResults = [...deniedResults];
11896
+ if (callsToExecute.length > 0) {
11897
+ const execResults = await this.harness.executeTools(callsToExecute, toolContext);
11898
+ toolResults.push(...execResults.map((r) => ({
11899
+ callId: r.callId,
11900
+ toolName: r.tool,
11901
+ result: r.output,
11902
+ error: r.error
11903
+ })));
11904
+ }
11905
+ await this.resumeRunFromCheckpoint(subagentId, conv, checkpointRef, toolResults);
11906
+ await this.handleSubagentCompletion(subagentId);
11907
+ }
11908
+ // ── Subagent run ──
11909
+ async runSubagent(childConversationId, parentConversationId, task, ownerId) {
11910
+ if (!this.hooks?.createChildHarness) {
11911
+ throw new Error("createChildHarness hook is required for subagent support");
11912
+ }
11913
+ const childHarness = await this.hooks.createChildHarness();
11914
+ const childAbortController = new AbortController();
11915
+ this.activeSubagentRuns.set(childConversationId, { abortController: childAbortController, harness: childHarness, parentConversationId });
11916
+ this.activeConversationRuns.set(childConversationId, {
11917
+ ownerId,
11918
+ abortController: childAbortController,
11919
+ runId: null
11920
+ });
11921
+ const spawnCount = this.recentlySpawnedParents.get(parentConversationId) ?? 0;
11922
+ if (spawnCount <= 1) {
11923
+ this.recentlySpawnedParents.delete(parentConversationId);
11924
+ } else {
11925
+ this.recentlySpawnedParents.set(parentConversationId, spawnCount - 1);
11926
+ }
11927
+ childHarness.unregisterTools(["memory_main_write", "memory_main_edit"]);
11928
+ const draft = createTurnDraftState();
11929
+ let latestRunId = "";
11930
+ let runResult;
11931
+ try {
11932
+ const conversation = await this.conversationStore.getWithArchive(childConversationId);
11933
+ if (!conversation) throw new Error("Subagent conversation not found");
11934
+ if (conversation.subagentMeta?.status === "stopped") return;
11935
+ conversation.lastActivityAt = Date.now();
11936
+ await this.conversationStore.update(conversation);
11937
+ const runOutcome = resolveRunRequest(conversation, {
11938
+ conversationId: childConversationId,
11939
+ messages: conversation.messages
11940
+ });
11941
+ const harnessMessages = [...runOutcome.messages];
11942
+ const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: childConversationId }) ?? {};
11943
+ for await (const event of childHarness.runWithTelemetry({
11944
+ task,
11945
+ conversationId: childConversationId,
11946
+ tenantId: conversation.tenantId ?? void 0,
11947
+ parameters: withToolResultArchiveParam({
11948
+ ...recallParams,
11949
+ __activeConversationId: childConversationId,
11950
+ __ownerId: ownerId
11951
+ }, conversation),
11952
+ messages: harnessMessages,
11953
+ abortSignal: childAbortController.signal
11954
+ })) {
11955
+ if (event.type === "run:started") {
11956
+ latestRunId = event.runId;
11957
+ const active = this.activeConversationRuns.get(childConversationId);
11958
+ if (active) active.runId = event.runId;
11959
+ }
11960
+ recordStandardTurnEvent(draft, event);
11961
+ if (event.type === "tool:approval:required") {
11962
+ const toolText = `- approval required \`${event.tool}\``;
11963
+ draft.toolTimeline.push(toolText);
11964
+ draft.currentTools.push(toolText);
11965
+ await this.eventSink(parentConversationId, {
11966
+ type: "subagent:approval_needed",
11967
+ subagentId: childConversationId,
11968
+ conversationId: childConversationId,
11969
+ tool: event.tool,
11970
+ approvalId: event.approvalId,
11971
+ input: event.input
11972
+ });
11973
+ }
11974
+ if (event.type === "tool:approval:checkpoint") {
11975
+ const cpConv = await this.conversationStore.get(childConversationId);
11976
+ if (cpConv) {
11977
+ const allCpData = buildApprovalCheckpoints({
11978
+ approvals: event.approvals,
11979
+ runId: latestRunId,
11980
+ checkpointMessages: [...harnessMessages, ...event.checkpointMessages],
11981
+ baseMessageCount: 0,
11982
+ pendingToolCalls: event.pendingToolCalls
11983
+ });
11984
+ cpConv.pendingApprovals = allCpData;
11985
+ cpConv.updatedAt = Date.now();
11986
+ await this.conversationStore.update(cpConv);
11987
+ const decidedApprovals = await new Promise((resolve12) => {
11988
+ for (const cpData of allCpData) {
11989
+ this.pendingSubagentApprovals.set(cpData.approvalId, {
11990
+ resolve: resolve12,
11991
+ childHarness,
11992
+ checkpoint: cpData,
11993
+ childConversationId,
11994
+ parentConversationId
11995
+ });
11996
+ }
11997
+ });
11998
+ const checkpointRef = normalizeApprovalCheckpoint(allCpData[0], [...harnessMessages]);
11999
+ const toolContext = {
12000
+ runId: checkpointRef.runId,
12001
+ agentId: this.agentId,
12002
+ step: 0,
12003
+ workingDir: this.workingDir,
12004
+ parameters: {},
12005
+ conversationId: childConversationId
12006
+ };
12007
+ const approvalToolCallIds = new Set(decidedApprovals.map((a) => a.toolCallId));
12008
+ const callsToExecute = [];
12009
+ const deniedResults = [];
12010
+ for (const a of decidedApprovals) {
12011
+ if (a.decision === "approved" && a.toolCallId) {
12012
+ callsToExecute.push({ id: a.toolCallId, name: a.tool, input: a.input });
12013
+ const toolText = `- done \`${a.tool}\``;
12014
+ draft.toolTimeline.push(toolText);
12015
+ draft.currentTools.push(toolText);
12016
+ } else if (a.toolCallId) {
12017
+ deniedResults.push({ callId: a.toolCallId, toolName: a.tool, error: "Tool execution denied by user" });
12018
+ const toolText = `- denied \`${a.tool}\``;
12019
+ draft.toolTimeline.push(toolText);
12020
+ draft.currentTools.push(toolText);
12021
+ }
12022
+ }
12023
+ const cpPendingToolCalls = checkpointRef.pendingToolCalls ?? [];
12024
+ for (const tc of cpPendingToolCalls) {
12025
+ if (!approvalToolCallIds.has(tc.id)) {
12026
+ callsToExecute.push(tc);
12027
+ }
12028
+ }
12029
+ let toolResults = [...deniedResults];
12030
+ if (callsToExecute.length > 0) {
12031
+ const execResults = await childHarness.executeTools(callsToExecute, toolContext);
12032
+ toolResults.push(...execResults.map((r) => ({
12033
+ callId: r.callId,
12034
+ toolName: r.tool,
12035
+ result: r.output,
12036
+ error: r.error
12037
+ })));
12038
+ }
12039
+ const resumeMessages = [...checkpointRef.checkpointMessages];
12040
+ for await (const resumeEvent of childHarness.continueFromToolResult({
12041
+ messages: resumeMessages,
12042
+ toolResults,
12043
+ conversationId: childConversationId,
12044
+ abortSignal: childAbortController.signal
12045
+ })) {
12046
+ recordStandardTurnEvent(draft, resumeEvent);
12047
+ if (resumeEvent.type === "run:completed") {
12048
+ runResult = { status: resumeEvent.result.status, response: resumeEvent.result.response, steps: resumeEvent.result.steps, duration: resumeEvent.result.duration };
12049
+ if (draft.assistantResponse.length === 0 && resumeEvent.result.response) {
12050
+ draft.assistantResponse = resumeEvent.result.response;
12051
+ }
12052
+ }
12053
+ if (resumeEvent.type === "run:error") {
12054
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${resumeEvent.error.message}]`;
12055
+ }
12056
+ await this.eventSink(childConversationId, resumeEvent);
12057
+ }
12058
+ }
12059
+ }
12060
+ if (event.type === "run:completed") {
12061
+ runResult = { status: event.result.status, response: event.result.response, steps: event.result.steps, duration: event.result.duration, continuation: event.result.continuation, continuationMessages: event.result.continuationMessages };
12062
+ if (draft.assistantResponse.length === 0 && event.result.response) {
12063
+ draft.assistantResponse = event.result.response;
12064
+ }
12065
+ }
12066
+ if (event.type === "run:error") {
12067
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
12068
+ }
12069
+ await this.eventSink(childConversationId, event);
12070
+ }
12071
+ flushTurnDraft(draft);
12072
+ const conv = await this.conversationStore.get(childConversationId);
12073
+ if (conv) {
12074
+ const hasContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0;
12075
+ if (hasContent) {
12076
+ conv.messages.push({
12077
+ role: "assistant",
12078
+ content: draft.assistantResponse,
12079
+ metadata: buildAssistantMetadata(draft)
12080
+ });
12081
+ }
12082
+ if (runResult?.continuation && runResult.continuationMessages) {
12083
+ conv._continuationMessages = runResult.continuationMessages;
12084
+ } else {
12085
+ conv._continuationMessages = void 0;
12086
+ conv._continuationCount = void 0;
12087
+ }
12088
+ if (runResult?.continuationMessages) {
12089
+ conv._harnessMessages = runResult.continuationMessages;
12090
+ } else if (runOutcome.shouldRebuildCanonical) {
12091
+ conv._harnessMessages = conv.messages;
12092
+ }
12093
+ conv._toolResultArchive = childHarness.getToolResultArchive(childConversationId);
12094
+ conv.lastActivityAt = Date.now();
12095
+ conv.updatedAt = Date.now();
12096
+ if (runResult?.continuation) {
12097
+ await this.conversationStore.update(conv);
12098
+ this.hooks?.onStreamEnd?.(childConversationId);
12099
+ this.activeSubagentRuns.delete(childConversationId);
12100
+ this.activeConversationRuns.delete(childConversationId);
12101
+ try {
12102
+ await childHarness.shutdown();
12103
+ } catch {
12104
+ }
12105
+ if (this.isServerless) {
12106
+ this.hooks.dispatchBackground("continuation", childConversationId);
12107
+ } else {
12108
+ this.runContinuation(childConversationId).catch(
12109
+ (err) => console.error(`[poncho][subagent] Continuation failed:`, err instanceof Error ? err.message : err)
12110
+ );
12111
+ }
12112
+ return;
12113
+ }
12114
+ conv.subagentMeta = { ...conv.subagentMeta, status: "completed" };
12115
+ await this.conversationStore.update(conv);
12116
+ }
12117
+ this.hooks?.onStreamEnd?.(childConversationId);
12118
+ await this.eventSink(parentConversationId, {
12119
+ type: "subagent:completed",
12120
+ subagentId: childConversationId,
12121
+ conversationId: childConversationId
12122
+ });
12123
+ let subagentResponse = runResult?.response ?? draft.assistantResponse;
12124
+ if (!subagentResponse) {
12125
+ const freshSubConv = await this.conversationStore.get(childConversationId);
12126
+ if (freshSubConv) {
12127
+ const lastAssistant = [...freshSubConv.messages].reverse().find((m) => m.role === "assistant");
12128
+ if (lastAssistant && typeof lastAssistant.content === "string") {
12129
+ subagentResponse = lastAssistant.content;
12130
+ }
12131
+ }
12132
+ }
12133
+ const pendingResult = {
12134
+ subagentId: childConversationId,
12135
+ task,
12136
+ status: "completed",
12137
+ result: runResult ? { status: runResult.status, response: subagentResponse, steps: runResult.steps, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult.duration } : void 0,
12138
+ timestamp: Date.now()
12139
+ };
12140
+ await this.conversationStore.appendSubagentResult(parentConversationId, pendingResult);
12141
+ this.triggerParentCallback(parentConversationId).catch(
12142
+ (err) => console.error(`[poncho][subagent] Parent callback failed:`, err instanceof Error ? err.message : err)
12143
+ );
12144
+ } catch (err) {
12145
+ const errMsg = err instanceof Error ? err.message : String(err);
12146
+ console.error(`[poncho][subagent] Error in subagent ${childConversationId}:`, errMsg);
12147
+ const conv = await this.conversationStore.get(childConversationId);
12148
+ if (conv) {
12149
+ conv.subagentMeta = {
12150
+ ...conv.subagentMeta,
12151
+ status: "error",
12152
+ error: { code: "SUBAGENT_ERROR", message: errMsg }
12153
+ };
12154
+ conv.updatedAt = Date.now();
12155
+ await this.conversationStore.update(conv);
12156
+ }
12157
+ this.hooks?.onStreamEnd?.(childConversationId);
12158
+ await this.eventSink(parentConversationId, {
12159
+ type: "subagent:error",
12160
+ subagentId: childConversationId,
12161
+ conversationId: childConversationId,
12162
+ error: errMsg
12163
+ });
12164
+ const pendingResult = {
12165
+ subagentId: childConversationId,
12166
+ task,
12167
+ status: "error",
12168
+ error: { code: "SUBAGENT_ERROR", message: errMsg },
12169
+ timestamp: Date.now()
12170
+ };
12171
+ await this.conversationStore.appendSubagentResult(parentConversationId, pendingResult).catch(() => {
12172
+ });
12173
+ this.triggerParentCallback(parentConversationId).catch(
12174
+ (err2) => console.error(`[poncho][subagent] Parent callback failed:`, err2 instanceof Error ? err2.message : err2)
12175
+ );
12176
+ } finally {
12177
+ for (const [aid, pa] of this.pendingSubagentApprovals) {
12178
+ if (pa.childHarness === childHarness) {
12179
+ this.pendingSubagentApprovals.delete(aid);
12180
+ }
12181
+ }
12182
+ this.activeSubagentRuns.delete(childConversationId);
12183
+ this.activeConversationRuns.delete(childConversationId);
12184
+ try {
12185
+ await childHarness.shutdown();
12186
+ } catch {
12187
+ }
12188
+ }
12189
+ }
12190
+ // ── Parent callback ──
12191
+ async triggerParentCallback(parentConversationId) {
12192
+ if (this.activeConversationRuns.has(parentConversationId)) {
12193
+ this.pendingCallbackNeeded.add(parentConversationId);
12194
+ return;
12195
+ }
12196
+ if (this.isServerless) {
12197
+ this.hooks.dispatchBackground("subagent-callback", parentConversationId);
12198
+ return;
12199
+ }
12200
+ await this.processSubagentCallback(parentConversationId);
12201
+ }
12202
+ async processSubagentCallback(conversationId, skipLockCheck = false) {
12203
+ const conversation = await this.conversationStore.getWithArchive(conversationId);
12204
+ if (!conversation) return;
12205
+ const pendingResults = conversation.pendingSubagentResults ?? [];
12206
+ const hasOrphanedContinuation = pendingResults.length === 0 && Array.isArray(conversation._continuationMessages) && conversation._continuationMessages.length > 0 && !this.activeConversationRuns.has(conversationId);
12207
+ if (pendingResults.length === 0 && !hasOrphanedContinuation) return;
12208
+ if (!skipLockCheck && conversation.runningCallbackSince) {
12209
+ const elapsed = Date.now() - conversation.runningCallbackSince;
12210
+ if (elapsed < CALLBACK_LOCK_STALE_MS) return;
12211
+ console.warn(`[poncho][subagent-callback] Stale lock detected (${elapsed}ms) for ${conversationId}, proceeding`);
12212
+ }
12213
+ conversation.pendingSubagentResults = [];
12214
+ conversation.runningCallbackSince = Date.now();
12215
+ conversation.runStatus = "running";
12216
+ const callbackCount = (conversation.subagentCallbackCount ?? 0) + 1;
12217
+ conversation.subagentCallbackCount = callbackCount;
12218
+ for (const pr of pendingResults) {
12219
+ const resultBody = pr.result ? `Status: ${pr.result.status}
12220
+ Response: ${pr.result.response ?? "(no response)"}
12221
+ Steps: ${pr.result.steps}, Duration: ${pr.result.duration}ms` : pr.error ? `Error: ${pr.error.message}` : "(no result)";
12222
+ conversation.messages.push({
12223
+ role: "user",
12224
+ content: `[Subagent Result] Subagent "${pr.task}" (${pr.subagentId}) ${pr.status}:
12225
+
12226
+ ${resultBody}`,
12227
+ metadata: { _subagentCallback: true, subagentId: pr.subagentId, task: pr.task, timestamp: pr.timestamp }
12228
+ });
12229
+ }
12230
+ const processedIds = new Set(pendingResults.map((pr) => pr.subagentId));
12231
+ const freshForPending = await this.conversationStore.get(conversationId);
12232
+ const arrivedDuringCallback = (freshForPending?.pendingSubagentResults ?? []).filter((pr) => !processedIds.has(pr.subagentId));
12233
+ conversation.pendingSubagentResults = arrivedDuringCallback;
12234
+ conversation._harnessMessages = [...conversation.messages];
12235
+ conversation.updatedAt = Date.now();
12236
+ await this.conversationStore.update(conversation);
12237
+ if (callbackCount > MAX_SUBAGENT_CALLBACK_COUNT) {
12238
+ console.warn(`[poncho][subagent-callback] Circuit breaker: ${callbackCount} callbacks for ${conversationId}, skipping re-run`);
12239
+ conversation.runningCallbackSince = void 0;
12240
+ conversation.runStatus = "idle";
12241
+ await this.conversationStore.update(conversation);
12242
+ return;
12243
+ }
12244
+ const isContinuationResume = hasOrphanedContinuation && pendingResults.length === 0;
12245
+ console.log(`[poncho][subagent-callback] Processing ${pendingResults.length} result(s) for ${conversationId} (callback #${callbackCount})${isContinuationResume ? " (continuation resume)" : ""}`);
12246
+ const abortController = new AbortController();
12247
+ this.activeConversationRuns.set(conversationId, {
12248
+ ownerId: conversation.ownerId,
12249
+ abortController,
12250
+ runId: null
12251
+ });
12252
+ this.hooks?.onCallbackStreamReset?.(conversationId);
12253
+ const historySelection = resolveRunRequest(conversation, {
12254
+ conversationId,
12255
+ messages: conversation.messages,
12256
+ preferContinuation: isContinuationResume
12257
+ });
12258
+ const historyMessages = [...historySelection.messages];
12259
+ console.info(
12260
+ `[poncho][subagent-callback] conversation="${conversationId}" history_source=${historySelection.source}`
12261
+ );
12262
+ let execution;
12263
+ try {
12264
+ execution = await executeConversationTurn({
12265
+ harness: this.harness,
12266
+ runInput: {
12267
+ task: void 0,
12268
+ conversationId,
12269
+ tenantId: conversation.tenantId ?? void 0,
12270
+ parameters: withToolResultArchiveParam({
12271
+ __activeConversationId: conversationId,
12272
+ __ownerId: conversation.ownerId
12273
+ }, conversation),
12274
+ messages: historyMessages,
12275
+ abortSignal: abortController.signal
12276
+ },
12277
+ initialContextTokens: conversation.contextTokens ?? 0,
12278
+ initialContextWindow: conversation.contextWindow ?? 0,
12279
+ onEvent: (event) => {
12280
+ if (event.type === "run:started") {
12281
+ const active = this.activeConversationRuns.get(conversationId);
12282
+ if (active) active.runId = event.runId;
12283
+ }
12284
+ this.eventSink(conversationId, event);
12285
+ }
12286
+ });
12287
+ flushTurnDraft(execution.draft);
12288
+ const callbackNeedsContinuation = execution.runContinuation && execution.runContinuationMessages;
12289
+ if (callbackNeedsContinuation || execution.draft.assistantResponse.length > 0 || execution.draft.toolTimeline.length > 0) {
12290
+ const freshConv = await this.conversationStore.get(conversationId);
12291
+ if (freshConv) {
12292
+ if (!callbackNeedsContinuation) {
12293
+ freshConv.messages.push({
12294
+ role: "assistant",
12295
+ content: execution.draft.assistantResponse,
12296
+ metadata: buildAssistantMetadata(execution.draft)
12297
+ });
12298
+ }
12299
+ applyTurnMetadata(freshConv, {
12300
+ latestRunId: execution.latestRunId,
12301
+ contextTokens: execution.runContextTokens,
12302
+ contextWindow: execution.runContextWindow,
12303
+ continuation: !!callbackNeedsContinuation,
12304
+ continuationMessages: execution.runContinuationMessages,
12305
+ harnessMessages: callbackNeedsContinuation ? execution.runHarnessMessages : void 0,
12306
+ toolResultArchive: this.harness.getToolResultArchive(conversationId)
12307
+ }, { shouldRebuildCanonical: true, clearApprovals: false });
12308
+ freshConv.runningCallbackSince = void 0;
12309
+ await this.conversationStore.update(freshConv);
12310
+ if (freshConv.channelMeta && execution.draft.assistantResponse.length > 0) {
12311
+ this.hooks?.onMessagingNotify?.(conversationId, execution.draft.assistantResponse);
12312
+ }
12313
+ }
12314
+ }
12315
+ if (execution.runContinuation) {
12316
+ if (this.isServerless) {
12317
+ this.hooks.dispatchBackground("subagent-callback", conversationId);
12318
+ } else {
12319
+ this.processSubagentCallback(conversationId, true).catch(
12320
+ (err) => console.error(`[poncho][subagent-callback] Continuation failed:`, err instanceof Error ? err.message : err)
12321
+ );
12322
+ }
12323
+ }
12324
+ } catch (err) {
12325
+ console.error(`[poncho][subagent-callback] Error during parent re-run for ${conversationId}:`, err instanceof Error ? err.message : err);
12326
+ const errConv = await this.conversationStore.get(conversationId);
12327
+ if (errConv) {
12328
+ errConv.runningCallbackSince = void 0;
12329
+ errConv.runStatus = "idle";
12330
+ await this.conversationStore.update(errConv);
12331
+ }
12332
+ } finally {
12333
+ this.activeConversationRuns.delete(conversationId);
12334
+ const hadDeferredTrigger = this.pendingCallbackNeeded.delete(conversationId);
12335
+ const freshConv = await this.conversationStore.get(conversationId);
12336
+ const hasPendingInStore = !!freshConv?.pendingSubagentResults?.length;
12337
+ const hasRunningCallbackChildren = this.hasRunningSubagentsForParent(conversationId);
12338
+ if (!hadDeferredTrigger && !hasPendingInStore && !hasRunningCallbackChildren) {
12339
+ this.hooks?.onStreamEnd?.(conversationId);
12340
+ }
12341
+ if (hadDeferredTrigger || hasPendingInStore) {
12342
+ if (this.isServerless) {
12343
+ this.hooks.dispatchBackground("subagent-callback", conversationId);
12344
+ } else {
12345
+ this.processSubagentCallback(conversationId, true).catch(
12346
+ (err) => console.error(`[poncho][subagent-callback] Recursive callback failed:`, err instanceof Error ? err.message : err)
12347
+ );
12348
+ }
12349
+ } else if (freshConv?.runningCallbackSince) {
12350
+ const afterClear = await this.conversationStore.clearCallbackLock(conversationId);
12351
+ if (afterClear?.pendingSubagentResults?.length) {
12352
+ if (this.isServerless) {
12353
+ this.hooks.dispatchBackground("subagent-callback", conversationId);
12354
+ } else {
12355
+ this.processSubagentCallback(conversationId, true).catch(
12356
+ (err) => console.error(`[poncho][subagent-callback] Post-clear callback failed:`, err instanceof Error ? err.message : err)
12357
+ );
12358
+ }
12359
+ }
12360
+ }
12361
+ }
12362
+ }
12363
+ // ── Subagent continuation ──
12364
+ async *runSubagentContinuation(conversationId, conversation, continuationMessages) {
12365
+ if (!this.hooks?.createChildHarness) {
12366
+ throw new Error("createChildHarness hook is required for subagent support");
12367
+ }
12368
+ const parentConversationId = conversation.parentConversationId;
12369
+ const task = conversation.subagentMeta?.task ?? "";
12370
+ const ownerId = conversation.ownerId;
12371
+ const childHarness = await this.hooks.createChildHarness();
12372
+ childHarness.unregisterTools(["memory_main_write", "memory_main_edit"]);
12373
+ const childAbortController = this.activeConversationRuns.get(conversationId)?.abortController ?? new AbortController();
12374
+ this.activeSubagentRuns.set(conversationId, { abortController: childAbortController, harness: childHarness, parentConversationId });
12375
+ const draft = createTurnDraftState();
12376
+ let runResult;
12377
+ try {
12378
+ const recallParams = this.hooks?.buildRecallParams?.({ ownerId, tenantId: conversation.tenantId, excludeConversationId: conversationId }) ?? {};
12379
+ for await (const event of childHarness.runWithTelemetry({
12380
+ conversationId,
12381
+ tenantId: conversation.tenantId ?? void 0,
12382
+ parameters: withToolResultArchiveParam({
12383
+ ...recallParams,
12384
+ __activeConversationId: conversationId,
12385
+ __ownerId: ownerId
12386
+ }, conversation),
12387
+ messages: continuationMessages,
12388
+ abortSignal: childAbortController.signal
12389
+ })) {
12390
+ if (event.type === "run:started") {
12391
+ const active = this.activeConversationRuns.get(conversationId);
12392
+ if (active) active.runId = event.runId;
12393
+ }
12394
+ recordStandardTurnEvent(draft, event);
12395
+ if (event.type === "run:completed") {
12396
+ runResult = {
12397
+ status: event.result.status,
12398
+ response: event.result.response,
12399
+ steps: event.result.steps,
12400
+ duration: event.result.duration,
12401
+ continuation: event.result.continuation,
12402
+ continuationMessages: event.result.continuationMessages
12403
+ };
12404
+ if (!draft.assistantResponse && event.result.response) {
12405
+ draft.assistantResponse = event.result.response;
12406
+ }
12407
+ }
12408
+ if (event.type === "run:error") {
12409
+ draft.assistantResponse = draft.assistantResponse || `[Error: ${event.error.message}]`;
12410
+ }
12411
+ await this.eventSink(conversationId, event);
12412
+ yield event;
12413
+ }
12414
+ flushTurnDraft(draft);
12415
+ const conv = await this.conversationStore.get(conversationId);
12416
+ if (conv) {
12417
+ const hasContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0;
12418
+ if (runResult?.continuation && runResult.continuationMessages) {
12419
+ if (hasContent) {
12420
+ conv.messages.push({
12421
+ role: "assistant",
12422
+ content: draft.assistantResponse,
12423
+ metadata: buildAssistantMetadata(draft)
12424
+ });
12425
+ }
12426
+ conv._continuationMessages = runResult.continuationMessages;
12427
+ conv._continuationCount = conversation._continuationCount;
12428
+ } else {
12429
+ conv._continuationMessages = void 0;
12430
+ conv._continuationCount = void 0;
12431
+ if (hasContent) {
12432
+ conv.messages.push({
12433
+ role: "assistant",
12434
+ content: draft.assistantResponse,
12435
+ metadata: buildAssistantMetadata(draft)
12436
+ });
12437
+ }
12438
+ }
12439
+ if (runResult?.continuationMessages) {
12440
+ conv._harnessMessages = runResult.continuationMessages;
12441
+ } else {
12442
+ conv._harnessMessages = conv.messages;
12443
+ }
12444
+ conv._toolResultArchive = childHarness.getToolResultArchive(conversationId);
12445
+ conv.lastActivityAt = Date.now();
12446
+ conv.runStatus = "idle";
12447
+ conv.updatedAt = Date.now();
12448
+ if (runResult?.continuation) {
12449
+ await this.conversationStore.update(conv);
12450
+ this.activeSubagentRuns.delete(conversationId);
12451
+ try {
12452
+ await childHarness.shutdown();
12453
+ } catch {
12454
+ }
12455
+ return;
12456
+ }
12457
+ conv.subagentMeta = { ...conv.subagentMeta, status: "completed" };
12458
+ await this.conversationStore.update(conv);
12459
+ }
12460
+ this.activeSubagentRuns.delete(conversationId);
12461
+ await this.eventSink(parentConversationId, {
12462
+ type: "subagent:completed",
12463
+ subagentId: conversationId,
12464
+ conversationId
12465
+ });
12466
+ let subagentResponse = runResult?.response ?? draft.assistantResponse;
12467
+ if (!subagentResponse) {
12468
+ const freshSubConv = await this.conversationStore.get(conversationId);
12469
+ if (freshSubConv) {
12470
+ const lastAssistant = [...freshSubConv.messages].reverse().find((m) => m.role === "assistant");
12471
+ if (lastAssistant) {
12472
+ subagentResponse = typeof lastAssistant.content === "string" ? lastAssistant.content : "";
12473
+ }
12474
+ }
12475
+ }
12476
+ const parentConv = await this.conversationStore.get(parentConversationId);
12477
+ if (parentConv) {
12478
+ const result = {
12479
+ subagentId: conversationId,
12480
+ task,
12481
+ status: "completed",
12482
+ result: { status: "completed", response: subagentResponse, steps: runResult?.steps ?? 0, tokens: { input: 0, output: 0, cached: 0 }, duration: runResult?.duration ?? 0 },
12483
+ timestamp: Date.now()
12484
+ };
12485
+ await this.conversationStore.appendSubagentResult(parentConversationId, result);
12486
+ if (this.isServerless) {
12487
+ this.hooks.dispatchBackground("subagent-callback", parentConversationId);
12488
+ } else {
12489
+ this.processSubagentCallback(parentConversationId).catch(
12490
+ (err) => console.error(`[poncho][subagent] Callback failed:`, err instanceof Error ? err.message : err)
12491
+ );
12492
+ }
12493
+ }
12494
+ try {
12495
+ await childHarness.shutdown();
12496
+ } catch {
12497
+ }
12498
+ } catch (err) {
12499
+ this.activeSubagentRuns.delete(conversationId);
12500
+ try {
12501
+ await childHarness.shutdown();
12502
+ } catch {
12503
+ }
12504
+ const conv = await this.conversationStore.get(conversationId);
12505
+ if (conv) {
12506
+ conv.subagentMeta = { ...conv.subagentMeta, status: "error", error: { code: "CONTINUATION_ERROR", message: err instanceof Error ? err.message : String(err) } };
12507
+ conv.runStatus = "idle";
12508
+ conv._continuationMessages = void 0;
12509
+ conv._continuationCount = void 0;
12510
+ conv.updatedAt = Date.now();
12511
+ await this.conversationStore.update(conv);
12512
+ }
12513
+ await this.eventSink(conversation.parentConversationId, {
12514
+ type: "subagent:completed",
12515
+ subagentId: conversationId,
12516
+ conversationId
12517
+ });
12518
+ const parentConv = await this.conversationStore.get(conversation.parentConversationId);
12519
+ if (parentConv) {
12520
+ const result = {
12521
+ subagentId: conversationId,
12522
+ task,
12523
+ status: "error",
12524
+ error: { code: "CONTINUATION_ERROR", message: err instanceof Error ? err.message : String(err) },
12525
+ timestamp: Date.now()
12526
+ };
12527
+ await this.conversationStore.appendSubagentResult(conversation.parentConversationId, result);
12528
+ if (this.isServerless) {
12529
+ this.hooks.dispatchBackground("subagent-callback", conversation.parentConversationId);
12530
+ } else {
12531
+ this.processSubagentCallback(conversation.parentConversationId).catch(() => {
12532
+ });
12533
+ }
12534
+ }
12535
+ }
12536
+ }
12537
+ // ── SubagentManager factory ──
12538
+ createSubagentManager() {
12539
+ return {
12540
+ spawn: async (opts) => {
12541
+ const depth = await this.getSubagentDepth(opts.parentConversationId);
12542
+ if (depth >= MAX_SUBAGENT_NESTING - 1) {
12543
+ throw new Error(`Maximum subagent nesting (${MAX_SUBAGENT_NESTING} levels) reached. Cannot spawn deeper subagents.`);
12544
+ }
12545
+ if (this.getRunningSubagentCountForParent(opts.parentConversationId) >= MAX_CONCURRENT_SUBAGENTS) {
12546
+ throw new Error(`Maximum concurrent subagents (${MAX_CONCURRENT_SUBAGENTS}) per parent reached. Wait for running subagents to complete or stop some first.`);
12547
+ }
12548
+ const conversation = await this.conversationStore.create(
12549
+ opts.ownerId,
12550
+ opts.task.slice(0, 80),
12551
+ opts.tenantId ?? null,
12552
+ {
12553
+ parentConversationId: opts.parentConversationId,
12554
+ subagentMeta: { task: opts.task, status: "running" },
12555
+ messages: [{ role: "user", content: opts.task }]
12556
+ }
12557
+ );
12558
+ this.recentlySpawnedParents.set(
12559
+ opts.parentConversationId,
12560
+ (this.recentlySpawnedParents.get(opts.parentConversationId) ?? 0) + 1
12561
+ );
12562
+ await this.eventSink(opts.parentConversationId, {
12563
+ type: "subagent:spawned",
12564
+ subagentId: conversation.conversationId,
12565
+ conversationId: conversation.conversationId,
12566
+ task: opts.task
12567
+ });
12568
+ if (this.isServerless) {
12569
+ this.hooks.dispatchBackground("subagent-run", conversation.conversationId);
12570
+ } else {
12571
+ this.runSubagent(
12572
+ conversation.conversationId,
12573
+ opts.parentConversationId,
12574
+ opts.task,
12575
+ opts.ownerId
12576
+ ).catch((err) => console.error(`[poncho][subagent] Background spawn failed:`, err instanceof Error ? err.message : err));
12577
+ }
12578
+ return { subagentId: conversation.conversationId };
12579
+ },
12580
+ sendMessage: async (subagentId, message) => {
12581
+ const conversation = await this.conversationStore.get(subagentId);
12582
+ if (!conversation) {
12583
+ console.error(`[poncho][subagent] sendMessage: conversation "${subagentId}" not found in store`);
12584
+ throw new Error(`Subagent "${subagentId}" not found.`);
12585
+ }
12586
+ if (!conversation.parentConversationId) {
12587
+ throw new Error(`Conversation "${subagentId}" is not a subagent.`);
12588
+ }
12589
+ if (!conversation.subagentMeta) {
12590
+ console.warn(`[poncho][subagent] sendMessage: conversation "${subagentId}" missing subagentMeta, recovering`);
12591
+ conversation.subagentMeta = { task: conversation.title, status: "stopped" };
12592
+ await this.conversationStore.update(conversation);
12593
+ }
12594
+ if (conversation.subagentMeta.status === "running") {
12595
+ throw new Error(`Subagent "${subagentId}" is currently running. Wait for it to complete before sending a new message.`);
12596
+ }
12597
+ conversation.messages.push({ role: "user", content: message });
12598
+ conversation.subagentMeta.status = "running";
12599
+ conversation.updatedAt = Date.now();
12600
+ await this.conversationStore.update(conversation);
12601
+ if (this.isServerless) {
12602
+ this.hooks.dispatchBackground("subagent-run", subagentId);
12603
+ } else {
12604
+ this.runSubagent(
12605
+ subagentId,
12606
+ conversation.parentConversationId,
12607
+ message,
12608
+ conversation.ownerId
12609
+ ).catch((err) => console.error(`[poncho][subagent] Background sendMessage failed:`, err instanceof Error ? err.message : err));
12610
+ }
12611
+ return { subagentId };
12612
+ },
12613
+ stop: async (subagentId) => {
12614
+ const active = this.activeSubagentRuns.get(subagentId);
12615
+ if (active) {
12616
+ active.abortController.abort();
12617
+ }
12618
+ const conversation = await this.conversationStore.get(subagentId);
12619
+ if (conversation?.subagentMeta && conversation.subagentMeta.status === "running") {
12620
+ conversation.subagentMeta.status = "stopped";
12621
+ conversation.updatedAt = Date.now();
12622
+ await this.conversationStore.update(conversation);
12623
+ }
12624
+ },
12625
+ list: async (parentConversationId) => {
12626
+ const parentConv = await this.conversationStore.get(parentConversationId);
12627
+ const summaries = await this.conversationStore.listSummaries(parentConv?.ownerId ?? "local-owner");
12628
+ const childSummaries = summaries.filter((s) => s.parentConversationId === parentConversationId);
12629
+ const results = [];
12630
+ for (const s of childSummaries) {
12631
+ const c = await this.conversationStore.get(s.conversationId);
12632
+ if (c) {
12633
+ results.push({
12634
+ subagentId: c.conversationId,
12635
+ task: c.subagentMeta?.task ?? c.title,
12636
+ status: c.subagentMeta?.status ?? "stopped",
12637
+ messageCount: c.messages.length
12638
+ });
12639
+ }
12640
+ }
12641
+ return results;
12642
+ }
12643
+ };
12644
+ }
12645
+ // ── Stale subagent recovery ──
12646
+ async recoverStaleSubagents() {
12647
+ const allSummaries = await this.conversationStore.listSummaries();
12648
+ const subagentSummaries = allSummaries.filter((s) => s.parentConversationId);
12649
+ if (subagentSummaries.length === 0) return;
12650
+ const parentsToCallback = /* @__PURE__ */ new Set();
12651
+ const CONCURRENCY = 10;
12652
+ for (let i = 0; i < subagentSummaries.length; i += CONCURRENCY) {
12653
+ const batch = subagentSummaries.slice(i, i + CONCURRENCY);
12654
+ const convs = await Promise.all(batch.map((s) => this.conversationStore.get(s.conversationId)));
12655
+ for (const conv of convs) {
12656
+ if (conv?.subagentMeta?.status === "running" && conv.parentConversationId) {
12657
+ const lastActivity = conv.lastActivityAt ?? conv.updatedAt;
12658
+ const elapsed = Date.now() - lastActivity;
12659
+ if (elapsed < STALE_SUBAGENT_THRESHOLD_MS) continue;
12660
+ conv.subagentMeta.status = "error";
12661
+ conv.subagentMeta.error = { code: "STALE_SUBAGENT", message: `Subagent inactive for ${Math.round(elapsed / 1e3)}s (threshold: ${STALE_SUBAGENT_THRESHOLD_MS / 1e3}s)` };
12662
+ conv.updatedAt = Date.now();
12663
+ await this.conversationStore.update(conv);
12664
+ const pendingResult = {
12665
+ subagentId: conv.conversationId,
12666
+ task: conv.subagentMeta.task,
12667
+ status: "error",
12668
+ error: conv.subagentMeta.error,
12669
+ timestamp: Date.now()
12670
+ };
12671
+ await this.conversationStore.appendSubagentResult(conv.parentConversationId, pendingResult);
12672
+ parentsToCallback.add(conv.parentConversationId);
12673
+ }
12674
+ }
12675
+ }
12676
+ for (const parentId of parentsToCallback) {
12677
+ this.processSubagentCallback(parentId).catch(
12678
+ (err) => console.error(`[poncho][subagent] Recovery callback failed for ${parentId}:`, err instanceof Error ? err.message : err)
12679
+ );
12680
+ }
12681
+ }
12682
+ };
12683
+
10610
12684
  // src/index.ts
10611
12685
  import { defineTool as defineTool13 } from "@poncho-ai/sdk";
10612
12686
  export {
10613
12687
  AgentHarness,
12688
+ AgentOrchestrator,
10614
12689
  BashEnvironmentManager,
12690
+ CALLBACK_LOCK_STALE_MS,
10615
12691
  InMemoryConversationStore,
10616
12692
  InMemoryEngine,
10617
12693
  InMemoryStateStore,
10618
12694
  LocalMcpBridge,
10619
12695
  LocalUploadStore,
12696
+ MAX_CONCURRENT_SUBAGENTS,
12697
+ MAX_CONTINUATION_COUNT,
12698
+ MAX_SUBAGENT_CALLBACK_COUNT,
12699
+ MAX_SUBAGENT_NESTING,
10620
12700
  OPENAI_CODEX_CLIENT_ID,
10621
12701
  PONCHO_UPLOAD_SCHEME,
10622
12702
  PonchoFsAdapter,
10623
12703
  PostgresEngine,
10624
12704
  S3UploadStore,
12705
+ STALE_SUBAGENT_THRESHOLD_MS,
10625
12706
  STORAGE_SCHEMA_VERSION,
10626
12707
  SqliteEngine,
12708
+ TOOL_RESULT_ARCHIVE_PARAM2 as TOOL_RESULT_ARCHIVE_PARAM,
10627
12709
  TelemetryEmitter,
10628
12710
  ToolDispatcher,
10629
12711
  VFS_SCHEME,
10630
12712
  VercelBlobUploadStore,
12713
+ applyTurnMetadata,
10631
12714
  buildAgentDirectoryName,
12715
+ buildApprovalCheckpoints,
12716
+ buildAssistantMetadata,
10632
12717
  buildSkillContextWindow,
12718
+ buildToolCompletedText,
12719
+ cloneSections,
10633
12720
  compactMessages,
10634
12721
  completeOpenAICodexDeviceAuth,
12722
+ computeNextOccurrence,
10635
12723
  createBashTool,
10636
12724
  createConversationStore,
10637
12725
  createConversationStoreFromEngine,
@@ -10653,6 +12741,7 @@ export {
10653
12741
  createStorageEngine,
10654
12742
  createSubagentTools,
10655
12743
  createTodoStoreFromEngine,
12744
+ createTurnDraftState,
10656
12745
  createUploadStore,
10657
12746
  createWriteTool,
10658
12747
  defineTool13 as defineTool,
@@ -10661,7 +12750,9 @@ export {
10661
12750
  ensureAgentIdentity,
10662
12751
  estimateTokens,
10663
12752
  estimateTotalTokens,
12753
+ executeConversationTurn,
10664
12754
  findSafeSplitPoint,
12755
+ flushTurnDraft,
10665
12756
  generateAgentId,
10666
12757
  getAgentStoreDirectory,
10667
12758
  getModelContextWindow,
@@ -10669,11 +12760,15 @@ export {
10669
12760
  getOpenAICodexAuthFilePath,
10670
12761
  getOpenAICodexRequiredScopes,
10671
12762
  getPonchoStoreRoot,
12763
+ isMessageArray,
10672
12764
  jsonSchemaToZod,
12765
+ loadCanonicalHistory,
10673
12766
  loadPonchoConfig,
12767
+ loadRunHistory,
10674
12768
  loadSkillContext,
10675
12769
  loadSkillInstructions,
10676
12770
  loadSkillMetadata,
12771
+ normalizeApprovalCheckpoint,
10677
12772
  normalizeOtlp,
10678
12773
  normalizeScriptPolicyPath,
10679
12774
  parseAgentFile,
@@ -10681,15 +12776,18 @@ export {
10681
12776
  ponchoDocsTool,
10682
12777
  readOpenAICodexSession,
10683
12778
  readSkillResource,
12779
+ recordStandardTurnEvent,
10684
12780
  renderAgentPrompt,
10685
12781
  resolveAgentIdentity,
10686
12782
  resolveCompactionConfig,
10687
12783
  resolveEnv,
10688
12784
  resolveMemoryConfig,
12785
+ resolveRunRequest,
10689
12786
  resolveSkillDirs,
10690
12787
  resolveStateConfig,
10691
12788
  slugifyStorageComponent,
10692
12789
  startOpenAICodexDeviceAuth,
10693
12790
  verifyTenantToken,
12791
+ withToolResultArchiveParam,
10694
12792
  writeOpenAICodexSession
10695
12793
  };