agents 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -120,7 +120,7 @@ const DEFAULT_KEEP_ALIVE_INTERVAL_MS = 3e4;
120
120
  * The constructor stores this as a row in cf_agents_state and checks it
121
121
  * on wake to skip DDL on established DOs.
122
122
  */
123
- const CURRENT_SCHEMA_VERSION = 7;
123
+ const CURRENT_SCHEMA_VERSION = 8;
124
124
  const SCHEMA_VERSION_ROW_ID = "cf_schema_version";
125
125
  const STATE_ROW_ID = "cf_state_row_id";
126
126
  const STATE_WAS_CHANGED = "cf_state_was_changed";
@@ -549,6 +549,32 @@ var Agent = class Agent extends Server {
549
549
  this.sql`
550
550
  CREATE INDEX IF NOT EXISTS idx_facet_runs_owner_path_key
551
551
  ON cf_agents_facet_runs(owner_path_key)
552
+ `;
553
+ this.sql`
554
+ CREATE TABLE IF NOT EXISTS cf_agents_fibers (
555
+ fiber_id TEXT PRIMARY KEY,
556
+ idempotency_key TEXT UNIQUE,
557
+ name TEXT NOT NULL,
558
+ status TEXT NOT NULL,
559
+ snapshot TEXT,
560
+ metadata_json TEXT,
561
+ error_message TEXT,
562
+ created_at INTEGER NOT NULL,
563
+ started_at INTEGER,
564
+ completed_at INTEGER
565
+ )
566
+ `;
567
+ this.sql`
568
+ CREATE INDEX IF NOT EXISTS idx_fibers_status_created
569
+ ON cf_agents_fibers(status, created_at, fiber_id)
570
+ `;
571
+ this.sql`
572
+ CREATE INDEX IF NOT EXISTS idx_fibers_name_status_created
573
+ ON cf_agents_fibers(name, status, created_at, fiber_id)
574
+ `;
575
+ this.sql`
576
+ CREATE INDEX IF NOT EXISTS idx_fibers_status_completed
577
+ ON cf_agents_fibers(status, completed_at, created_at)
552
578
  `;
553
579
  this.sql`
554
580
  CREATE TABLE IF NOT EXISTS cf_agent_tool_runs (
@@ -595,6 +621,9 @@ var Agent = class Agent extends Server {
595
621
  this._keepAliveRefs = 0;
596
622
  this._facetKeepAliveTokens = /* @__PURE__ */ new Set();
597
623
  this._runFiberActiveFibers = /* @__PURE__ */ new Set();
624
+ this._managedFiberAbortControllers = /* @__PURE__ */ new Map();
625
+ this._managedFiberExecutions = /* @__PURE__ */ new Map();
626
+ this._managedFiberTerminalWaiters = /* @__PURE__ */ new Map();
598
627
  this._runFiberRecoveryInProgress = false;
599
628
  this._ParentClass = Object.getPrototypeOf(this).constructor;
600
629
  this.initialState = DEFAULT_STATE;
@@ -2211,6 +2240,295 @@ var Agent = class Agent extends Server {
2211
2240
  dispose();
2212
2241
  }
2213
2242
  }
2243
+ _isTerminalFiberStatus(status) {
2244
+ return status === "completed" || status === "aborted" || status === "interrupted" || status === "error";
2245
+ }
2246
+ _notifyManagedFiberTerminal(fiberId) {
2247
+ const row = this._readFiber(fiberId);
2248
+ if (row && !this._isTerminalFiberStatus(row.status)) return;
2249
+ const waiters = this._managedFiberTerminalWaiters.get(fiberId);
2250
+ if (!waiters) return;
2251
+ this._managedFiberTerminalWaiters.delete(fiberId);
2252
+ for (const resolve of waiters) resolve();
2253
+ }
2254
+ _waitForManagedFiberTerminal(fiberId) {
2255
+ const row = this._readFiber(fiberId);
2256
+ if (!row || this._isTerminalFiberStatus(row.status)) return Promise.resolve();
2257
+ return new Promise((resolve) => {
2258
+ let waiters = this._managedFiberTerminalWaiters.get(fiberId);
2259
+ if (!waiters) {
2260
+ waiters = /* @__PURE__ */ new Set();
2261
+ this._managedFiberTerminalWaiters.set(fiberId, waiters);
2262
+ }
2263
+ waiters.add(resolve);
2264
+ });
2265
+ }
2266
+ _normalizeFiberStatusFilter(status) {
2267
+ if (!status) return null;
2268
+ return new Set(Array.isArray(status) ? status : [status]);
2269
+ }
2270
+ _parseFiberJsonObject(value) {
2271
+ if (value === null) return null;
2272
+ try {
2273
+ const parsed = JSON.parse(value);
2274
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
2275
+ } catch {}
2276
+ return null;
2277
+ }
2278
+ _parseFiberSnapshot(value) {
2279
+ if (value === null) return void 0;
2280
+ try {
2281
+ return JSON.parse(value);
2282
+ } catch {
2283
+ return;
2284
+ }
2285
+ }
2286
+ _fiberErrorMessage(error) {
2287
+ return error instanceof Error ? error.message : String(error);
2288
+ }
2289
+ _stringifyFiberSnapshot(snapshot) {
2290
+ return snapshot === void 0 ? null : JSON.stringify(snapshot);
2291
+ }
2292
+ _fiberRecoveryErrorMessage(result) {
2293
+ if (result.status === "error") return result.error === void 0 ? null : this._fiberErrorMessage(result.error);
2294
+ if (result.status === "aborted" || result.status === "interrupted") return result.reason ?? null;
2295
+ return null;
2296
+ }
2297
+ _applyManagedFiberRecoveryResult(fiberId, result) {
2298
+ const completedAt = Date.now();
2299
+ const snapshot = this._stringifyFiberSnapshot(result.snapshot);
2300
+ const errorMessage = this._fiberRecoveryErrorMessage(result);
2301
+ const metadata = result.status === "completed" && result.metadata !== void 0 ? JSON.stringify(result.metadata) : void 0;
2302
+ if (metadata !== void 0) {
2303
+ this.sql`
2304
+ UPDATE cf_agents_fibers
2305
+ SET status = ${result.status},
2306
+ snapshot = COALESCE(${snapshot}, snapshot),
2307
+ metadata_json = ${metadata},
2308
+ error_message = ${errorMessage},
2309
+ completed_at = ${completedAt}
2310
+ WHERE fiber_id = ${fiberId}
2311
+ AND status = 'interrupted'
2312
+ `;
2313
+ this._notifyManagedFiberTerminal(fiberId);
2314
+ return;
2315
+ }
2316
+ this.sql`
2317
+ UPDATE cf_agents_fibers
2318
+ SET status = ${result.status},
2319
+ snapshot = COALESCE(${snapshot}, snapshot),
2320
+ error_message = ${errorMessage},
2321
+ completed_at = ${completedAt}
2322
+ WHERE fiber_id = ${fiberId}
2323
+ AND status = 'interrupted'
2324
+ `;
2325
+ this._notifyManagedFiberTerminal(fiberId);
2326
+ }
2327
+ _settleManagedFiberExecution(fiberId, outcome, signal) {
2328
+ const completedAt = Date.now();
2329
+ if (outcome.ok) {
2330
+ this.sql`
2331
+ UPDATE cf_agents_fibers
2332
+ SET status = 'completed', completed_at = ${completedAt}
2333
+ WHERE fiber_id = ${fiberId} AND status = 'running'
2334
+ `;
2335
+ this._notifyManagedFiberTerminal(fiberId);
2336
+ return;
2337
+ }
2338
+ const message = this._fiberErrorMessage(outcome.error);
2339
+ const status = signal.aborted ? "aborted" : "error";
2340
+ this.sql`
2341
+ UPDATE cf_agents_fibers
2342
+ SET status = ${status},
2343
+ error_message = ${message},
2344
+ completed_at = ${completedAt}
2345
+ WHERE fiber_id = ${fiberId} AND status = 'running'
2346
+ `;
2347
+ this._notifyManagedFiberTerminal(fiberId);
2348
+ }
2349
+ _parseFiberRecoverySnapshot(fiberId, snapshotText) {
2350
+ if (!snapshotText) return null;
2351
+ try {
2352
+ return JSON.parse(snapshotText);
2353
+ } catch {
2354
+ console.warn(`[Agent] Corrupted snapshot for fiber ${fiberId}, treating as null`);
2355
+ return null;
2356
+ }
2357
+ }
2358
+ async _runFiberRecoveryHook(ctx, managedRow) {
2359
+ try {
2360
+ if (!await this._handleInternalFiberRecovery(ctx)) {
2361
+ const recoveryResult = await this.onFiberRecovered(ctx);
2362
+ if (managedRow && recoveryResult) this._applyManagedFiberRecoveryResult(ctx.id, recoveryResult);
2363
+ }
2364
+ } catch (e) {
2365
+ if (managedRow) this.sql`
2366
+ UPDATE cf_agents_fibers
2367
+ SET error_message = ${this._fiberErrorMessage(e)}
2368
+ WHERE fiber_id = ${ctx.id}
2369
+ AND status = 'interrupted'
2370
+ `;
2371
+ console.error(`[Agent] Fiber recovery failed for "${ctx.name}" (${ctx.id}):`, e);
2372
+ }
2373
+ }
2374
+ _fiberInspectionFromRow(row) {
2375
+ const snapshot = this._parseFiberSnapshot(row.snapshot);
2376
+ const inspection = {
2377
+ fiberId: row.fiber_id,
2378
+ name: row.name,
2379
+ status: row.status,
2380
+ createdAt: row.created_at
2381
+ };
2382
+ if (row.idempotency_key !== null) inspection.idempotencyKey = row.idempotency_key;
2383
+ if (snapshot !== void 0) inspection.snapshot = snapshot;
2384
+ if (row.error_message !== null) inspection.error = row.error_message;
2385
+ const metadata = this._parseFiberJsonObject(row.metadata_json);
2386
+ if (metadata !== null) inspection.metadata = metadata;
2387
+ if (row.started_at !== null) inspection.startedAt = row.started_at;
2388
+ if (row.completed_at !== null) inspection.settledAt = row.completed_at;
2389
+ return inspection;
2390
+ }
2391
+ async _waitForManagedFiber(fiberId) {
2392
+ const row = this._readFiber(fiberId);
2393
+ if (!row || this._isTerminalFiberStatus(row.status)) return row ? this._fiberInspectionFromRow(row) : null;
2394
+ if (this._managedFiberExecutions.has(fiberId)) {
2395
+ await this._waitForManagedFiberTerminal(fiberId);
2396
+ return this.inspectFiber(fiberId);
2397
+ }
2398
+ await this._checkRunFibers();
2399
+ await this._waitForManagedFiberTerminal(fiberId);
2400
+ return this.inspectFiber(fiberId);
2401
+ }
2402
+ _readFiber(fiberId) {
2403
+ return this.sql`
2404
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2405
+ error_message, created_at, started_at, completed_at
2406
+ FROM cf_agents_fibers
2407
+ WHERE fiber_id = ${fiberId}
2408
+ LIMIT 1
2409
+ `[0] ?? null;
2410
+ }
2411
+ _readFiberByKey(idempotencyKey) {
2412
+ return this.sql`
2413
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2414
+ error_message, created_at, started_at, completed_at
2415
+ FROM cf_agents_fibers
2416
+ WHERE idempotency_key = ${idempotencyKey}
2417
+ LIMIT 1
2418
+ `[0] ?? null;
2419
+ }
2420
+ _listFiberRows(options) {
2421
+ const limit = Math.min(Math.max(options?.limit ?? 50, 1), 100);
2422
+ const statuses = this._normalizeFiberStatusFilter(options?.status);
2423
+ if (statuses) return [...statuses].flatMap((status) => this._listFiberRowsByStatus(status, limit, options?.name)).sort((a, b) => b.created_at === a.created_at ? b.fiber_id.localeCompare(a.fiber_id) : b.created_at - a.created_at).slice(0, limit);
2424
+ if (options?.name) return this.sql`
2425
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2426
+ error_message, created_at, started_at, completed_at
2427
+ FROM cf_agents_fibers
2428
+ WHERE name = ${options.name}
2429
+ ORDER BY created_at DESC, fiber_id DESC
2430
+ LIMIT ${limit}
2431
+ `;
2432
+ return this.sql`
2433
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2434
+ error_message, created_at, started_at, completed_at
2435
+ FROM cf_agents_fibers
2436
+ ORDER BY created_at DESC, fiber_id DESC
2437
+ LIMIT ${limit}
2438
+ `;
2439
+ }
2440
+ _listFiberRowsByStatus(status, limit, name) {
2441
+ if (name) return this.sql`
2442
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2443
+ error_message, created_at, started_at, completed_at
2444
+ FROM cf_agents_fibers
2445
+ WHERE status = ${status} AND name = ${name}
2446
+ ORDER BY created_at DESC, fiber_id DESC
2447
+ LIMIT ${limit}
2448
+ `;
2449
+ return this.sql`
2450
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2451
+ error_message, created_at, started_at, completed_at
2452
+ FROM cf_agents_fibers
2453
+ WHERE status = ${status}
2454
+ ORDER BY created_at DESC, fiber_id DESC
2455
+ LIMIT ${limit}
2456
+ `;
2457
+ }
2458
+ async inspectFiber(fiberId) {
2459
+ const row = this._readFiber(fiberId);
2460
+ return row ? this._fiberInspectionFromRow(row) : null;
2461
+ }
2462
+ async inspectFiberByKey(idempotencyKey) {
2463
+ const row = this._readFiberByKey(idempotencyKey);
2464
+ return row ? this._fiberInspectionFromRow(row) : null;
2465
+ }
2466
+ async listFibers(options) {
2467
+ return this._listFiberRows(options).map((row) => this._fiberInspectionFromRow(row));
2468
+ }
2469
+ async cancelFiber(fiberId, reason) {
2470
+ const row = this._readFiber(fiberId);
2471
+ if (!row || this._isTerminalFiberStatus(row.status)) return false;
2472
+ const now = Date.now();
2473
+ this.sql`
2474
+ UPDATE cf_agents_fibers
2475
+ SET status = 'aborted',
2476
+ error_message = ${reason ?? null},
2477
+ completed_at = ${now}
2478
+ WHERE fiber_id = ${fiberId}
2479
+ AND status IN ('pending', 'running')
2480
+ `;
2481
+ this._managedFiberAbortControllers.get(fiberId)?.abort(reason);
2482
+ this._notifyManagedFiberTerminal(fiberId);
2483
+ return true;
2484
+ }
2485
+ async cancelFiberByKey(idempotencyKey, reason) {
2486
+ const row = this._readFiberByKey(idempotencyKey);
2487
+ return row ? this.cancelFiber(row.fiber_id, reason) : false;
2488
+ }
2489
+ async resolveFiber(fiberId, result) {
2490
+ const row = this._readFiber(fiberId);
2491
+ if (!row || row.status !== "interrupted") return false;
2492
+ this._applyManagedFiberRecoveryResult(fiberId, result);
2493
+ return true;
2494
+ }
2495
+ async deleteFibers(options) {
2496
+ const terminalStatuses = [...this._normalizeFiberStatusFilter(options?.status) ?? new Set([
2497
+ "completed",
2498
+ "aborted",
2499
+ "error"
2500
+ ])].filter((status) => this._isTerminalFiberStatus(status));
2501
+ if (terminalStatuses.length === 0) return 0;
2502
+ const limit = Math.min(Math.max(options?.limit ?? 100, 1), 500);
2503
+ const settledBefore = options?.settledBefore?.getTime();
2504
+ const rows = terminalStatuses.flatMap((status) => this._listTerminalFiberRowsForDelete(status, limit, settledBefore)).sort((a, b) => a.completed_at === b.completed_at ? a.created_at - b.created_at : (a.completed_at ?? 0) - (b.completed_at ?? 0)).slice(0, limit);
2505
+ for (const row of rows) this.sql`
2506
+ DELETE FROM cf_agents_fibers
2507
+ WHERE fiber_id = ${row.fiber_id}
2508
+ AND status IN ('completed', 'aborted', 'interrupted', 'error')
2509
+ `;
2510
+ return rows.length;
2511
+ }
2512
+ _listTerminalFiberRowsForDelete(status, limit, settledBefore) {
2513
+ if (settledBefore !== void 0) return this.sql`
2514
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2515
+ error_message, created_at, started_at, completed_at
2516
+ FROM cf_agents_fibers
2517
+ WHERE status = ${status}
2518
+ AND completed_at IS NOT NULL
2519
+ AND completed_at < ${settledBefore}
2520
+ ORDER BY completed_at ASC, created_at ASC
2521
+ LIMIT ${limit}
2522
+ `;
2523
+ return this.sql`
2524
+ SELECT fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2525
+ error_message, created_at, started_at, completed_at
2526
+ FROM cf_agents_fibers
2527
+ WHERE status = ${status}
2528
+ ORDER BY completed_at ASC, created_at ASC
2529
+ LIMIT ${limit}
2530
+ `;
2531
+ }
2214
2532
  /**
2215
2533
  * Run a function as a durable fiber. The fiber is registered in SQLite
2216
2534
  * before execution, checkpointable during execution via `ctx.stash()`,
@@ -2225,7 +2543,100 @@ var Agent = class Agent extends Server {
2225
2543
  * @returns The return value of fn
2226
2544
  */
2227
2545
  async runFiber(name, fn) {
2228
- const id = nanoid();
2546
+ return this._runFiberInternal(nanoid(), name, fn);
2547
+ }
2548
+ async startFiber(name, fn, options) {
2549
+ const fiberId = options?.fiberId ?? nanoid();
2550
+ const idempotencyKey = options?.idempotencyKey;
2551
+ if (options?.fiberId !== void 0 && options.fiberId.trim() === "") throw new Error("fiberId must not be blank");
2552
+ if (options?.idempotencyKey !== void 0 && options.idempotencyKey.trim() === "") throw new Error("idempotencyKey must not be blank");
2553
+ const existingById = this._readFiber(fiberId);
2554
+ const existingByKey = idempotencyKey ? this._readFiberByKey(idempotencyKey) : null;
2555
+ if (existingById && existingByKey && existingById.fiber_id !== existingByKey.fiber_id) throw new Error("fiberId and idempotencyKey refer to different fibers");
2556
+ if (existingByKey && options?.fiberId && existingByKey.fiber_id !== fiberId) throw new Error("fiberId and idempotencyKey refer to different fibers");
2557
+ const existing = existingById ?? existingByKey;
2558
+ if (existing) {
2559
+ if (options?.waitForCompletion && !this._isTerminalFiberStatus(existing.status)) {
2560
+ const waited = await this._waitForManagedFiber(existing.fiber_id);
2561
+ if (waited) return {
2562
+ ...waited,
2563
+ accepted: false
2564
+ };
2565
+ throw new Error(`Fiber ${existing.fiber_id} no longer exists`);
2566
+ }
2567
+ return {
2568
+ ...this._fiberInspectionFromRow(existing),
2569
+ accepted: false
2570
+ };
2571
+ }
2572
+ const now = Date.now();
2573
+ this.sql`
2574
+ INSERT INTO cf_agents_fibers
2575
+ (fiber_id, idempotency_key, name, status, snapshot, metadata_json,
2576
+ error_message, created_at, started_at, completed_at)
2577
+ VALUES
2578
+ (${fiberId}, ${idempotencyKey ?? null}, ${name}, 'pending', NULL,
2579
+ ${options?.metadata ? JSON.stringify(options.metadata) : null}, NULL,
2580
+ ${now}, NULL, NULL)
2581
+ `;
2582
+ const row = this._readFiber(fiberId);
2583
+ if (!row) throw new Error(`Failed to create fiber ${fiberId}`);
2584
+ const execution = this._executeManagedFiber(fiberId, name, fn).catch((error) => {
2585
+ console.error(`[Agent] Managed fiber "${name}" (${fiberId}) failed:`, error);
2586
+ }).finally(() => {
2587
+ if (this._managedFiberExecutions.get(fiberId) === execution) this._managedFiberExecutions.delete(fiberId);
2588
+ });
2589
+ this._managedFiberExecutions.set(fiberId, execution);
2590
+ if (options?.waitForCompletion) {
2591
+ const completed = await this._waitForManagedFiber(fiberId);
2592
+ if (!completed) throw new Error(`Fiber ${fiberId} no longer exists`);
2593
+ return {
2594
+ ...completed,
2595
+ accepted: true
2596
+ };
2597
+ }
2598
+ return {
2599
+ ...this._fiberInspectionFromRow(row),
2600
+ accepted: true
2601
+ };
2602
+ }
2603
+ async _executeManagedFiber(fiberId, name, fn) {
2604
+ const row = this._readFiber(fiberId);
2605
+ if (!row || row.status !== "pending") return;
2606
+ const controller = new AbortController();
2607
+ this._managedFiberAbortControllers.set(fiberId, controller);
2608
+ const now = Date.now();
2609
+ this.sql`
2610
+ UPDATE cf_agents_fibers
2611
+ SET status = 'running', started_at = ${now}
2612
+ WHERE fiber_id = ${fiberId} AND status = 'pending'
2613
+ `;
2614
+ const updated = this._readFiber(fiberId);
2615
+ if (!updated || updated.status !== "running") {
2616
+ this._managedFiberAbortControllers.delete(fiberId);
2617
+ return;
2618
+ }
2619
+ let settled = false;
2620
+ try {
2621
+ await this._runFiberInternal(fiberId, name, fn, {
2622
+ signal: controller.signal,
2623
+ managed: true,
2624
+ beforeRunCleanup: (outcome) => {
2625
+ settled = true;
2626
+ this._settleManagedFiberExecution(fiberId, outcome, controller.signal);
2627
+ }
2628
+ });
2629
+ } catch (error) {
2630
+ if (!settled) this._settleManagedFiberExecution(fiberId, {
2631
+ ok: false,
2632
+ error
2633
+ }, controller.signal);
2634
+ } finally {
2635
+ this._managedFiberAbortControllers.delete(fiberId);
2636
+ }
2637
+ }
2638
+ async _runFiberInternal(id, name, fn, options) {
2639
+ const signal = options?.signal ?? new AbortController().signal;
2229
2640
  this.sql`
2230
2641
  INSERT INTO cf_agents_runs (id, name, snapshot, created_at)
2231
2642
  VALUES (${id}, ${name}, NULL, ${Date.now()})
@@ -2242,19 +2653,36 @@ var Agent = class Agent extends Server {
2242
2653
  }
2243
2654
  dispose = await this.keepAlive();
2244
2655
  const stash = (data) => {
2656
+ const snapshot = JSON.stringify(data);
2245
2657
  this.sql`
2246
- UPDATE cf_agents_runs SET snapshot = ${JSON.stringify(data)}
2658
+ UPDATE cf_agents_runs SET snapshot = ${snapshot}
2247
2659
  WHERE id = ${id}
2248
2660
  `;
2661
+ if (options?.managed) this.sql`
2662
+ UPDATE cf_agents_fibers SET snapshot = ${snapshot}
2663
+ WHERE fiber_id = ${id}
2664
+ `;
2249
2665
  };
2250
- return await _fiberALS.run({
2251
- id,
2252
- stash
2253
- }, () => fn({
2254
- id,
2255
- stash,
2256
- snapshot: null
2257
- }));
2666
+ try {
2667
+ const result = await _fiberALS.run({
2668
+ id,
2669
+ signal,
2670
+ stash
2671
+ }, () => fn({
2672
+ id,
2673
+ signal,
2674
+ stash,
2675
+ snapshot: null
2676
+ }));
2677
+ options?.beforeRunCleanup?.({ ok: true });
2678
+ return result;
2679
+ } catch (error) {
2680
+ options?.beforeRunCleanup?.({
2681
+ ok: false,
2682
+ error
2683
+ });
2684
+ throw error;
2685
+ }
2258
2686
  } finally {
2259
2687
  this._runFiberActiveFibers.delete(id);
2260
2688
  this.sql`DELETE FROM cf_agents_runs WHERE id = ${id}`;
@@ -2306,24 +2734,67 @@ var Agent = class Agent extends Server {
2306
2734
  const rows = this.sql`SELECT id, name, snapshot, created_at FROM cf_agents_runs`;
2307
2735
  for (const row of rows) {
2308
2736
  if (this._runFiberActiveFibers.has(row.id)) continue;
2309
- let snapshot = null;
2310
- if (row.snapshot) try {
2311
- snapshot = JSON.parse(row.snapshot);
2312
- } catch {
2313
- console.warn(`[Agent] Corrupted snapshot for fiber ${row.id}, treating as null`);
2314
- }
2737
+ const snapshot = this._parseFiberRecoverySnapshot(row.id, row.snapshot);
2315
2738
  const ctx = {
2316
2739
  id: row.id,
2317
2740
  name: row.name,
2318
2741
  snapshot,
2319
2742
  createdAt: row.created_at
2320
2743
  };
2321
- try {
2322
- if (!await this._handleInternalFiberRecovery(ctx)) await this.onFiberRecovered(ctx);
2323
- } catch (e) {
2324
- console.error(`[Agent] Fiber recovery failed for "${ctx.name}" (${ctx.id}):`, e);
2744
+ const managedRow = this._readFiber(row.id);
2745
+ if (managedRow) {
2746
+ if (this._isTerminalFiberStatus(managedRow.status)) {
2747
+ this.sql`DELETE FROM cf_agents_runs WHERE id = ${row.id}`;
2748
+ this._notifyManagedFiberTerminal(row.id);
2749
+ continue;
2750
+ }
2751
+ const completedAt = Date.now();
2752
+ this.sql`
2753
+ UPDATE cf_agents_fibers
2754
+ SET status = 'interrupted',
2755
+ snapshot = ${row.snapshot},
2756
+ completed_at = ${completedAt}
2757
+ WHERE fiber_id = ${row.id}
2758
+ AND status IN ('pending', 'running')
2759
+ `;
2760
+ ctx.idempotencyKey = managedRow.idempotency_key ?? void 0;
2761
+ ctx.metadata = this._parseFiberJsonObject(managedRow.metadata_json);
2762
+ ctx.status = "interrupted";
2325
2763
  }
2764
+ await this._runFiberRecoveryHook(ctx, managedRow);
2326
2765
  this.sql`DELETE FROM cf_agents_runs WHERE id = ${row.id}`;
2766
+ if (managedRow) this._notifyManagedFiberTerminal(row.id);
2767
+ }
2768
+ const ledgerOnlyRows = this.sql`
2769
+ SELECT f.fiber_id, f.idempotency_key, f.name, f.status, f.snapshot,
2770
+ f.metadata_json, f.error_message, f.created_at, f.started_at,
2771
+ f.completed_at
2772
+ FROM cf_agents_fibers f
2773
+ LEFT JOIN cf_agents_runs r ON r.id = f.fiber_id
2774
+ WHERE f.status IN ('pending', 'running')
2775
+ AND r.id IS NULL
2776
+ `;
2777
+ for (const row of ledgerOnlyRows) {
2778
+ if (this._runFiberActiveFibers.has(row.fiber_id)) continue;
2779
+ const snapshot = this._parseFiberRecoverySnapshot(row.fiber_id, row.snapshot);
2780
+ const completedAt = Date.now();
2781
+ this.sql`
2782
+ UPDATE cf_agents_fibers
2783
+ SET status = 'interrupted',
2784
+ completed_at = ${completedAt}
2785
+ WHERE fiber_id = ${row.fiber_id}
2786
+ AND status IN ('pending', 'running')
2787
+ `;
2788
+ await this._runFiberRecoveryHook({
2789
+ id: row.fiber_id,
2790
+ name: row.name,
2791
+ snapshot,
2792
+ createdAt: row.created_at,
2793
+ idempotencyKey: row.idempotency_key ?? void 0,
2794
+ metadata: this._parseFiberJsonObject(row.metadata_json),
2795
+ status: "interrupted"
2796
+ }, row);
2797
+ this._notifyManagedFiberTerminal(row.fiber_id);
2327
2798
  }
2328
2799
  } finally {
2329
2800
  this._runFiberRecoveryInProgress = false;
@@ -4021,6 +4492,7 @@ var Agent = class Agent extends Server {
4021
4492
  this.sql`DROP TABLE IF EXISTS cf_agents_workflows`;
4022
4493
  this.sql`DROP TABLE IF EXISTS cf_agents_sub_agents`;
4023
4494
  this.sql`DROP TABLE IF EXISTS cf_agents_runs`;
4495
+ this.sql`DROP TABLE IF EXISTS cf_agents_fibers`;
4024
4496
  this.sql`DROP TABLE IF EXISTS cf_agents_facet_runs`;
4025
4497
  this.sql`DROP TABLE IF EXISTS cf_agent_tool_runs`;
4026
4498
  }