@slock-ai/daemon 0.57.0 → 0.57.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.
@@ -2235,6 +2235,412 @@ function reduceApmGatedFlushReadiness(state, input) {
2235
2235
  };
2236
2236
  }
2237
2237
 
2238
+ // src/agentInboxStateMachine.ts
2239
+ var DEFAULT_HELD_CONTEXT_LIMIT = 3;
2240
+ function planAgentInboxSideEffect(input) {
2241
+ const heldContextLimit = input.heldContextLimit ?? DEFAULT_HELD_CONTEXT_LIMIT;
2242
+ const trace = [
2243
+ {
2244
+ step: "input",
2245
+ data: compactTraceData({
2246
+ action: input.action,
2247
+ target: input.target,
2248
+ continueAnyway: input.continueAnyway,
2249
+ pendingCount: input.pendingMessages.length,
2250
+ recentCount: input.recentMessages.length,
2251
+ existingSeenUpToSeq: input.existingSeenUpToSeq,
2252
+ modelSeenSeq: input.modelSeenSeq
2253
+ })
2254
+ }
2255
+ ];
2256
+ if (input.continueAnyway) {
2257
+ appendTrace(trace, "continue_anyway_bypass");
2258
+ return forwardPlan(input, {
2259
+ action: input.action,
2260
+ decision: "bypass",
2261
+ target: input.target,
2262
+ inboxTrustState: "trusted",
2263
+ reason: "continue_anyway"
2264
+ }, trace);
2265
+ }
2266
+ if (input.pendingMessages.length > 0) {
2267
+ appendTrace(trace, "pending_messages_found", { pendingCount: input.pendingMessages.length });
2268
+ const pending = sortInboxMessagesBySeq(normalizeInboxVisibleMessages(input.pendingMessages, input.target));
2269
+ const boundary2 = resolveFreshnessBoundary(pending);
2270
+ if (!boundary2.ok) {
2271
+ appendTrace(trace, "pending_context_missing_boundary");
2272
+ return forwardWithoutDecision(input, trace);
2273
+ }
2274
+ const alreadySeenPending = [];
2275
+ const unconsumedMessages = [];
2276
+ for (const message of pending) {
2277
+ if (isMessageModelSeen(input, message)) {
2278
+ alreadySeenPending.push(message);
2279
+ } else {
2280
+ unconsumedMessages.push(message);
2281
+ }
2282
+ }
2283
+ appendTrace(trace, "pending_context_classified", {
2284
+ pendingCount: pending.length,
2285
+ unseenCount: unconsumedMessages.length
2286
+ });
2287
+ if (unconsumedMessages.length === 0) {
2288
+ const contiguousBoundary = maxKnownContiguousBoundary(input);
2289
+ const canAdvanceBoundary = typeof contiguousBoundary === "number" && contiguousBoundary >= boundary2.seenUpToSeq;
2290
+ appendTrace(trace, "pending_context_already_seen", { boundarySeq: boundary2.seenUpToSeq });
2291
+ return forwardPlan(input, {
2292
+ action: input.action,
2293
+ decision: "forward",
2294
+ target: input.target,
2295
+ inboxTrustState: "trusted",
2296
+ reason: "exact_target_pending_already_seen",
2297
+ pendingCount: pending.length,
2298
+ pendingMaxSeq: boundary2.seenUpToSeq,
2299
+ modelSeenSeq: contiguousBoundary,
2300
+ heldMessageCount: 0,
2301
+ omittedMessageCount: 0
2302
+ }, trace, {
2303
+ forwardSeenUpToSeq: input.action === "send" && canAdvanceBoundary ? boundary2.seenUpToSeq : void 0,
2304
+ consumeEffect: {
2305
+ type: "consume_visible_messages",
2306
+ target: input.target,
2307
+ messages: exactSeenConsumeMessages(input, pending),
2308
+ boundarySeq: canAdvanceBoundary ? boundary2.seenUpToSeq : void 0,
2309
+ source: "side_effect_preflight_context"
2310
+ }
2311
+ });
2312
+ }
2313
+ const heldBoundary = resolveFreshnessBoundary(unconsumedMessages);
2314
+ if (heldBoundary.ok) {
2315
+ const heldMessages = latestVisibleMessages(unconsumedMessages, heldContextLimit);
2316
+ const omittedMessageCount = Math.max(0, unconsumedMessages.length - heldMessages.length);
2317
+ const context = {
2318
+ heldMessages,
2319
+ newMessageCount: unconsumedMessages.length,
2320
+ shownMessageCount: heldMessages.length,
2321
+ omittedMessageCount,
2322
+ seenUpToSeq: heldBoundary.seenUpToSeq
2323
+ };
2324
+ appendTrace(trace, "held_context_built", {
2325
+ boundarySeq: boundary2.seenUpToSeq,
2326
+ heldBoundarySeq: heldBoundary.seenUpToSeq,
2327
+ heldCount: context.shownMessageCount,
2328
+ omittedCount: context.omittedMessageCount
2329
+ });
2330
+ return heldPlan(input, {
2331
+ decision: {
2332
+ action: input.action,
2333
+ decision: "local_hold",
2334
+ target: input.target,
2335
+ inboxTrustState: "trusted",
2336
+ reason: "exact_target_pending",
2337
+ pendingCount: input.pendingMessages.length,
2338
+ pendingMaxSeq: heldBoundary.seenUpToSeq,
2339
+ modelSeenSeq: input.modelSeenSeq,
2340
+ heldMessageCount: context.shownMessageCount,
2341
+ omittedMessageCount: context.omittedMessageCount
2342
+ },
2343
+ context,
2344
+ consumeMessages: sortInboxMessagesBySeq([
2345
+ ...exactSeenConsumeMessages(input, alreadySeenPending),
2346
+ ...context.heldMessages
2347
+ ]),
2348
+ consumeBoundarySeq: heldBoundary.seenUpToSeq
2349
+ }, trace);
2350
+ }
2351
+ appendTrace(trace, "pending_unseen_context_missing_boundary");
2352
+ return forwardWithoutDecision(input, trace);
2353
+ }
2354
+ const boundary = Math.max(input.existingSeenUpToSeq ?? 0, input.modelSeenSeq ?? 0);
2355
+ appendTrace(trace, "model_boundary_checked", { boundary });
2356
+ if (boundary > 0) {
2357
+ appendTrace(trace, "model_boundary_selected", { boundary });
2358
+ return forwardPlan(input, {
2359
+ action: input.action,
2360
+ decision: "forward",
2361
+ target: input.target,
2362
+ inboxTrustState: "trusted",
2363
+ reason: "model_seen_boundary",
2364
+ pendingCount: 0,
2365
+ modelSeenSeq: boundary
2366
+ }, trace, { forwardSeenUpToSeq: input.action === "send" ? boundary : void 0 });
2367
+ }
2368
+ if (input.recentMessages.length > 0) {
2369
+ return planFirstTouchRecentContext(input, heldContextLimit, trace);
2370
+ }
2371
+ appendTrace(trace, "no_context_available");
2372
+ return forwardPlan(input, {
2373
+ action: input.action,
2374
+ decision: "forward",
2375
+ target: input.target,
2376
+ inboxTrustState: "trusted",
2377
+ reason: "no_exact_target_pending_or_recent_context",
2378
+ pendingCount: 0,
2379
+ modelSeenSeq: 0
2380
+ }, trace);
2381
+ }
2382
+ function normalizeInboxVisibleMessage(message, target) {
2383
+ const targetFields = target ? parseTargetFields(target) : {};
2384
+ const normalized = {
2385
+ ...targetFields,
2386
+ ...message,
2387
+ message_id: message.message_id ?? message.id,
2388
+ timestamp: message.timestamp ?? message.createdAt,
2389
+ sender_type: message.sender_type ?? message.senderType,
2390
+ sender_name: message.sender_name ?? message.senderName,
2391
+ sender_description: message.sender_description ?? message.senderDescription ?? null
2392
+ };
2393
+ const senderId = messageSenderId(message);
2394
+ if (senderId) normalized.sender_id = senderId;
2395
+ return normalized;
2396
+ }
2397
+ function normalizeInboxVisibleMessages(messages, target) {
2398
+ return messages.map((message) => normalizeInboxVisibleMessage(message, target));
2399
+ }
2400
+ function maxInboxMessageSeq(messages) {
2401
+ let maxSeq = 0;
2402
+ for (const message of messages) {
2403
+ const seq = Math.floor(messageSeq(message));
2404
+ if (Number.isFinite(seq) && seq > 0) maxSeq = Math.max(maxSeq, seq);
2405
+ }
2406
+ return maxSeq > 0 ? maxSeq : void 0;
2407
+ }
2408
+ function maxKnownContiguousBoundary(input) {
2409
+ const boundary = Math.max(input.existingSeenUpToSeq ?? 0, input.modelSeenSeq ?? 0);
2410
+ return boundary > 0 ? boundary : void 0;
2411
+ }
2412
+ function exactSeenConsumeMessages(input, messages) {
2413
+ const boundary = maxKnownContiguousBoundary(input);
2414
+ return messages.map((message) => {
2415
+ const seq = Math.floor(messageSeq(message));
2416
+ if (Number.isFinite(seq) && seq > 0 && typeof boundary === "number" && boundary >= seq) {
2417
+ return message;
2418
+ }
2419
+ const id = typeof message.message_id === "string" && message.message_id.length > 0 ? message.message_id : typeof message.id === "string" && message.id.length > 0 ? message.id : void 0;
2420
+ return id ? { ...message, seq: void 0 } : message;
2421
+ });
2422
+ }
2423
+ function sortInboxMessagesBySeq(messages) {
2424
+ return [...messages].sort((a, b) => messageSeq(a) - messageSeq(b));
2425
+ }
2426
+ function planFirstTouchRecentContext(input, heldContextLimit, trace) {
2427
+ const recent = sortInboxMessagesBySeq(normalizeInboxVisibleMessages(input.recentMessages, input.target));
2428
+ const unconsumedMessages = recent.filter((message) => !isMessageModelSeen(input, message));
2429
+ appendTrace(trace, "recent_context_loaded", {
2430
+ recentCount: recent.length,
2431
+ unseenCount: unconsumedMessages.length
2432
+ });
2433
+ const boundary = resolveFreshnessBoundary(recent);
2434
+ if (!boundary.ok) {
2435
+ appendTrace(trace, "recent_context_missing_boundary");
2436
+ return forwardPlan(input, {
2437
+ action: input.action,
2438
+ decision: "forward",
2439
+ target: input.target,
2440
+ inboxTrustState: "untrusted",
2441
+ reason: "target_first_touch_recent_context_without_seq_boundary",
2442
+ pendingCount: 0,
2443
+ modelSeenSeq: 0
2444
+ }, trace);
2445
+ }
2446
+ appendTrace(trace, "recent_boundary_resolved", { boundarySeq: boundary.seenUpToSeq });
2447
+ if (unconsumedMessages.length === 0) {
2448
+ appendTrace(trace, "recent_context_already_seen");
2449
+ return forwardPlan(input, {
2450
+ action: input.action,
2451
+ decision: "forward",
2452
+ target: input.target,
2453
+ inboxTrustState: "untrusted",
2454
+ reason: "target_first_touch_recent_context_already_seen",
2455
+ pendingCount: 0,
2456
+ pendingMaxSeq: boundary.seenUpToSeq,
2457
+ modelSeenSeq: boundary.seenUpToSeq,
2458
+ heldMessageCount: 0,
2459
+ omittedMessageCount: 0
2460
+ }, trace, {
2461
+ forwardSeenUpToSeq: input.action === "send" ? boundary.seenUpToSeq : void 0,
2462
+ consumeEffect: {
2463
+ type: "consume_visible_messages",
2464
+ target: input.target,
2465
+ messages: recent,
2466
+ boundarySeq: boundary.seenUpToSeq,
2467
+ source: "side_effect_preflight_context"
2468
+ }
2469
+ });
2470
+ }
2471
+ const heldBoundary = resolveFreshnessBoundary(unconsumedMessages);
2472
+ if (!heldBoundary.ok) {
2473
+ appendTrace(trace, "unseen_context_missing_boundary");
2474
+ return forwardPlan(input, {
2475
+ action: input.action,
2476
+ decision: "forward",
2477
+ target: input.target,
2478
+ inboxTrustState: "untrusted",
2479
+ reason: "target_first_touch_unseen_context_without_seq_boundary",
2480
+ pendingCount: 0,
2481
+ modelSeenSeq: 0
2482
+ }, trace);
2483
+ }
2484
+ appendTrace(trace, "unseen_boundary_resolved", { boundarySeq: heldBoundary.seenUpToSeq });
2485
+ const heldMessages = latestVisibleMessages(unconsumedMessages, heldContextLimit);
2486
+ const omittedMessageCount = Math.max(0, unconsumedMessages.length - heldMessages.length);
2487
+ appendTrace(trace, "unseen_hold_selected", {
2488
+ heldCount: heldMessages.length,
2489
+ omittedCount: omittedMessageCount,
2490
+ consumeBoundarySeq: boundary.seenUpToSeq
2491
+ });
2492
+ return heldPlan(input, {
2493
+ decision: {
2494
+ action: input.action,
2495
+ decision: "syncing_hold",
2496
+ target: input.target,
2497
+ inboxTrustState: "untrusted",
2498
+ reason: "target_first_touch_recent_context",
2499
+ pendingCount: 0,
2500
+ pendingMaxSeq: heldBoundary.seenUpToSeq,
2501
+ modelSeenSeq: 0,
2502
+ heldMessageCount: heldMessages.length,
2503
+ omittedMessageCount
2504
+ },
2505
+ context: {
2506
+ heldMessages,
2507
+ newMessageCount: unconsumedMessages.length,
2508
+ shownMessageCount: heldMessages.length,
2509
+ omittedMessageCount,
2510
+ seenUpToSeq: boundary.seenUpToSeq
2511
+ },
2512
+ consumeMessages: recent,
2513
+ consumeBoundarySeq: boundary.seenUpToSeq
2514
+ }, trace);
2515
+ }
2516
+ function heldPlan(input, held, trace) {
2517
+ const producerFactId = buildApmFreshnessDecisionProducerFactId(input.agentId, held.decision);
2518
+ const decision = { ...held.decision, producerFactId };
2519
+ appendTrace(trace, "plan_built", {
2520
+ outcome: "held",
2521
+ decision: decision.decision,
2522
+ effectCount: 2,
2523
+ localResponseState: "held",
2524
+ seenUpToSeq: held.context.seenUpToSeq
2525
+ });
2526
+ return {
2527
+ outcome: "held",
2528
+ target: input.target,
2529
+ effects: [
2530
+ {
2531
+ type: "consume_visible_messages",
2532
+ target: input.target,
2533
+ messages: held.consumeMessages,
2534
+ boundarySeq: held.consumeBoundarySeq,
2535
+ source: "side_effect_preflight_context"
2536
+ },
2537
+ { type: "record_freshness_decision", decision }
2538
+ ],
2539
+ trace,
2540
+ localResponse: projectApmHeldFreshnessEnvelope({
2541
+ producerFactId,
2542
+ action: input.action,
2543
+ heldMessages: held.context.heldMessages,
2544
+ newMessageCount: held.context.newMessageCount,
2545
+ omittedMessageCount: held.context.omittedMessageCount,
2546
+ seenUpToSeq: held.context.seenUpToSeq
2547
+ }).body
2548
+ };
2549
+ }
2550
+ function forwardPlan(input, decision, trace, options = {}) {
2551
+ appendTrace(trace, "plan_built", {
2552
+ outcome: "forward",
2553
+ decision: decision.decision,
2554
+ effectCount: options.consumeEffect ? 2 : 1,
2555
+ forwardSeenUpToSeq: options.forwardSeenUpToSeq
2556
+ });
2557
+ return {
2558
+ outcome: "forward",
2559
+ target: input.target,
2560
+ forwardSeenUpToSeq: options.forwardSeenUpToSeq,
2561
+ effects: [
2562
+ ...options.consumeEffect ? [options.consumeEffect] : [],
2563
+ { type: "record_freshness_decision", decision }
2564
+ ],
2565
+ trace
2566
+ };
2567
+ }
2568
+ function forwardWithoutDecision(input, trace) {
2569
+ appendTrace(trace, "plan_built", {
2570
+ outcome: "forward",
2571
+ decision: "none",
2572
+ effectCount: 0
2573
+ });
2574
+ return {
2575
+ outcome: "forward",
2576
+ target: input.target,
2577
+ effects: [],
2578
+ trace
2579
+ };
2580
+ }
2581
+ function resolveFreshnessBoundary(messages) {
2582
+ const seenUpToSeq = maxInboxMessageSeq(messages);
2583
+ return typeof seenUpToSeq === "number" ? { ok: true, seenUpToSeq } : { ok: false, reason: "missing_seq_boundary" };
2584
+ }
2585
+ function latestVisibleMessages(messages, limit) {
2586
+ const sorted = sortInboxMessagesBySeq(messages);
2587
+ return sorted.slice(Math.max(0, sorted.length - limit));
2588
+ }
2589
+ function isMessageModelSeen(input, message) {
2590
+ const seq = Math.floor(messageSeq(message));
2591
+ if (Number.isFinite(seq) && seq > 0 && typeof input.modelSeenSeq === "number" && input.modelSeenSeq >= seq) return true;
2592
+ return input.isMessageModelSeen?.({ target: input.target, message }) === true;
2593
+ }
2594
+ function messageSeq(message) {
2595
+ return Number(message.seq ?? 0);
2596
+ }
2597
+ function messageSenderId(message) {
2598
+ if (typeof message.sender_id === "string" && message.sender_id.length > 0) return message.sender_id;
2599
+ if (typeof message.senderId === "string" && message.senderId.length > 0) return message.senderId;
2600
+ return void 0;
2601
+ }
2602
+ function appendTrace(trace, step, data) {
2603
+ const compacted = compactTraceData(data);
2604
+ trace.push(Object.keys(compacted).length > 0 ? { step, data: compacted } : { step });
2605
+ }
2606
+ function compactTraceData(data) {
2607
+ const compacted = {};
2608
+ if (!data) return compacted;
2609
+ for (const [key, value] of Object.entries(data)) {
2610
+ if (value !== void 0) compacted[key] = value;
2611
+ }
2612
+ return compacted;
2613
+ }
2614
+ function parseTargetFields(target) {
2615
+ if (target.startsWith("dm:@")) {
2616
+ const rest = target.slice("dm:@".length);
2617
+ const [peer, threadId] = rest.split(":", 2);
2618
+ if (threadId) {
2619
+ return {
2620
+ channel_type: "thread",
2621
+ channel_name: threadId,
2622
+ parent_channel_type: "dm",
2623
+ parent_channel_name: peer
2624
+ };
2625
+ }
2626
+ return { channel_type: "dm", channel_name: peer };
2627
+ }
2628
+ if (target.startsWith("#")) {
2629
+ const rest = target.slice(1);
2630
+ const [channel, threadId] = rest.split(":", 2);
2631
+ if (threadId) {
2632
+ return {
2633
+ channel_type: "thread",
2634
+ channel_name: threadId,
2635
+ parent_channel_type: "channel",
2636
+ parent_channel_name: channel
2637
+ };
2638
+ }
2639
+ return { channel_type: "channel", channel_name: channel };
2640
+ }
2641
+ return {};
2642
+ }
2643
+
2238
2644
  // src/agentInboxProjection.ts
2239
2645
  function projectAgentInboxSnapshot(messages) {
2240
2646
  const buckets = /* @__PURE__ */ new Map();
@@ -2264,16 +2670,16 @@ function projectBucket(target, messages) {
2264
2670
  channelType: latest.channel_type,
2265
2671
  pendingCount: messages.length,
2266
2672
  firstPendingMsgId: messageId(first),
2267
- firstPendingSeq: messageSeq(first),
2673
+ firstPendingSeq: messageSeq2(first),
2268
2674
  latestMsgId: messageId(latest),
2269
- latestSeq: messageSeq(latest),
2675
+ latestSeq: messageSeq2(latest),
2270
2676
  latestSenderName: latest.sender_name ?? latest.senderName,
2271
2677
  latestSenderType: normalizeSenderType(latest.sender_type ?? latest.senderType),
2272
2678
  flags: [...flags].sort()
2273
2679
  });
2274
2680
  }
2275
2681
  function compareInboxMessages(a, b) {
2276
- return (messageSeq(a) ?? 0) - (messageSeq(b) ?? 0) || (messageId(a) ?? "").localeCompare(messageId(b) ?? "");
2682
+ return (messageSeq2(a) ?? 0) - (messageSeq2(b) ?? 0) || (messageId(a) ?? "").localeCompare(messageId(b) ?? "");
2277
2683
  }
2278
2684
  function formatInboxMessageTarget(message) {
2279
2685
  if (message.channel_type === "thread" && message.parent_channel_name && message.channel_name) {
@@ -2289,7 +2695,7 @@ function messageId(message) {
2289
2695
  if (!message) return void 0;
2290
2696
  return nonEmptyString(message.message_id) ?? nonEmptyString(message.id);
2291
2697
  }
2292
- function messageSeq(message) {
2698
+ function messageSeq2(message) {
2293
2699
  if (!message || typeof message.seq !== "number" || !Number.isFinite(message.seq) || message.seq <= 0) return void 0;
2294
2700
  return Math.floor(message.seq);
2295
2701
  }
@@ -2814,42 +3220,9 @@ async function readRequestBody(req) {
2814
3220
  }
2815
3221
  return Buffer.concat(chunks);
2816
3222
  }
2817
- function messageSeq2(message) {
3223
+ function messageSeq3(message) {
2818
3224
  return Number(message.seq ?? 0);
2819
3225
  }
2820
- function maxMessageSeq(messages) {
2821
- let maxSeq = 0;
2822
- for (const message of messages) {
2823
- const seq = Math.floor(messageSeq2(message));
2824
- if (Number.isFinite(seq) && seq > 0) maxSeq = Math.max(maxSeq, seq);
2825
- }
2826
- return maxSeq > 0 ? maxSeq : void 0;
2827
- }
2828
- function messageSenderId(message) {
2829
- if (typeof message.sender_id === "string" && message.sender_id.length > 0) return message.sender_id;
2830
- if (typeof message.senderId === "string" && message.senderId.length > 0) return message.senderId;
2831
- return void 0;
2832
- }
2833
- function isSelfAuthoredMessage(registration, message) {
2834
- return messageSenderId(message) === registration.agentId;
2835
- }
2836
- function isMessageModelSeen(coordinator, target, message) {
2837
- const seq = Math.floor(messageSeq2(message));
2838
- const boundary = coordinator.getBoundary(target);
2839
- if (Number.isFinite(seq) && seq > 0 && typeof boundary === "number" && boundary >= seq) return true;
2840
- return coordinator.isMessageModelSeen?.({ target, message }) === true;
2841
- }
2842
- function resolveFreshnessBoundary(messages) {
2843
- const seenUpToSeq = maxMessageSeq(messages);
2844
- return typeof seenUpToSeq === "number" ? { ok: true, seenUpToSeq } : { ok: false, reason: "missing_seq_boundary" };
2845
- }
2846
- function sortBySeq(messages) {
2847
- return [...messages].sort((a, b) => messageSeq2(a) - messageSeq2(b));
2848
- }
2849
- function latestVisibleMessages(messages, limit) {
2850
- const sorted = sortBySeq(messages);
2851
- return sorted.slice(Math.max(0, sorted.length - limit));
2852
- }
2853
3226
  function localAgentApiInboxResponse(registration) {
2854
3227
  const coordinator = registration.inboxCoordinator;
2855
3228
  if (!coordinator) return void 0;
@@ -2897,9 +3270,9 @@ function localAgentApiEventsResponse(registration, target) {
2897
3270
  return { status: 400, body: parsedQuery.error };
2898
3271
  }
2899
3272
  if (pending.length === 0) return void 0;
2900
- const normalized = sortBySeq(normalizeVisibleMessages(pending));
3273
+ const normalized = sortInboxMessagesBySeq(normalizeInboxVisibleMessages(pending));
2901
3274
  const filtered = parsedQuery.sinceSeq !== null ? normalized.filter((message) => {
2902
- const seq = messageSeq2(message);
3275
+ const seq = messageSeq3(message);
2903
3276
  return Number.isFinite(seq) && seq > parsedQuery.sinceSeq;
2904
3277
  }) : normalized;
2905
3278
  const events = filtered.slice(0, parsedQuery.limit);
@@ -2908,7 +3281,7 @@ function localAgentApiEventsResponse(registration, target) {
2908
3281
  const lastSeenMsgId = newestEvent?.message_id ?? newestEvent?.id ?? null;
2909
3282
  const lastSeenSeq = newestEvent?.seq ?? parsedQuery.sinceSeq;
2910
3283
  if (events.length > 0) {
2911
- coordinator.consumeVisibleMessages({ messages: events, source: "agent_api_events" });
3284
+ coordinator.consumeVisibleMessages({ messages: events, source: "agent_api_events_local" });
2912
3285
  }
2913
3286
  coordinator.recordDrainOutcome?.({
2914
3287
  source: "daemon_pending",
@@ -2930,30 +3303,6 @@ function localAgentApiEventsResponse(registration, target) {
2930
3303
  }
2931
3304
  };
2932
3305
  }
2933
- function localHeldContext(input) {
2934
- if (input.messages.length === 0) return { ok: false, reason: "empty_context" };
2935
- const normalized = sortBySeq(normalizeVisibleMessages(input.messages, input.target));
2936
- const heldMessages = latestVisibleMessages(normalized, LOCAL_HELD_CONTEXT_LIMIT);
2937
- const omittedMessageCount = Math.max(0, normalized.length - heldMessages.length);
2938
- const boundary = resolveFreshnessBoundary(normalized);
2939
- if (!boundary.ok) return boundary;
2940
- input.coordinator.consumeVisibleMessages({
2941
- target: input.target,
2942
- messages: heldMessages,
2943
- boundarySeq: boundary.seenUpToSeq,
2944
- source: input.source
2945
- });
2946
- return {
2947
- ok: true,
2948
- context: {
2949
- heldMessages,
2950
- newMessageCount: normalized.length,
2951
- shownMessageCount: heldMessages.length,
2952
- omittedMessageCount,
2953
- seenUpToSeq: boundary.seenUpToSeq
2954
- }
2955
- };
2956
- }
2957
3306
  function recordFreshnessDecision(coordinator, decision) {
2958
3307
  coordinator?.recordFreshnessDecision?.(decision);
2959
3308
  }
@@ -2967,53 +3316,6 @@ function sideEffectTarget(action, body) {
2967
3316
  const field = action === "send" ? body.target : body.channel;
2968
3317
  return typeof field === "string" && field.length > 0 ? field : void 0;
2969
3318
  }
2970
- function parseTargetFields(target) {
2971
- if (target.startsWith("dm:@")) {
2972
- const rest = target.slice("dm:@".length);
2973
- const [peer, threadId] = rest.split(":", 2);
2974
- if (threadId) {
2975
- return {
2976
- channel_type: "thread",
2977
- channel_name: threadId,
2978
- parent_channel_type: "dm",
2979
- parent_channel_name: peer
2980
- };
2981
- }
2982
- return { channel_type: "dm", channel_name: peer };
2983
- }
2984
- if (target.startsWith("#")) {
2985
- const rest = target.slice(1);
2986
- const [channel, threadId] = rest.split(":", 2);
2987
- if (threadId) {
2988
- return {
2989
- channel_type: "thread",
2990
- channel_name: threadId,
2991
- parent_channel_type: "channel",
2992
- parent_channel_name: channel
2993
- };
2994
- }
2995
- return { channel_type: "channel", channel_name: channel };
2996
- }
2997
- return {};
2998
- }
2999
- function normalizeVisibleMessage(message, target) {
3000
- const targetFields = target ? parseTargetFields(target) : {};
3001
- const normalized = {
3002
- ...targetFields,
3003
- ...message,
3004
- message_id: message.message_id ?? message.id,
3005
- timestamp: message.timestamp ?? message.createdAt,
3006
- sender_type: message.sender_type ?? message.senderType,
3007
- sender_name: message.sender_name ?? message.senderName,
3008
- sender_description: message.sender_description ?? message.senderDescription ?? null
3009
- };
3010
- const senderId = messageSenderId(message);
3011
- if (senderId) normalized.sender_id = senderId;
3012
- return normalized;
3013
- }
3014
- function normalizeVisibleMessages(messages, target) {
3015
- return messages.map((message) => normalizeVisibleMessage(message, target));
3016
- }
3017
3319
  async function loadRecentTargetMessages(registration, headers, target) {
3018
3320
  const historyUrl = new URL2("/internal/agent-api/history", registration.serverUrl);
3019
3321
  historyUrl.searchParams.set("channel", target);
@@ -3024,7 +3326,21 @@ async function loadRecentTargetMessages(registration, headers, target) {
3024
3326
  const res = await fetch(historyUrl, { method: "GET", headers: historyHeaders });
3025
3327
  if (!res.ok) return [];
3026
3328
  const parsed = await res.json().catch(() => null);
3027
- return Array.isArray(parsed?.messages) ? normalizeVisibleMessages(parsed.messages, target) : [];
3329
+ return Array.isArray(parsed?.messages) ? normalizeInboxVisibleMessages(parsed.messages, target) : [];
3330
+ }
3331
+ function applyAgentInboxStateMachineEffects(coordinator, effects) {
3332
+ for (const effect of effects) {
3333
+ if (effect.type === "record_freshness_decision") {
3334
+ recordFreshnessDecision(coordinator, effect.decision);
3335
+ continue;
3336
+ }
3337
+ coordinator.consumeVisibleMessages({
3338
+ target: effect.target,
3339
+ messages: effect.messages,
3340
+ boundarySeq: effect.boundarySeq,
3341
+ source: effect.source
3342
+ });
3343
+ }
3028
3344
  }
3029
3345
  async function prepareAgentApiSideEffectForward(registration, headers, rawBody, action) {
3030
3346
  let body;
@@ -3038,166 +3354,28 @@ async function prepareAgentApiSideEffectForward(registration, headers, rawBody,
3038
3354
  if (!target || !coordinator) {
3039
3355
  return { bodyText: JSON.stringify(body), target };
3040
3356
  }
3041
- if (action === "send" && body.continueAnyway === true) {
3042
- recordFreshnessDecision(coordinator, {
3043
- action,
3044
- decision: "bypass",
3045
- target,
3046
- inboxTrustState: "trusted",
3047
- reason: "continue_anyway"
3048
- });
3049
- return { bodyText: JSON.stringify(body), target };
3050
- }
3051
3357
  const pending = coordinator.getPendingMessages(target);
3052
- if (pending.length > 0) {
3053
- const modelSeenSeq = coordinator.getBoundary(target);
3054
- const contextResult = localHeldContext({
3055
- target,
3056
- messages: pending,
3057
- coordinator,
3058
- source: "side_effect_preflight_context"
3059
- });
3060
- if (contextResult.ok) {
3061
- const { context } = contextResult;
3062
- const decision = {
3063
- action,
3064
- decision: "local_hold",
3065
- target,
3066
- inboxTrustState: "trusted",
3067
- reason: "exact_target_pending",
3068
- pendingCount: pending.length,
3069
- pendingMaxSeq: context.seenUpToSeq,
3070
- modelSeenSeq,
3071
- heldMessageCount: context.shownMessageCount,
3072
- omittedMessageCount: context.omittedMessageCount
3073
- };
3074
- const producerFactId = buildApmFreshnessDecisionProducerFactId(registration.agentId, decision);
3075
- const localResponse = projectApmHeldFreshnessEnvelope({
3076
- producerFactId,
3077
- action,
3078
- heldMessages: context.heldMessages,
3079
- newMessageCount: context.newMessageCount,
3080
- omittedMessageCount: context.omittedMessageCount,
3081
- seenUpToSeq: context.seenUpToSeq
3082
- }).body;
3083
- recordFreshnessDecision(coordinator, { ...decision, producerFactId });
3084
- return {
3085
- bodyText: JSON.stringify(body),
3086
- target,
3087
- localResponse
3088
- };
3089
- }
3090
- return {
3091
- bodyText: JSON.stringify(body),
3092
- target
3093
- };
3094
- }
3095
3358
  const existingBoundary = typeof body.seenUpToSeq === "number" && Number.isFinite(body.seenUpToSeq) ? Math.max(0, Math.floor(body.seenUpToSeq)) : void 0;
3096
- const loadedBoundary = coordinator.getBoundary(target);
3097
- const boundary = Math.max(existingBoundary ?? 0, loadedBoundary ?? 0);
3098
- if (boundary > 0) {
3099
- if (action === "send") body.seenUpToSeq = boundary;
3100
- recordFreshnessDecision(coordinator, {
3101
- action,
3102
- decision: "forward",
3103
- target,
3104
- inboxTrustState: "trusted",
3105
- reason: "model_seen_boundary",
3106
- pendingCount: 0,
3107
- modelSeenSeq: boundary
3108
- });
3109
- return { bodyText: JSON.stringify(body), target };
3110
- }
3111
- const recent = await loadRecentTargetMessages(registration, headers, target);
3112
- if (recent.length > 0) {
3113
- const unconsumedCounterparty = recent.filter(
3114
- (message) => !isSelfAuthoredMessage(registration, message) && !isMessageModelSeen(coordinator, target, message)
3115
- );
3116
- const boundary2 = resolveFreshnessBoundary(recent);
3117
- if (!boundary2.ok) {
3118
- recordFreshnessDecision(coordinator, {
3119
- action,
3120
- decision: "forward",
3121
- target,
3122
- inboxTrustState: "untrusted",
3123
- reason: "target_first_touch_recent_context_without_seq_boundary",
3124
- pendingCount: 0,
3125
- modelSeenSeq: 0
3126
- });
3127
- return { bodyText: JSON.stringify(body), target };
3128
- }
3129
- if (unconsumedCounterparty.length === 0) {
3130
- const { seenUpToSeq: seenUpToSeq2 } = boundary2;
3131
- if (action === "send") body.seenUpToSeq = seenUpToSeq2;
3132
- coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq2, source: "side_effect_preflight_context" });
3133
- recordFreshnessDecision(coordinator, {
3134
- action,
3135
- decision: "forward",
3136
- target,
3137
- inboxTrustState: "untrusted",
3138
- reason: "target_first_touch_recent_context_already_seen",
3139
- pendingCount: 0,
3140
- pendingMaxSeq: seenUpToSeq2,
3141
- modelSeenSeq: seenUpToSeq2,
3142
- heldMessageCount: 0,
3143
- omittedMessageCount: 0
3144
- });
3145
- return { bodyText: JSON.stringify(body), target };
3146
- }
3147
- const heldBoundary = resolveFreshnessBoundary(unconsumedCounterparty);
3148
- if (!heldBoundary.ok) {
3149
- recordFreshnessDecision(coordinator, {
3150
- action,
3151
- decision: "forward",
3152
- target,
3153
- inboxTrustState: "untrusted",
3154
- reason: "target_first_touch_counterparty_context_without_seq_boundary",
3155
- pendingCount: 0,
3156
- modelSeenSeq: 0
3157
- });
3158
- return { bodyText: JSON.stringify(body), target };
3159
- }
3160
- const heldMessages = latestVisibleMessages(unconsumedCounterparty, LOCAL_HELD_CONTEXT_LIMIT);
3161
- const omittedMessageCount = Math.max(0, unconsumedCounterparty.length - heldMessages.length);
3162
- const { seenUpToSeq } = boundary2;
3163
- coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq, source: "side_effect_preflight_context" });
3164
- const decision = {
3165
- action,
3166
- decision: "syncing_hold",
3167
- target,
3168
- inboxTrustState: "untrusted",
3169
- reason: "target_first_touch_recent_context",
3170
- pendingCount: 0,
3171
- pendingMaxSeq: heldBoundary.seenUpToSeq,
3172
- modelSeenSeq: 0,
3173
- heldMessageCount: heldMessages.length,
3174
- omittedMessageCount
3175
- };
3176
- const producerFactId = buildApmFreshnessDecisionProducerFactId(registration.agentId, decision);
3177
- recordFreshnessDecision(coordinator, { ...decision, producerFactId });
3178
- return {
3179
- bodyText: JSON.stringify(body),
3180
- target,
3181
- localResponse: projectApmHeldFreshnessEnvelope({
3182
- producerFactId,
3183
- action,
3184
- heldMessages,
3185
- newMessageCount: unconsumedCounterparty.length,
3186
- omittedMessageCount,
3187
- seenUpToSeq
3188
- }).body
3189
- };
3190
- }
3191
- recordFreshnessDecision(coordinator, {
3359
+ const continueAnyway = action === "send" && body.continueAnyway === true;
3360
+ const shouldLoadRecent = pending.length === 0 && !continueAnyway && Math.max(existingBoundary ?? 0, coordinator.getBoundary(target) ?? 0) <= 0;
3361
+ const recent = shouldLoadRecent ? await loadRecentTargetMessages(registration, headers, target) : [];
3362
+ const plan = planAgentInboxSideEffect({
3363
+ agentId: registration.agentId,
3192
3364
  action,
3193
- decision: "forward",
3194
3365
  target,
3195
- inboxTrustState: "trusted",
3196
- reason: "no_exact_target_pending_or_recent_context",
3197
- pendingCount: 0,
3198
- modelSeenSeq: 0
3366
+ continueAnyway,
3367
+ existingSeenUpToSeq: existingBoundary,
3368
+ modelSeenSeq: coordinator.getBoundary(target),
3369
+ pendingMessages: pending,
3370
+ recentMessages: recent,
3371
+ isMessageModelSeen: (messageInput) => coordinator.isMessageModelSeen?.(messageInput) === true,
3372
+ heldContextLimit: LOCAL_HELD_CONTEXT_LIMIT
3199
3373
  });
3200
- return { bodyText: JSON.stringify(body), target };
3374
+ applyAgentInboxStateMachineEffects(coordinator, plan.effects);
3375
+ if (typeof plan.forwardSeenUpToSeq === "number") {
3376
+ if (action === "send") body.seenUpToSeq = plan.forwardSeenUpToSeq;
3377
+ }
3378
+ return { bodyText: JSON.stringify(body), target, localResponse: plan.localResponse };
3201
3379
  }
3202
3380
  function shouldBufferJsonResponse(upstream, pathname, registration) {
3203
3381
  if (!registration.inboxCoordinator) return false;
@@ -3217,27 +3395,26 @@ function consumeVisibleResponse(registration, targetUrl, sendTarget, responseTex
3217
3395
  if (targetUrl.pathname === "/internal/agent-api/send" && parsed.state === "held" && Array.isArray(parsed.heldMessages)) {
3218
3396
  coordinator.consumeVisibleMessages({
3219
3397
  target: sendTarget,
3220
- messages: normalizeVisibleMessages(parsed.heldMessages, sendTarget),
3398
+ messages: normalizeInboxVisibleMessages(parsed.heldMessages, sendTarget),
3221
3399
  boundarySeq: typeof parsed.seenUpToSeq === "number" ? parsed.seenUpToSeq : void 0,
3222
3400
  source: "server_held_context"
3223
3401
  });
3224
3402
  return;
3225
3403
  }
3226
3404
  if (targetUrl.pathname === "/internal/agent-api/send" && parsed.state === "sent") {
3227
- const messageSeq3 = typeof parsed.messageSeq === "number" && Number.isFinite(parsed.messageSeq) ? Math.floor(parsed.messageSeq) : void 0;
3228
- if (sendTarget && messageSeq3 && messageSeq3 > 0) {
3405
+ const messageSeq4 = typeof parsed.messageSeq === "number" && Number.isFinite(parsed.messageSeq) ? Math.floor(parsed.messageSeq) : void 0;
3406
+ if (sendTarget && messageSeq4 && messageSeq4 > 0) {
3229
3407
  coordinator.consumeVisibleMessages({
3230
3408
  target: sendTarget,
3231
- messages: normalizeVisibleMessages([{ seq: messageSeq3, id: parsed.messageId }], sendTarget),
3232
- boundarySeq: messageSeq3,
3409
+ messages: normalizeInboxVisibleMessages([{ id: parsed.messageId }], sendTarget),
3233
3410
  source: "agent_api_send_commit"
3234
3411
  });
3235
3412
  }
3236
3413
  return;
3237
3414
  }
3238
3415
  if (targetUrl.pathname === "/internal/agent-api/events" && Array.isArray(parsed.events)) {
3239
- const messages = normalizeVisibleMessages(parsed.events);
3240
- coordinator.consumeVisibleMessages({ messages, source: "agent_api_events" });
3416
+ const messages = normalizeInboxVisibleMessages(parsed.events);
3417
+ coordinator.consumeVisibleMessages({ messages, source: "agent_api_events_server" });
3241
3418
  coordinator.recordDrainOutcome?.({
3242
3419
  source: "server_events",
3243
3420
  sinceCursorKind: parseAgentApiEventsQuery(targetUrl).sinceCursorKind,
@@ -3249,8 +3426,8 @@ function consumeVisibleResponse(registration, targetUrl, sendTarget, responseTex
3249
3426
  }
3250
3427
  if (targetUrl.pathname === "/internal/agent-api/history" && Array.isArray(parsed.messages)) {
3251
3428
  const target = targetUrl.searchParams.get("channel") ?? void 0;
3252
- const messages = normalizeVisibleMessages(parsed.messages, target);
3253
- coordinator.consumeVisibleMessages({ target, messages, boundarySeq: maxMessageSeq(messages), source: "agent_api_history" });
3429
+ const messages = normalizeInboxVisibleMessages(parsed.messages, target);
3430
+ coordinator.consumeVisibleMessages({ target, messages, boundarySeq: maxInboxMessageSeq(messages), source: "agent_api_history" });
3254
3431
  }
3255
3432
  }
3256
3433
  async function registerAgentCredentialProxy(input) {
@@ -7204,9 +7381,26 @@ var RuntimeProgressState = class {
7204
7381
  };
7205
7382
 
7206
7383
  // src/runtimeNotificationState.ts
7384
+ function computeInboxNoticeFingerprint(messages) {
7385
+ const keys = [];
7386
+ for (const m of messages) {
7387
+ const seq = typeof m.seq === "number" && Number.isFinite(m.seq) && m.seq > 0 ? Math.floor(m.seq) : null;
7388
+ if (seq !== null) {
7389
+ keys.push(`s:${seq}`);
7390
+ continue;
7391
+ }
7392
+ const id = typeof m.message_id === "string" && m.message_id.length > 0 ? m.message_id : typeof m.id === "string" && m.id.length > 0 ? m.id : "";
7393
+ if (id.length > 0) keys.push(`m:${id}`);
7394
+ }
7395
+ if (keys.length === 0) return "";
7396
+ keys.sort();
7397
+ return keys.join(",");
7398
+ }
7207
7399
  var RuntimeNotificationState = class {
7208
7400
  timerValue = null;
7209
7401
  pendingCountValue = 0;
7402
+ lastNoticeFingerprint = null;
7403
+ lastNoticeSessionId = null;
7210
7404
  get pendingCount() {
7211
7405
  return this.pendingCountValue;
7212
7406
  }
@@ -7237,6 +7431,25 @@ var RuntimeNotificationState = class {
7237
7431
  clear() {
7238
7432
  this.clearPending();
7239
7433
  this.clearTimer();
7434
+ this.clearNoticeFingerprint();
7435
+ }
7436
+ /**
7437
+ * True iff this exact unread-set was already successfully written in THIS
7438
+ * session (per-(agent,session) scope — never suppress across a session
7439
+ * change). Empty fingerprint always returns false (fail toward sending).
7440
+ */
7441
+ isDuplicateNotice(fingerprint, sessionId) {
7442
+ if (fingerprint.length === 0) return false;
7443
+ return this.lastNoticeFingerprint === fingerprint && this.lastNoticeSessionId === sessionId;
7444
+ }
7445
+ /** Register a fingerprint as written — call ONLY after a successful stdin write. */
7446
+ recordNoticeWritten(fingerprint, sessionId) {
7447
+ this.lastNoticeFingerprint = fingerprint;
7448
+ this.lastNoticeSessionId = sessionId;
7449
+ }
7450
+ clearNoticeFingerprint() {
7451
+ this.lastNoticeFingerprint = null;
7452
+ this.lastNoticeSessionId = null;
7240
7453
  }
7241
7454
  schedule(callback, delayMs) {
7242
7455
  if (this.timerValue) return false;
@@ -7368,34 +7581,24 @@ function toLocalTime(iso) {
7368
7581
  function formatChannelLabel(message) {
7369
7582
  return message.channel_type === "dm" ? `DM:@${message.channel_name}` : `#${message.channel_name}`;
7370
7583
  }
7371
- function formatMessageTarget(message) {
7372
- if (message.channel_type === "thread" && message.parent_channel_name) {
7373
- const shortId = getMessageShortId(message.channel_name);
7374
- if (message.parent_channel_type === "dm") {
7375
- return `dm:@${message.parent_channel_name}:${shortId}`;
7584
+ function computeTarget(channelType, channelName, parentChannelName, parentChannelType) {
7585
+ if (channelType === "thread" && parentChannelName) {
7586
+ const shortId = getMessageShortId(String(channelName ?? ""));
7587
+ if (parentChannelType === "dm") {
7588
+ return `dm:@${parentChannelName}:${shortId}`;
7376
7589
  }
7377
- return `#${message.parent_channel_name}:${shortId}`;
7590
+ return `#${parentChannelName}:${shortId}`;
7378
7591
  }
7379
- if (message.channel_type === "dm") {
7380
- return `dm:@${message.channel_name}`;
7592
+ if (channelType === "dm") {
7593
+ return `dm:@${channelName}`;
7381
7594
  }
7382
- return `#${message.channel_name}`;
7595
+ return `#${channelName}`;
7596
+ }
7597
+ function formatMessageTarget(message) {
7598
+ return computeTarget(message.channel_type, message.channel_name, message.parent_channel_name, message.parent_channel_type);
7383
7599
  }
7384
7600
  function formatVisibleMessageTarget(message) {
7385
- if (message.channel_type === "thread" && message.parent_channel_name && message.channel_name) {
7386
- const shortId = getMessageShortId(String(message.channel_name));
7387
- if (message.parent_channel_type === "dm") {
7388
- return `dm:@${message.parent_channel_name}:${shortId}`;
7389
- }
7390
- return `#${message.parent_channel_name}:${shortId}`;
7391
- }
7392
- if (message.channel_type === "dm" && message.channel_name) {
7393
- return `dm:@${message.channel_name}`;
7394
- }
7395
- if (message.channel_name) {
7396
- return `#${message.channel_name}`;
7397
- }
7398
- return null;
7601
+ return computeTarget(message.channel_type, message.channel_name, message.parent_channel_name, message.parent_channel_type);
7399
7602
  }
7400
7603
  function getMessageShortId(messageId2) {
7401
7604
  return messageId2.startsWith("thread-") ? messageId2.slice(7) : messageId2.slice(0, 8);
@@ -7617,6 +7820,8 @@ var TRAJECTORY_COALESCE_MS = 350;
7617
7820
  var ACTIVITY_HEARTBEAT_MS = 6e4;
7618
7821
  var STDIN_NOTIFICATION_INITIAL_DELAY_MS = 3e3;
7619
7822
  var STDIN_NOTIFICATION_RETRY_DELAY_MS = 15e3;
7823
+ var RUNTIME_ERROR_DELIVERY_BACKOFF_BASE_MS = 1e4;
7824
+ var RUNTIME_ERROR_DELIVERY_BACKOFF_MAX_MS = 5 * 6e4;
7620
7825
  var COMPACTION_STALE_MS = 5 * 6e4;
7621
7826
  var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
7622
7827
  var DEFAULT_RUNTIME_START_TIMEOUT_MS = 2 * 6e4;
@@ -8030,6 +8235,14 @@ function createRuntimeTraceCounters() {
8030
8235
  thinkingEvents: 0
8031
8236
  };
8032
8237
  }
8238
+ function createRuntimeErrorDeliveryBackoffState() {
8239
+ return {
8240
+ attempts: 0,
8241
+ untilMs: 0,
8242
+ timer: null,
8243
+ reason: null
8244
+ };
8245
+ }
8033
8246
  function cleanupAgentCredentialProxy(agentId, launchId) {
8034
8247
  unregisterAgentCredentialProxyForLaunch({ agentId, launchId });
8035
8248
  }
@@ -8474,6 +8687,10 @@ var AgentProcessManager = class _AgentProcessManager {
8474
8687
  defaultAgentEnvVarsProvider;
8475
8688
  tracer;
8476
8689
  stdinNotificationRetryMs;
8690
+ runtimeErrorDeliveryBackoffBaseMs;
8691
+ runtimeErrorDeliveryBackoffMaxMs;
8692
+ runtimeErrorDeliveryBackoffJitterRatio;
8693
+ runtimeErrorDeliveryBackoffFailPointForTesting;
8477
8694
  cliTransportTraceDir = null;
8478
8695
  deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
8479
8696
  runtimeExitTraceAttrs = /* @__PURE__ */ new WeakMap();
@@ -8497,6 +8714,19 @@ var AgentProcessManager = class _AgentProcessManager {
8497
8714
  0,
8498
8715
  Math.floor(opts.stdinNotificationRetryMs ?? STDIN_NOTIFICATION_RETRY_DELAY_MS)
8499
8716
  );
8717
+ this.runtimeErrorDeliveryBackoffBaseMs = Math.max(
8718
+ 0,
8719
+ Math.floor(opts.runtimeErrorDeliveryBackoff?.baseMs ?? RUNTIME_ERROR_DELIVERY_BACKOFF_BASE_MS)
8720
+ );
8721
+ this.runtimeErrorDeliveryBackoffMaxMs = Math.max(
8722
+ this.runtimeErrorDeliveryBackoffBaseMs,
8723
+ Math.floor(opts.runtimeErrorDeliveryBackoff?.maxMs ?? RUNTIME_ERROR_DELIVERY_BACKOFF_MAX_MS)
8724
+ );
8725
+ this.runtimeErrorDeliveryBackoffJitterRatio = Math.max(
8726
+ 0,
8727
+ Math.min(1, opts.runtimeErrorDeliveryBackoff?.jitterRatio ?? 0.1)
8728
+ );
8729
+ this.runtimeErrorDeliveryBackoffFailPointForTesting = opts.runtimeErrorDeliveryBackoff?.failPointForTesting ?? null;
8500
8730
  this.maxConcurrentAgentStarts = Math.max(
8501
8731
  1,
8502
8732
  Math.floor(
@@ -8550,6 +8780,183 @@ var AgentProcessManager = class _AgentProcessManager {
8550
8780
  this.sendStdinNotification(agentId);
8551
8781
  }, delayMs);
8552
8782
  }
8783
+ clearRuntimeErrorDeliveryBackoff(ap) {
8784
+ if (ap.runtimeErrorDeliveryBackoff.timer) {
8785
+ clearTimeout(ap.runtimeErrorDeliveryBackoff.timer);
8786
+ }
8787
+ ap.runtimeErrorDeliveryBackoff = createRuntimeErrorDeliveryBackoffState();
8788
+ }
8789
+ clearRuntimeErrorDeliveryBackoffAfterProgress(agentId, ap, eventKind) {
8790
+ if (ap.runtimeErrorDeliveryBackoff.attempts === 0 && ap.runtimeErrorDeliveryBackoff.untilMs === 0) return;
8791
+ const attempts = ap.runtimeErrorDeliveryBackoff.attempts;
8792
+ const reason = ap.runtimeErrorDeliveryBackoff.reason;
8793
+ this.clearRuntimeErrorDeliveryBackoff(ap);
8794
+ this.recordDaemonTrace("daemon.agent.runtime_error_delivery_backoff.reset", {
8795
+ agentId,
8796
+ runtime: ap.config.runtime,
8797
+ model: ap.config.model,
8798
+ launchId: ap.launchId || void 0,
8799
+ reason: reason || void 0,
8800
+ attempts,
8801
+ reset_source: eventKind
8802
+ });
8803
+ }
8804
+ runtimeErrorDeliveryBackoffRemainingMs(ap) {
8805
+ return Math.max(0, ap.runtimeErrorDeliveryBackoff.untilMs - Date.now());
8806
+ }
8807
+ scheduleRuntimeErrorDeliveryBackoffFlush(agentId, ap) {
8808
+ const delayMs = this.runtimeErrorDeliveryBackoffRemainingMs(ap);
8809
+ if (delayMs <= 0 || ap.runtimeErrorDeliveryBackoff.timer) return false;
8810
+ ap.runtimeErrorDeliveryBackoff.timer = setTimeout(() => {
8811
+ ap.runtimeErrorDeliveryBackoff.timer = null;
8812
+ this.flushRuntimeErrorDeliveryBackoff(agentId);
8813
+ }, delayMs);
8814
+ ap.runtimeErrorDeliveryBackoff.timer.unref?.();
8815
+ return true;
8816
+ }
8817
+ recoverableRuntimeDeliveryBackoffReason(message, terminalFailure, stickyTerminalFailure, reasonOverride) {
8818
+ if (stickyTerminalFailure || terminalFailure?.actionRequired) return null;
8819
+ if (reasonOverride !== void 0) return reasonOverride;
8820
+ if (terminalFailure) return "recoverable_terminal_runtime_error";
8821
+ const runtimeErrorClass = buildRuntimeErrorDiagnosticEnvelope(message).spanAttrs.runtime_error_class;
8822
+ switch (runtimeErrorClass) {
8823
+ case "RateLimitError":
8824
+ return "rate_limited";
8825
+ case "ProviderServerError":
8826
+ return "provider_server_error";
8827
+ case "ProviderConnectionError":
8828
+ return "provider_connection_error";
8829
+ case "ProviderStreamError":
8830
+ return "provider_stream_error";
8831
+ default:
8832
+ return null;
8833
+ }
8834
+ }
8835
+ noteRuntimeErrorDeliveryBackoff(agentId, ap, message, terminalFailure, stickyTerminalFailure, reasonOverride) {
8836
+ const reason = this.recoverableRuntimeDeliveryBackoffReason(message, terminalFailure, stickyTerminalFailure, reasonOverride);
8837
+ if (!reason) return false;
8838
+ const attempts = ap.runtimeErrorDeliveryBackoff.attempts + 1;
8839
+ const exponentialDelayMs = this.runtimeErrorDeliveryBackoffBaseMs * 2 ** Math.max(0, attempts - 1);
8840
+ const cappedDelayMs = Math.min(this.runtimeErrorDeliveryBackoffMaxMs, exponentialDelayMs);
8841
+ const jitterMs = Math.floor(cappedDelayMs * this.runtimeErrorDeliveryBackoffJitterRatio * Math.random());
8842
+ const delayMs = Math.min(this.runtimeErrorDeliveryBackoffMaxMs, cappedDelayMs + jitterMs);
8843
+ if (ap.runtimeErrorDeliveryBackoff.timer) {
8844
+ clearTimeout(ap.runtimeErrorDeliveryBackoff.timer);
8845
+ ap.runtimeErrorDeliveryBackoff.timer = null;
8846
+ }
8847
+ ap.runtimeErrorDeliveryBackoff.attempts = attempts;
8848
+ ap.runtimeErrorDeliveryBackoff.reason = reason;
8849
+ ap.runtimeErrorDeliveryBackoff.untilMs = Date.now() + delayMs;
8850
+ if (ap.inbox.length > 0) {
8851
+ this.scheduleRuntimeErrorDeliveryBackoffFlush(agentId, ap);
8852
+ }
8853
+ this.recordDaemonTrace("daemon.agent.runtime_error_delivery_backoff", {
8854
+ agentId,
8855
+ runtime: ap.config.runtime,
8856
+ model: ap.config.model,
8857
+ launchId: ap.launchId || void 0,
8858
+ reason,
8859
+ attempts,
8860
+ delay_ms: delayMs,
8861
+ until_ms: ap.runtimeErrorDeliveryBackoff.untilMs,
8862
+ inbox_count: ap.inbox.length,
8863
+ pending_notification_count: ap.notifications.pendingCount
8864
+ });
8865
+ return true;
8866
+ }
8867
+ queueDeliveryForRuntimeErrorBackoff(agentId, ap, message) {
8868
+ const remainingMs = this.runtimeErrorDeliveryBackoffRemainingMs(ap);
8869
+ if (remainingMs <= 0) return false;
8870
+ ap.inbox.push(message);
8871
+ if (!ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
8872
+ ap.notifications.add();
8873
+ }
8874
+ const scheduled = this.scheduleRuntimeErrorDeliveryBackoffFlush(agentId, ap);
8875
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
8876
+ outcome: "queued_runtime_error_backoff",
8877
+ accepted: true,
8878
+ process_present: true,
8879
+ runtime: ap.config.runtime,
8880
+ session_id_present: Boolean(ap.sessionId),
8881
+ launchId: ap.launchId || void 0,
8882
+ is_idle: ap.isIdle,
8883
+ inbox_count: ap.inbox.length,
8884
+ pending_notification_count: ap.notifications.pendingCount,
8885
+ runtime_error_backoff_remaining_ms: remainingMs,
8886
+ runtime_error_backoff_attempts: ap.runtimeErrorDeliveryBackoff.attempts,
8887
+ runtime_error_backoff_reason: ap.runtimeErrorDeliveryBackoff.reason || void 0,
8888
+ runtime_error_backoff_timer_scheduled: scheduled || ap.runtimeErrorDeliveryBackoff.timer !== null
8889
+ }));
8890
+ return true;
8891
+ }
8892
+ flushRuntimeErrorDeliveryBackoff(agentId) {
8893
+ const ap = this.agents.get(agentId);
8894
+ if (!ap) return false;
8895
+ const remainingMs = this.runtimeErrorDeliveryBackoffRemainingMs(ap);
8896
+ if (remainingMs > 0) {
8897
+ this.scheduleRuntimeErrorDeliveryBackoffFlush(agentId, ap);
8898
+ return false;
8899
+ }
8900
+ if (ap.inbox.length === 0) return false;
8901
+ const reason = ap.runtimeErrorDeliveryBackoff.reason || "runtime_error_backoff";
8902
+ if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
8903
+ const messages = [...ap.inbox];
8904
+ ap.notifications.clearPending();
8905
+ ap.notifications.clearTimer();
8906
+ this.commitApmIdleState(agentId, ap, false);
8907
+ this.startRuntimeTrace(agentId, ap, "runtime-error-backoff-idle-delivery", messages);
8908
+ this.broadcastActivity(agentId, "working", "Message received");
8909
+ const accepted2 = this.deliverInboxUpdateViaStdin(
8910
+ agentId,
8911
+ ap,
8912
+ messages,
8913
+ "idle",
8914
+ "runtime_error_backoff_idle_delivery"
8915
+ );
8916
+ this.recordDaemonTrace("daemon.agent.runtime_error_delivery_backoff.flush", {
8917
+ agentId,
8918
+ runtime: ap.config.runtime,
8919
+ model: ap.config.model,
8920
+ launchId: ap.launchId || void 0,
8921
+ reason,
8922
+ mode: "idle",
8923
+ outcome: accepted2 ? "written" : "not_written",
8924
+ inbox_count: ap.inbox.length,
8925
+ messages_count: messages.length
8926
+ });
8927
+ return accepted2;
8928
+ }
8929
+ if (!ap.driver.supportsStdinNotification || !ap.sessionId) {
8930
+ this.recordDaemonTrace("daemon.agent.runtime_error_delivery_backoff.flush", {
8931
+ agentId,
8932
+ runtime: ap.config.runtime,
8933
+ model: ap.config.model,
8934
+ launchId: ap.launchId || void 0,
8935
+ reason,
8936
+ outcome: "queued_without_stdin",
8937
+ inbox_count: ap.inbox.length,
8938
+ session_id_present: Boolean(ap.sessionId),
8939
+ supports_stdin_notification: ap.driver.supportsStdinNotification
8940
+ });
8941
+ return false;
8942
+ }
8943
+ if (ap.notifications.pendingCount === 0) {
8944
+ ap.notifications.add(ap.inbox.length);
8945
+ }
8946
+ const accepted = this.sendStdinNotification(agentId);
8947
+ this.recordDaemonTrace("daemon.agent.runtime_error_delivery_backoff.flush", {
8948
+ agentId,
8949
+ runtime: ap.config.runtime,
8950
+ model: ap.config.model,
8951
+ launchId: ap.launchId || void 0,
8952
+ reason,
8953
+ mode: "busy",
8954
+ outcome: accepted ? "written" : "not_written",
8955
+ inbox_count: ap.inbox.length,
8956
+ pending_notification_count: ap.notifications.pendingCount
8957
+ });
8958
+ return accepted;
8959
+ }
8553
8960
  allPendingVisibleMessages(agentId) {
8554
8961
  const collect = (messages) => (messages ?? []).filter((message) => typeof message.seq === "number" && message.seq > 0);
8555
8962
  return [
@@ -8596,10 +9003,13 @@ var AgentProcessManager = class _AgentProcessManager {
8596
9003
  if (byTarget.size === 0) return;
8597
9004
  const boundaryMap = this.visibleBoundaryMap(agentId);
8598
9005
  const visibleIds = this.visibleMessageIdMap(agentId);
9006
+ const advancesBoundary = input.source === "spawn_wake_message" || input.source === "agent_api_events_local" || input.source.startsWith("stdin_");
8599
9007
  for (const [target, bucket] of byTarget) {
8600
- const highWaterSeq = Math.max(bucket.maxSeq, bucket.boundarySeq);
8601
- const previous = boundaryMap.get(target) ?? 0;
8602
- boundaryMap.set(target, Math.max(previous, highWaterSeq));
9008
+ if (advancesBoundary) {
9009
+ const highWaterSeq = Math.max(bucket.maxSeq, bucket.boundarySeq);
9010
+ const previous = boundaryMap.get(target) ?? 0;
9011
+ boundaryMap.set(target, Math.max(previous, highWaterSeq));
9012
+ }
8603
9013
  if (bucket.ids.size > 0) {
8604
9014
  let targetIds = visibleIds.get(target);
8605
9015
  if (!targetIds) {
@@ -8663,6 +9073,7 @@ var AgentProcessManager = class _AgentProcessManager {
8663
9073
  active.notifications.remove(removedActive);
8664
9074
  if (active.inbox.length === 0) {
8665
9075
  active.notifications.clear();
9076
+ this.clearRuntimeErrorDeliveryBackoff(active);
8666
9077
  }
8667
9078
  }
8668
9079
  const removedCount = removedActive + removedStarting;
@@ -9177,6 +9588,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9177
9588
  recentStdout: [],
9178
9589
  recentStderr: [],
9179
9590
  lastRuntimeError: null,
9591
+ runtimeErrorDeliveryBackoff: createRuntimeErrorDeliveryBackoffState(),
9180
9592
  spawnError: null,
9181
9593
  exitCode: null,
9182
9594
  exitSignal: null,
@@ -9279,6 +9691,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9279
9691
  const ap = this.agents.get(agentId);
9280
9692
  if (ap.runtime !== runtime) return;
9281
9693
  ap.notifications.clearTimer();
9694
+ this.clearRuntimeErrorDeliveryBackoff(ap);
9282
9695
  if (ap.pendingTrajectory?.timer) {
9283
9696
  clearTimeout(ap.pendingTrajectory.timer);
9284
9697
  }
@@ -9396,6 +9809,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9396
9809
  logger.warn(`[Agent ${agentId}] Recoverable provider stream failure (${reason}) \u2014 keeping agent wakeable`);
9397
9810
  this.sendAgentStatus(agentId, "active", ap.launchId);
9398
9811
  } else if (startupTimeoutTermination) {
9812
+ this.cacheStartupTimeoutRetryConfig(agentId, ap);
9399
9813
  logger.warn(`[Agent ${agentId}] Startup timeout cleanup completed (${reason})`);
9400
9814
  } else {
9401
9815
  this.idleAgentConfigs.delete(agentId);
@@ -9466,6 +9880,23 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9466
9880
  this.agents.delete(agentId);
9467
9881
  this.idleAgentConfigs.delete(agentId);
9468
9882
  }
9883
+ cacheStartupTimeoutRetryConfig(agentId, ap) {
9884
+ const retryConfig = {
9885
+ ...stripManagedRunnerCredential(ap.config),
9886
+ sessionId: ap.sessionId
9887
+ };
9888
+ this.idleAgentConfigs.set(agentId, {
9889
+ config: retryConfig,
9890
+ sessionId: ap.sessionId,
9891
+ launchId: ap.launchId
9892
+ });
9893
+ this.recordDaemonTrace("daemon.agent.startup_timeout.retry_config_cached", {
9894
+ agentId,
9895
+ launchId: ap.launchId || void 0,
9896
+ runtime: ap.config.runtime,
9897
+ session_id_present: Boolean(ap.sessionId)
9898
+ });
9899
+ }
9469
9900
  async buildSpawnConfig(agentId, config) {
9470
9901
  const baseConfig = config.serverUrl === this.serverUrl ? config : { ...config, serverUrl: this.serverUrl };
9471
9902
  const runnerConfig = await this.ensureManagedRunnerCredential(agentId, baseConfig);
@@ -9693,6 +10124,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9693
10124
  return;
9694
10125
  }
9695
10126
  ap.notifications.clearTimer();
10127
+ this.clearRuntimeErrorDeliveryBackoff(ap);
9696
10128
  if (ap.activityHeartbeat) {
9697
10129
  clearInterval(ap.activityHeartbeat);
9698
10130
  }
@@ -9747,6 +10179,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9747
10179
  this.deliveryTraceContexts.set(message, traceContext);
9748
10180
  }
9749
10181
  const transientDelivery = this.isTransientDelivery(message);
10182
+ if (!transientDelivery && this.isVisibleMessageModelSeen(agentId, formatMessageTarget(message), message)) {
10183
+ this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
10184
+ outcome: "dropped_already_consumed",
10185
+ accepted: true,
10186
+ process_present: Boolean(this.agents.get(agentId))
10187
+ }));
10188
+ return true;
10189
+ }
9750
10190
  const ap = this.agents.get(agentId);
9751
10191
  if (!ap) {
9752
10192
  if (this.agentsStarting.has(agentId) || this.queuedAgentStarts.has(agentId)) {
@@ -9890,6 +10330,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9890
10330
  return true;
9891
10331
  }
9892
10332
  }
10333
+ if (!transientDelivery && this.queueDeliveryForRuntimeErrorBackoff(agentId, ap, message)) {
10334
+ return true;
10335
+ }
9893
10336
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
9894
10337
  if (transientDelivery) {
9895
10338
  this.commitApmIdleState(agentId, ap, false);
@@ -10942,6 +11385,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
10942
11385
  return written;
10943
11386
  }
10944
11387
  case "deliver_stdin": {
11388
+ const runtimeErrorBackoffRemainingMs = this.runtimeErrorDeliveryBackoffRemainingMs(ap);
11389
+ if (runtimeErrorBackoffRemainingMs > 0) {
11390
+ const scheduled = this.scheduleRuntimeErrorDeliveryBackoffFlush(agentId, ap);
11391
+ this.recordApmGatedSteeringEffectTrace(agentId, ap, effect, {
11392
+ outcome: "suppressed_runtime_error_backoff",
11393
+ runtime_error_backoff_remaining_ms: runtimeErrorBackoffRemainingMs,
11394
+ runtime_error_backoff_attempts: ap.runtimeErrorDeliveryBackoff.attempts,
11395
+ runtime_error_backoff_reason: ap.runtimeErrorDeliveryBackoff.reason || void 0,
11396
+ runtime_error_backoff_timer_scheduled: scheduled || ap.runtimeErrorDeliveryBackoff.timer !== null,
11397
+ delivered_messages_count: 0
11398
+ });
11399
+ return false;
11400
+ }
10945
11401
  const messages = [...ap.inbox];
10946
11402
  ap.notifications.clear();
10947
11403
  if (messages.length === 0) {
@@ -11051,7 +11507,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11051
11507
  logger.warn(`[Agent ${agentId}] ${ap.driver.id} did not emit a startup runtime event within ${timeoutMs}ms; terminating process`);
11052
11508
  this.broadcastActivity(agentId, "error", detail, [{ kind: "text", text: `Error: ${detail}` }], ap.launchId);
11053
11509
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
11054
- this.idleAgentConfigs.delete(agentId);
11510
+ this.cacheStartupTimeoutRetryConfig(agentId, ap);
11055
11511
  try {
11056
11512
  this.runtimeExitTraceAttrs.set(ap.runtime, {
11057
11513
  stop_source: "startup_timeout",
@@ -11182,6 +11638,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11182
11638
  }
11183
11639
  if (event.kind === "internal_progress") {
11184
11640
  ap.runtimeProgress.noteInternalProgress();
11641
+ this.clearRuntimeErrorDeliveryBackoffAfterProgress(agentId, ap, event.kind);
11185
11642
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.internal_observed", {
11186
11643
  turn_outcome: "held",
11187
11644
  turn_subtype: "runtime_progress",
@@ -11206,6 +11663,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11206
11663
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
11207
11664
  this.queueTrajectoryText(agentId, "thinking", event.text);
11208
11665
  if (ap) {
11666
+ this.clearRuntimeErrorDeliveryBackoffAfterProgress(agentId, ap, event.kind);
11209
11667
  const reduction = reduceApmGatedAssistantContinuation(ap.gatedSteering);
11210
11668
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "thinking" });
11211
11669
  }
@@ -11215,6 +11673,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11215
11673
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
11216
11674
  this.queueTrajectoryText(agentId, "text", event.text);
11217
11675
  if (ap) {
11676
+ this.clearRuntimeErrorDeliveryBackoffAfterProgress(agentId, ap, event.kind);
11218
11677
  const reduction = reduceApmGatedAssistantContinuation(ap.gatedSteering);
11219
11678
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "text" });
11220
11679
  }
@@ -11225,6 +11684,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11225
11684
  this.flushPendingTrajectory(agentId);
11226
11685
  const invocation = normalizeToolDisplayInvocation(event.name, event.input);
11227
11686
  if (ap) {
11687
+ this.clearRuntimeErrorDeliveryBackoffAfterProgress(agentId, ap, event.kind);
11228
11688
  const reduction = reduceApmGatedToolUse(ap.gatedSteering, { kind: "tool_call" });
11229
11689
  this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
11230
11690
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, {
@@ -11244,6 +11704,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11244
11704
  case "tool_output": {
11245
11705
  const invocation = normalizeToolDisplayInvocation(event.name, {});
11246
11706
  if (ap) {
11707
+ this.clearRuntimeErrorDeliveryBackoffAfterProgress(agentId, ap, event.kind);
11247
11708
  const reduction = reduceApmGatedToolUse(ap.gatedSteering, { kind: "tool_output" });
11248
11709
  this.recordRuntimeTraceEvent(agentId, ap, "tool.output.observed", { tool: invocation.toolName });
11249
11710
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.continuation.expected");
@@ -11303,6 +11764,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11303
11764
  this.commitApmIdleState(agentId, ap, true);
11304
11765
  if (stickyTerminalFailure) {
11305
11766
  this.broadcastActivity(agentId, "error", stickyTerminalFailure.detail);
11767
+ } else if (ap.lastRuntimeError && ap.runtimeErrorDeliveryBackoff.attempts > 0) {
11768
+ this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
11306
11769
  } else {
11307
11770
  this.broadcastActivity(agentId, "online", "Idle");
11308
11771
  }
@@ -11352,12 +11815,19 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11352
11815
  visibleErrorMessage = formatRuntimeLoginRequiredMessage(ap.driver.id);
11353
11816
  }
11354
11817
  const shouldDisableToolBoundaryFlush = ap.runtime.descriptor.busyDelivery === "gated" && this.isThinkingBlockMutationError(event.message);
11355
- const terminalFailure = classifyTerminalFailure(ap);
11818
+ const backoffFailPoint = this.runtimeErrorDeliveryBackoffFailPointForTesting?.({
11819
+ agentId,
11820
+ message: event.message
11821
+ }) ?? null;
11822
+ const terminalFailure = backoffFailPoint && Object.prototype.hasOwnProperty.call(backoffFailPoint, "terminalFailure") ? backoffFailPoint.terminalFailure ?? null : classifyTerminalFailure(ap);
11823
+ const stickyTerminalFailure = backoffFailPoint && Object.prototype.hasOwnProperty.call(backoffFailPoint, "stickyTerminalFailure") ? backoffFailPoint.stickyTerminalFailure ?? null : classifyStickyTerminalFailure(ap);
11824
+ const backoffReasonOverride = backoffFailPoint && Object.prototype.hasOwnProperty.call(backoffFailPoint, "reason") ? backoffFailPoint.reason ?? null : void 0;
11356
11825
  const reduction = reduceApmGatedError(ap.gatedSteering, {
11357
11826
  disableToolBoundaryFlush: shouldDisableToolBoundaryFlush,
11358
11827
  terminalWakeable: Boolean(ap.driver.supportsStdinNotification && terminalFailure && !terminalFailure.actionRequired)
11359
11828
  });
11360
11829
  this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "error" });
11830
+ this.noteRuntimeErrorDeliveryBackoff(agentId, ap, event.message, terminalFailure, stickyTerminalFailure, backoffReasonOverride);
11361
11831
  if (reduction.shouldDisableToolBoundaryFlush) {
11362
11832
  this.recordGatedSteeringEvent(agentId, ap, "disabled", {
11363
11833
  reason: "thinking_block_mutation_error",
@@ -11378,7 +11848,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11378
11848
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
11379
11849
  });
11380
11850
  if (ap.driver.supportsStdinNotification && terminalFailure) {
11381
- const stickyTerminalFailure = classifyStickyTerminalFailure(ap);
11382
11851
  if (terminalFailure.actionRequired) {
11383
11852
  logger.warn(`[Agent ${agentId}] ${ap.driver.id} auth requires user action; terminating runtime process`);
11384
11853
  try {
@@ -11396,7 +11865,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11396
11865
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
11397
11866
  logger.warn(`[Agent ${agentId}] ${ap.driver.id} terminal runtime error requires explicit recovery`);
11398
11867
  } else {
11399
- ap.notifications.clear();
11868
+ ap.notifications.clearPending();
11869
+ ap.notifications.clearTimer();
11400
11870
  logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
11401
11871
  }
11402
11872
  }
@@ -11507,6 +11977,27 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11507
11977
  if (count === 0) return false;
11508
11978
  if (ap.isIdle) return false;
11509
11979
  if (!ap.sessionId) return false;
11980
+ const runtimeErrorBackoffRemainingMs = this.runtimeErrorDeliveryBackoffRemainingMs(ap);
11981
+ if (runtimeErrorBackoffRemainingMs > 0) {
11982
+ ap.notifications.add(count);
11983
+ const scheduled = this.scheduleRuntimeErrorDeliveryBackoffFlush(agentId, ap);
11984
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
11985
+ agentId,
11986
+ runtime: ap.config.runtime,
11987
+ model: ap.config.model,
11988
+ launchId: ap.launchId || void 0,
11989
+ outcome: "suppressed_runtime_error_backoff",
11990
+ mode: "busy",
11991
+ pending_notification_count: count,
11992
+ inbox_count: ap.inbox.length,
11993
+ session_id_present: true,
11994
+ runtime_error_backoff_remaining_ms: runtimeErrorBackoffRemainingMs,
11995
+ runtime_error_backoff_attempts: ap.runtimeErrorDeliveryBackoff.attempts,
11996
+ runtime_error_backoff_reason: ap.runtimeErrorDeliveryBackoff.reason || void 0,
11997
+ runtime_error_backoff_timer_scheduled: scheduled || ap.runtimeErrorDeliveryBackoff.timer !== null
11998
+ });
11999
+ return false;
12000
+ }
11510
12001
  if (ap.gatedSteering.compacting) {
11511
12002
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.delivery_suppressed", {
11512
12003
  pendingNotificationCount: count,
@@ -11522,6 +12013,22 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
11522
12013
  const inboxCount = ap.inbox.length;
11523
12014
  if (inboxCount === 0) return false;
11524
12015
  const changedMessages = ap.inbox.slice(Math.max(0, ap.inbox.length - count));
12016
+ const noticeFingerprint = computeInboxNoticeFingerprint(changedMessages);
12017
+ if (ap.notifications.isDuplicateNotice(noticeFingerprint, ap.sessionId)) {
12018
+ this.recordDaemonTrace("daemon.agent.stdin_notification", {
12019
+ agentId,
12020
+ runtime: ap.config.runtime,
12021
+ model: ap.config.model,
12022
+ launchId: ap.launchId || void 0,
12023
+ outcome: "suppressed_duplicate",
12024
+ mode: "busy",
12025
+ pending_notification_count: count,
12026
+ inbox_count: ap.inbox.length,
12027
+ session_id_present: true
12028
+ });
12029
+ logger.info(`[Agent ${agentId}] Suppressing duplicate stdin inbox notice (unread-set unchanged since last write); pending=${ap.inbox.length}`);
12030
+ return false;
12031
+ }
11525
12032
  const inboxRows = projectAgentInboxSnapshot(changedMessages);
11526
12033
  const notification = `[Slock inbox notice:
11527
12034
  ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
@@ -11559,6 +12066,7 @@ ${formatAgentInboxDelta(inboxRows, { totalPendingMessages: inboxCount })}]`;
11559
12066
  inbox_target_count: inboxRows.length,
11560
12067
  session_id_present: true
11561
12068
  });
12069
+ ap.notifications.recordNoticeWritten(noticeFingerprint, ap.sessionId);
11562
12070
  return true;
11563
12071
  } else {
11564
12072
  ap.notifications.add(count);
@@ -11855,6 +12363,8 @@ var DaemonConnection = class {
11855
12363
  lastDroppedSendLogAt = 0;
11856
12364
  lastInboundAt = null;
11857
12365
  lastInboundMessageKind = null;
12366
+ pendingActivityByAgent = /* @__PURE__ */ new Map();
12367
+ latestObservedLaunchIdByAgent = /* @__PURE__ */ new Map();
11858
12368
  constructor(options) {
11859
12369
  this.options = options;
11860
12370
  this.clock = options.clock ?? systemClock;
@@ -11868,6 +12378,7 @@ var DaemonConnection = class {
11868
12378
  }
11869
12379
  disconnect() {
11870
12380
  this.shouldConnect = false;
12381
+ this.pendingActivityByAgent.clear();
11871
12382
  this.clearWatchdog();
11872
12383
  if (this.reconnectTimer) {
11873
12384
  this.clock.clearTimeout(this.reconnectTimer);
@@ -11881,9 +12392,15 @@ var DaemonConnection = class {
11881
12392
  }
11882
12393
  send(msg) {
11883
12394
  if (this.ws?.readyState === WebSocket.OPEN) {
12395
+ this.observeLaunchIdentity(msg);
12396
+ if (msg.type === "agent:activity") {
12397
+ this.pendingActivityByAgent.delete(msg.agentId);
12398
+ }
11884
12399
  this.ws.send(JSON.stringify(msg));
11885
12400
  return;
11886
12401
  }
12402
+ this.observeLaunchIdentity(msg);
12403
+ this.queueReplayableMessage(msg);
11887
12404
  const now = this.clock.now();
11888
12405
  if (now - this.lastDroppedSendLogAt > 5e3) {
11889
12406
  this.lastDroppedSendLogAt = now;
@@ -11926,6 +12443,7 @@ var DaemonConnection = class {
11926
12443
  reconnect_attempt: priorReconnectAttempt,
11927
12444
  inbound_watchdog_ms: this.options.inboundWatchdogMs ?? INBOUND_WATCHDOG_MS
11928
12445
  });
12446
+ this.flushPendingActivity(ws);
11929
12447
  this.options.onConnect();
11930
12448
  });
11931
12449
  ws.on("message", (data) => {
@@ -12023,6 +12541,65 @@ var DaemonConnection = class {
12023
12541
  this.lastInboundAt = this.clock.now();
12024
12542
  this.lastInboundMessageKind = messageKind;
12025
12543
  }
12544
+ queueReplayableMessage(msg) {
12545
+ if (msg.type !== "agent:activity") return;
12546
+ const latestLaunchId = this.latestObservedLaunchIdByAgent.get(msg.agentId);
12547
+ if (msg.launchId && latestLaunchId && msg.launchId !== latestLaunchId) {
12548
+ this.trace("daemon.connection.pending_activity_invalidated", {
12549
+ reason: "launch_changed",
12550
+ message_type: msg.type,
12551
+ agentId: msg.agentId,
12552
+ stale_launch_id_present: true,
12553
+ next_launch_id_present: true
12554
+ });
12555
+ return;
12556
+ }
12557
+ this.pendingActivityByAgent.set(msg.agentId, msg);
12558
+ }
12559
+ observeLaunchIdentity(msg) {
12560
+ const identity = this.agentLaunchIdentity(msg);
12561
+ if (!identity?.launchId) return;
12562
+ if (msg.type !== "agent:activity") {
12563
+ this.latestObservedLaunchIdByAgent.set(identity.agentId, identity.launchId);
12564
+ }
12565
+ const pending = this.pendingActivityByAgent.get(identity.agentId);
12566
+ if (!pending || pending.launchId === identity.launchId) return;
12567
+ this.pendingActivityByAgent.delete(identity.agentId);
12568
+ this.trace("daemon.connection.pending_activity_invalidated", {
12569
+ reason: "launch_changed",
12570
+ message_type: msg.type,
12571
+ agentId: identity.agentId,
12572
+ stale_launch_id_present: Boolean(pending.launchId),
12573
+ next_launch_id_present: true
12574
+ });
12575
+ }
12576
+ agentLaunchIdentity(msg) {
12577
+ switch (msg.type) {
12578
+ case "agent:activity":
12579
+ case "agent:status":
12580
+ case "agent:session":
12581
+ case "agent:runtime_profile":
12582
+ case "agent:runtime_profile:migration:ack":
12583
+ case "agent:runtime_profile:migration_done":
12584
+ case "agent:runtime_profile:daemon_release_notice:ack":
12585
+ return { agentId: msg.agentId, launchId: msg.launchId };
12586
+ default:
12587
+ return null;
12588
+ }
12589
+ }
12590
+ flushPendingActivity(ws) {
12591
+ if (this.pendingActivityByAgent.size === 0) return;
12592
+ if (this.ws !== ws || ws.readyState !== WebSocket.OPEN) return;
12593
+ const pending = [...this.pendingActivityByAgent.values()];
12594
+ this.pendingActivityByAgent.clear();
12595
+ for (const msg of pending) {
12596
+ ws.send(JSON.stringify(msg));
12597
+ }
12598
+ this.trace("daemon.connection.outbound_replayed", {
12599
+ message_type: "agent:activity",
12600
+ message_count: pending.length
12601
+ });
12602
+ }
12026
12603
  lastInboundAgeBucket() {
12027
12604
  return durationMsBucket(this.lastInboundAt == null ? null : this.clock.now() - this.lastInboundAt);
12028
12605
  }