larkin 0.5.16 → 0.5.18
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/CHANGELOG.md +4 -0
- package/README.md +11 -0
- package/dist/agent/config-cli-contract.cjs +3 -1
- package/dist/agent/config-cli-contract.mjs +3 -1
- package/dist/agent/context-prompt.cjs +7 -7
- package/dist/agent/context-prompt.mjs +7 -7
- package/dist/app/agent-cli.mjs +19 -2
- package/dist/app/agent-config.mjs +33 -4
- package/dist/app/cli.mjs +16 -0
- package/dist/app/lark-cli.mjs +77 -119
- package/dist/app/lark.mjs +14 -0
- package/dist/app/runtime-agent-config.mjs +15 -4
- package/dist/app/runtime-context.mjs +17 -0
- package/dist/dashboard/dashboard-config-controller.mjs +11 -0
- package/dist/dashboard/web/assets/dashboard.js +15 -15
- package/dist/feishu/host-shell.cjs +11 -1
- package/dist/feishu/host-shell.mjs +12 -2
- package/dist/feishu/im-freshness-adapter.cjs +2 -2
- package/dist/feishu/im-freshness-adapter.mjs +2 -2
- package/dist/platform/config.cjs +85 -2
- package/dist/platform/config.mjs +83 -2
- package/dist/runtime/runtime-host.cjs +112 -9
- package/dist/runtime/runtime-host.mjs +112 -9
- package/package.json +1 -1
|
@@ -308,6 +308,16 @@ function createRuntimeHost(options) {
|
|
|
308
308
|
maxAttempts: options.retryPolicy?.maxAttempts ?? 6,
|
|
309
309
|
stableWindowMs: options.retryPolicy?.stableWindowMs ?? 30_000,
|
|
310
310
|
};
|
|
311
|
+
const turnEndRetryPolicy = {
|
|
312
|
+
baseDelayMs: options.turnEndRetryPolicy?.baseDelayMs ?? 1_000,
|
|
313
|
+
maxDelayMs: options.turnEndRetryPolicy?.maxDelayMs ?? 30_000,
|
|
314
|
+
maxAttempts: options.turnEndRetryPolicy?.maxAttempts ?? 3,
|
|
315
|
+
};
|
|
316
|
+
const turnEndRetryScheduler = options.turnEndRetryScheduler ?? {
|
|
317
|
+
now: Date.now,
|
|
318
|
+
setTimeout,
|
|
319
|
+
clearTimeout,
|
|
320
|
+
};
|
|
311
321
|
const emit = (event) => {
|
|
312
322
|
if (event.type === "delivery")
|
|
313
323
|
telemetry?.delivery(event.agentId, event.messageId, event.status);
|
|
@@ -316,6 +326,7 @@ function createRuntimeHost(options) {
|
|
|
316
326
|
};
|
|
317
327
|
const runtimeEnv = (config, generation) => {
|
|
318
328
|
const base = {
|
|
329
|
+
LARKIN_RUNTIME: "1",
|
|
319
330
|
LARKIN_AGENT_ID: config.agentId,
|
|
320
331
|
...(generation ? {
|
|
321
332
|
LARKIN_RUNTIME_OBSERVATION_GENERATION: generation,
|
|
@@ -361,6 +372,8 @@ function createRuntimeHost(options) {
|
|
|
361
372
|
record.updatedAt = now();
|
|
362
373
|
agent.records.set(record.deliveryId, record);
|
|
363
374
|
agent.byMessage.set(record.messageId, record.deliveryId);
|
|
375
|
+
if (status === "consumed" || status === "error")
|
|
376
|
+
clearPendingRetryTimerWhenIdle(agent);
|
|
364
377
|
emitConsumed(agent, persist(agent));
|
|
365
378
|
return agent.records.get(record.deliveryId) ?? record;
|
|
366
379
|
};
|
|
@@ -466,8 +479,42 @@ function createRuntimeHost(options) {
|
|
|
466
479
|
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: next.deliveryId, messageId: next.messageId, status: "consumed" });
|
|
467
480
|
}
|
|
468
481
|
}
|
|
482
|
+
clearPendingRetryTimerWhenIdle(agent);
|
|
469
483
|
};
|
|
470
484
|
const TURN_END_RETRY_REASON = "runtime turn ended before Inbox consumption was observed";
|
|
485
|
+
const turnEndRetryDelayMs = (attempt) => Math.min(turnEndRetryPolicy.maxDelayMs, turnEndRetryPolicy.baseDelayMs * 2 ** (attempt - 1));
|
|
486
|
+
const isRetryDue = (record) => !record.retryNotBefore || Number.isNaN(Date.parse(record.retryNotBefore))
|
|
487
|
+
|| Date.parse(record.retryNotBefore) <= turnEndRetryScheduler.now();
|
|
488
|
+
const clearDeliveryRetryTimer = (agent) => {
|
|
489
|
+
if (!agent.deliveryRetryTimer)
|
|
490
|
+
return;
|
|
491
|
+
turnEndRetryScheduler.clearTimeout(agent.deliveryRetryTimer);
|
|
492
|
+
agent.deliveryRetryTimer = null;
|
|
493
|
+
};
|
|
494
|
+
function clearPendingRetryTimerWhenIdle(agent) {
|
|
495
|
+
if (!agent.deliveryRetryTimer)
|
|
496
|
+
return;
|
|
497
|
+
if ([...agent.records.values()].some((record) => record.status === "pending" && record.retryNotBefore))
|
|
498
|
+
return;
|
|
499
|
+
clearDeliveryRetryTimer(agent);
|
|
500
|
+
}
|
|
501
|
+
const schedulePendingRetry = (agent) => {
|
|
502
|
+
if (agent.stopped || agent.deliveryRetryTimer)
|
|
503
|
+
return;
|
|
504
|
+
const next = [...agent.records.values()]
|
|
505
|
+
.filter((record) => record.status === "pending" && record.retryNotBefore)
|
|
506
|
+
.map((record) => Date.parse(record.retryNotBefore))
|
|
507
|
+
.filter((at) => Number.isFinite(at))
|
|
508
|
+
.sort((left, right) => left - right)[0];
|
|
509
|
+
if (next === undefined)
|
|
510
|
+
return;
|
|
511
|
+
const delay = Math.max(0, next - turnEndRetryScheduler.now());
|
|
512
|
+
agent.deliveryRetryTimer = turnEndRetryScheduler.setTimeout(() => {
|
|
513
|
+
agent.deliveryRetryTimer = null;
|
|
514
|
+
void retryPending(agent);
|
|
515
|
+
}, delay);
|
|
516
|
+
agent.deliveryRetryTimer.unref?.();
|
|
517
|
+
};
|
|
471
518
|
const INBOX_UPDATE_PROMOTE_REASON = "accepted inbox_update promoted after Agent became idle";
|
|
472
519
|
const isInboxUpdateKind = (kind) => kind === "inbox_update" || kind === "inbox-update";
|
|
473
520
|
const agentIsIdleForInboxScan = (agent) => !agent.stopped && !agent.busy && !agent.submitting && !agent.turnInProgress && !agent.starting && Boolean(agent.session);
|
|
@@ -495,13 +542,32 @@ function createRuntimeHost(options) {
|
|
|
495
542
|
for (const record of agent.records.values()) {
|
|
496
543
|
if (record.status !== "accepted")
|
|
497
544
|
continue;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
545
|
+
const attempts = record.turnEndRetryAttempts ?? 0;
|
|
546
|
+
if (attempts >= turnEndRetryPolicy.maxAttempts) {
|
|
547
|
+
record.reason = `turn-end redelivery exhausted after ${turnEndRetryPolicy.maxAttempts} attempts`;
|
|
548
|
+
record.retryable = false;
|
|
549
|
+
const terminal = setRecord(agent, record, "error");
|
|
550
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: terminal.deliveryId,
|
|
551
|
+
messageId: terminal.messageId, status: "error", reason: terminal.reason });
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
const attempt = attempts + 1;
|
|
555
|
+
const delay = turnEndRetryDelayMs(attempt);
|
|
556
|
+
record.turnEndRetryAttempts = attempt;
|
|
557
|
+
record.retryNotBefore = new Date(turnEndRetryScheduler.now() + delay).toISOString();
|
|
558
|
+
record.reason = TURN_END_RETRY_REASON;
|
|
559
|
+
record.retryable = true;
|
|
560
|
+
record.input = { ...record.input, attempt: (record.input.attempt ?? 0) + 1 };
|
|
561
|
+
const deferred = setRecord(agent, record, "pending");
|
|
562
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: deferred.deliveryId,
|
|
563
|
+
messageId: deferred.messageId, status: "deferred", reason: TURN_END_RETRY_REASON });
|
|
564
|
+
}
|
|
565
|
+
schedulePendingRetry(agent);
|
|
501
566
|
return;
|
|
502
567
|
}
|
|
503
568
|
const consumed = [];
|
|
504
569
|
const deferred = [];
|
|
570
|
+
const exhausted = [];
|
|
505
571
|
try {
|
|
506
572
|
store.withInboxTransaction(() => {
|
|
507
573
|
const inboxIds = new Set(store.readNdjson("inbox").flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
|
|
@@ -522,8 +588,31 @@ function createRuntimeHost(options) {
|
|
|
522
588
|
if (current.status !== "accepted" || candidate.status !== "accepted" || !inboxIds.has(candidate.messageId)) {
|
|
523
589
|
return candidate;
|
|
524
590
|
}
|
|
525
|
-
const
|
|
526
|
-
|
|
591
|
+
const attempts = candidate.turnEndRetryAttempts ?? 0;
|
|
592
|
+
if (attempts >= turnEndRetryPolicy.maxAttempts) {
|
|
593
|
+
const terminal = { ...candidate, status: "error", updatedAt: now(),
|
|
594
|
+
reason: `turn-end redelivery exhausted after ${turnEndRetryPolicy.maxAttempts} attempts`,
|
|
595
|
+
retryable: false };
|
|
596
|
+
changed = true;
|
|
597
|
+
pendingUpdates.push(terminal);
|
|
598
|
+
return terminal;
|
|
599
|
+
}
|
|
600
|
+
const attempt = attempts + 1;
|
|
601
|
+
const delay = turnEndRetryDelayMs(attempt);
|
|
602
|
+
const staleInput = candidate.input && typeof candidate.input === "object" ? candidate.input : null;
|
|
603
|
+
const next = {
|
|
604
|
+
...candidate,
|
|
605
|
+
status: "pending",
|
|
606
|
+
updatedAt: now(),
|
|
607
|
+
reason: TURN_END_RETRY_REASON,
|
|
608
|
+
retryable: true,
|
|
609
|
+
turnEndRetryAttempts: attempt,
|
|
610
|
+
retryNotBefore: new Date(turnEndRetryScheduler.now() + delay).toISOString(),
|
|
611
|
+
input: {
|
|
612
|
+
...(staleInput ?? { inputId: candidate.deliveryId, deliveryId: candidate.deliveryId, kind: "wake", text: "" }),
|
|
613
|
+
attempt: (Number.isSafeInteger(staleInput?.attempt) && Number(staleInput?.attempt) >= 0 ? Number(staleInput.attempt) : 0) + 1,
|
|
614
|
+
},
|
|
615
|
+
};
|
|
527
616
|
changed = true;
|
|
528
617
|
pendingUpdates.push(next);
|
|
529
618
|
return next;
|
|
@@ -532,7 +621,10 @@ function createRuntimeHost(options) {
|
|
|
532
621
|
store.writeJson("runtimeDeliveries", { ...disk, records });
|
|
533
622
|
for (const next of pendingUpdates) {
|
|
534
623
|
agent.records.set(next.deliveryId, next);
|
|
535
|
-
|
|
624
|
+
if (next.status === "error")
|
|
625
|
+
exhausted.push(next);
|
|
626
|
+
else
|
|
627
|
+
deferred.push(next);
|
|
536
628
|
}
|
|
537
629
|
}
|
|
538
630
|
});
|
|
@@ -545,6 +637,11 @@ function createRuntimeHost(options) {
|
|
|
545
637
|
for (const record of deferred)
|
|
546
638
|
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
547
639
|
deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason: TURN_END_RETRY_REASON });
|
|
640
|
+
for (const record of exhausted)
|
|
641
|
+
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
642
|
+
deliveryId: record.deliveryId, messageId: record.messageId, status: "error", reason: record.reason });
|
|
643
|
+
if (deferred.length)
|
|
644
|
+
schedulePendingRetry(agent);
|
|
548
645
|
};
|
|
549
646
|
// Production callers persist the canonical Inbox before calling deliver(). If a
|
|
550
647
|
// concurrent drain wins between that append and ledger creation, close only the
|
|
@@ -743,9 +840,11 @@ function createRuntimeHost(options) {
|
|
|
743
840
|
if (idleGate)
|
|
744
841
|
await idleGate;
|
|
745
842
|
}
|
|
746
|
-
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
843
|
+
const record = [...agent.records.values()].find((candidate) => (candidate.status === "pending" && isRetryDue(candidate)) || candidate.status === "submitting");
|
|
747
844
|
if (record)
|
|
748
845
|
await submit(agent, record, false);
|
|
846
|
+
else
|
|
847
|
+
schedulePendingRetry(agent);
|
|
749
848
|
})();
|
|
750
849
|
agent.retryPendingInFlight = run;
|
|
751
850
|
try {
|
|
@@ -1519,6 +1618,8 @@ function createRuntimeHost(options) {
|
|
|
1519
1618
|
clearInterval(previous.poller);
|
|
1520
1619
|
if (previous.retryTimer)
|
|
1521
1620
|
clearTimeout(previous.retryTimer);
|
|
1621
|
+
if (previous.deliveryRetryTimer)
|
|
1622
|
+
turnEndRetryScheduler.clearTimeout(previous.deliveryRetryTimer);
|
|
1522
1623
|
if (previous.stabilityTimer)
|
|
1523
1624
|
clearTimeout(previous.stabilityTimer);
|
|
1524
1625
|
const previousFailure = currentAuthFailure(previous);
|
|
@@ -1530,7 +1631,7 @@ function createRuntimeHost(options) {
|
|
|
1530
1631
|
}, previousFailure));
|
|
1531
1632
|
const candidate = {
|
|
1532
1633
|
...previous, config, adapter, session, launchId: node_crypto_1.default.randomUUID(), busy: false, submitting: false,
|
|
1533
|
-
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
|
|
1634
|
+
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null, deliveryRetryTimer: null,
|
|
1534
1635
|
recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
|
|
1535
1636
|
disabledReason: null, configurationRecovery: null,
|
|
1536
1637
|
authFailureActive: scoped && previous.authFailureActive,
|
|
@@ -1623,6 +1724,7 @@ function createRuntimeHost(options) {
|
|
|
1623
1724
|
if (pendingAfterCreate > 0) {
|
|
1624
1725
|
throw new RuntimeSessionResetError("inbox_backlog", `Agent ${agentId} received Inbox backlog during reset`, pendingAfterCreate);
|
|
1625
1726
|
}
|
|
1727
|
+
clearDeliveryRetryTimer(agent);
|
|
1626
1728
|
agent.generation += 1;
|
|
1627
1729
|
agent.launchId = node_crypto_1.default.randomUUID();
|
|
1628
1730
|
agent.config.sessionId = null;
|
|
@@ -1893,7 +1995,7 @@ function createRuntimeHost(options) {
|
|
|
1893
1995
|
retryPendingInFlight: null,
|
|
1894
1996
|
records: new Map(persisted.records.map((record) => [record.deliveryId, record])),
|
|
1895
1997
|
byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
|
|
1896
|
-
generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
|
|
1998
|
+
generation: 0, poller: null, retryTimer: null, deliveryRetryTimer: null, recreateAttempts: 0,
|
|
1897
1999
|
stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
|
|
1898
2000
|
configurationRecovery: null, stateStore,
|
|
1899
2001
|
compactionBreaker: config.runtime === "pi" && stateStore
|
|
@@ -2065,6 +2167,7 @@ function createRuntimeHost(options) {
|
|
|
2065
2167
|
clearInterval(agent.poller);
|
|
2066
2168
|
if (agent.retryTimer)
|
|
2067
2169
|
clearTimeout(agent.retryTimer);
|
|
2170
|
+
clearDeliveryRetryTimer(agent);
|
|
2068
2171
|
if (agent.stabilityTimer)
|
|
2069
2172
|
clearTimeout(agent.stabilityTimer);
|
|
2070
2173
|
if (agent.busy)
|
|
@@ -299,6 +299,16 @@ export function createRuntimeHost(options) {
|
|
|
299
299
|
maxAttempts: options.retryPolicy?.maxAttempts ?? 6,
|
|
300
300
|
stableWindowMs: options.retryPolicy?.stableWindowMs ?? 30_000,
|
|
301
301
|
};
|
|
302
|
+
const turnEndRetryPolicy = {
|
|
303
|
+
baseDelayMs: options.turnEndRetryPolicy?.baseDelayMs ?? 1_000,
|
|
304
|
+
maxDelayMs: options.turnEndRetryPolicy?.maxDelayMs ?? 30_000,
|
|
305
|
+
maxAttempts: options.turnEndRetryPolicy?.maxAttempts ?? 3,
|
|
306
|
+
};
|
|
307
|
+
const turnEndRetryScheduler = options.turnEndRetryScheduler ?? {
|
|
308
|
+
now: Date.now,
|
|
309
|
+
setTimeout,
|
|
310
|
+
clearTimeout,
|
|
311
|
+
};
|
|
302
312
|
const emit = (event) => {
|
|
303
313
|
if (event.type === "delivery")
|
|
304
314
|
telemetry?.delivery(event.agentId, event.messageId, event.status);
|
|
@@ -307,6 +317,7 @@ export function createRuntimeHost(options) {
|
|
|
307
317
|
};
|
|
308
318
|
const runtimeEnv = (config, generation) => {
|
|
309
319
|
const base = {
|
|
320
|
+
LARKIN_RUNTIME: "1",
|
|
310
321
|
LARKIN_AGENT_ID: config.agentId,
|
|
311
322
|
...(generation ? {
|
|
312
323
|
LARKIN_RUNTIME_OBSERVATION_GENERATION: generation,
|
|
@@ -352,6 +363,8 @@ export function createRuntimeHost(options) {
|
|
|
352
363
|
record.updatedAt = now();
|
|
353
364
|
agent.records.set(record.deliveryId, record);
|
|
354
365
|
agent.byMessage.set(record.messageId, record.deliveryId);
|
|
366
|
+
if (status === "consumed" || status === "error")
|
|
367
|
+
clearPendingRetryTimerWhenIdle(agent);
|
|
355
368
|
emitConsumed(agent, persist(agent));
|
|
356
369
|
return agent.records.get(record.deliveryId) ?? record;
|
|
357
370
|
};
|
|
@@ -457,8 +470,42 @@ export function createRuntimeHost(options) {
|
|
|
457
470
|
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: next.deliveryId, messageId: next.messageId, status: "consumed" });
|
|
458
471
|
}
|
|
459
472
|
}
|
|
473
|
+
clearPendingRetryTimerWhenIdle(agent);
|
|
460
474
|
};
|
|
461
475
|
const TURN_END_RETRY_REASON = "runtime turn ended before Inbox consumption was observed";
|
|
476
|
+
const turnEndRetryDelayMs = (attempt) => Math.min(turnEndRetryPolicy.maxDelayMs, turnEndRetryPolicy.baseDelayMs * 2 ** (attempt - 1));
|
|
477
|
+
const isRetryDue = (record) => !record.retryNotBefore || Number.isNaN(Date.parse(record.retryNotBefore))
|
|
478
|
+
|| Date.parse(record.retryNotBefore) <= turnEndRetryScheduler.now();
|
|
479
|
+
const clearDeliveryRetryTimer = (agent) => {
|
|
480
|
+
if (!agent.deliveryRetryTimer)
|
|
481
|
+
return;
|
|
482
|
+
turnEndRetryScheduler.clearTimeout(agent.deliveryRetryTimer);
|
|
483
|
+
agent.deliveryRetryTimer = null;
|
|
484
|
+
};
|
|
485
|
+
function clearPendingRetryTimerWhenIdle(agent) {
|
|
486
|
+
if (!agent.deliveryRetryTimer)
|
|
487
|
+
return;
|
|
488
|
+
if ([...agent.records.values()].some((record) => record.status === "pending" && record.retryNotBefore))
|
|
489
|
+
return;
|
|
490
|
+
clearDeliveryRetryTimer(agent);
|
|
491
|
+
}
|
|
492
|
+
const schedulePendingRetry = (agent) => {
|
|
493
|
+
if (agent.stopped || agent.deliveryRetryTimer)
|
|
494
|
+
return;
|
|
495
|
+
const next = [...agent.records.values()]
|
|
496
|
+
.filter((record) => record.status === "pending" && record.retryNotBefore)
|
|
497
|
+
.map((record) => Date.parse(record.retryNotBefore))
|
|
498
|
+
.filter((at) => Number.isFinite(at))
|
|
499
|
+
.sort((left, right) => left - right)[0];
|
|
500
|
+
if (next === undefined)
|
|
501
|
+
return;
|
|
502
|
+
const delay = Math.max(0, next - turnEndRetryScheduler.now());
|
|
503
|
+
agent.deliveryRetryTimer = turnEndRetryScheduler.setTimeout(() => {
|
|
504
|
+
agent.deliveryRetryTimer = null;
|
|
505
|
+
void retryPending(agent);
|
|
506
|
+
}, delay);
|
|
507
|
+
agent.deliveryRetryTimer.unref?.();
|
|
508
|
+
};
|
|
462
509
|
const INBOX_UPDATE_PROMOTE_REASON = "accepted inbox_update promoted after Agent became idle";
|
|
463
510
|
const isInboxUpdateKind = (kind) => kind === "inbox_update" || kind === "inbox-update";
|
|
464
511
|
const agentIsIdleForInboxScan = (agent) => !agent.stopped && !agent.busy && !agent.submitting && !agent.turnInProgress && !agent.starting && Boolean(agent.session);
|
|
@@ -486,13 +533,32 @@ export function createRuntimeHost(options) {
|
|
|
486
533
|
for (const record of agent.records.values()) {
|
|
487
534
|
if (record.status !== "accepted")
|
|
488
535
|
continue;
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
536
|
+
const attempts = record.turnEndRetryAttempts ?? 0;
|
|
537
|
+
if (attempts >= turnEndRetryPolicy.maxAttempts) {
|
|
538
|
+
record.reason = `turn-end redelivery exhausted after ${turnEndRetryPolicy.maxAttempts} attempts`;
|
|
539
|
+
record.retryable = false;
|
|
540
|
+
const terminal = setRecord(agent, record, "error");
|
|
541
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: terminal.deliveryId,
|
|
542
|
+
messageId: terminal.messageId, status: "error", reason: terminal.reason });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
const attempt = attempts + 1;
|
|
546
|
+
const delay = turnEndRetryDelayMs(attempt);
|
|
547
|
+
record.turnEndRetryAttempts = attempt;
|
|
548
|
+
record.retryNotBefore = new Date(turnEndRetryScheduler.now() + delay).toISOString();
|
|
549
|
+
record.reason = TURN_END_RETRY_REASON;
|
|
550
|
+
record.retryable = true;
|
|
551
|
+
record.input = { ...record.input, attempt: (record.input.attempt ?? 0) + 1 };
|
|
552
|
+
const deferred = setRecord(agent, record, "pending");
|
|
553
|
+
emit({ type: "delivery", agentId: agent.config.agentId, deliveryId: deferred.deliveryId,
|
|
554
|
+
messageId: deferred.messageId, status: "deferred", reason: TURN_END_RETRY_REASON });
|
|
555
|
+
}
|
|
556
|
+
schedulePendingRetry(agent);
|
|
492
557
|
return;
|
|
493
558
|
}
|
|
494
559
|
const consumed = [];
|
|
495
560
|
const deferred = [];
|
|
561
|
+
const exhausted = [];
|
|
496
562
|
try {
|
|
497
563
|
store.withInboxTransaction(() => {
|
|
498
564
|
const inboxIds = new Set(store.readNdjson("inbox").flatMap((row) => typeof row?.message_id === "string" ? [row.message_id] : []));
|
|
@@ -513,8 +579,31 @@ export function createRuntimeHost(options) {
|
|
|
513
579
|
if (current.status !== "accepted" || candidate.status !== "accepted" || !inboxIds.has(candidate.messageId)) {
|
|
514
580
|
return candidate;
|
|
515
581
|
}
|
|
516
|
-
const
|
|
517
|
-
|
|
582
|
+
const attempts = candidate.turnEndRetryAttempts ?? 0;
|
|
583
|
+
if (attempts >= turnEndRetryPolicy.maxAttempts) {
|
|
584
|
+
const terminal = { ...candidate, status: "error", updatedAt: now(),
|
|
585
|
+
reason: `turn-end redelivery exhausted after ${turnEndRetryPolicy.maxAttempts} attempts`,
|
|
586
|
+
retryable: false };
|
|
587
|
+
changed = true;
|
|
588
|
+
pendingUpdates.push(terminal);
|
|
589
|
+
return terminal;
|
|
590
|
+
}
|
|
591
|
+
const attempt = attempts + 1;
|
|
592
|
+
const delay = turnEndRetryDelayMs(attempt);
|
|
593
|
+
const staleInput = candidate.input && typeof candidate.input === "object" ? candidate.input : null;
|
|
594
|
+
const next = {
|
|
595
|
+
...candidate,
|
|
596
|
+
status: "pending",
|
|
597
|
+
updatedAt: now(),
|
|
598
|
+
reason: TURN_END_RETRY_REASON,
|
|
599
|
+
retryable: true,
|
|
600
|
+
turnEndRetryAttempts: attempt,
|
|
601
|
+
retryNotBefore: new Date(turnEndRetryScheduler.now() + delay).toISOString(),
|
|
602
|
+
input: {
|
|
603
|
+
...(staleInput ?? { inputId: candidate.deliveryId, deliveryId: candidate.deliveryId, kind: "wake", text: "" }),
|
|
604
|
+
attempt: (Number.isSafeInteger(staleInput?.attempt) && Number(staleInput?.attempt) >= 0 ? Number(staleInput.attempt) : 0) + 1,
|
|
605
|
+
},
|
|
606
|
+
};
|
|
518
607
|
changed = true;
|
|
519
608
|
pendingUpdates.push(next);
|
|
520
609
|
return next;
|
|
@@ -523,7 +612,10 @@ export function createRuntimeHost(options) {
|
|
|
523
612
|
store.writeJson("runtimeDeliveries", { ...disk, records });
|
|
524
613
|
for (const next of pendingUpdates) {
|
|
525
614
|
agent.records.set(next.deliveryId, next);
|
|
526
|
-
|
|
615
|
+
if (next.status === "error")
|
|
616
|
+
exhausted.push(next);
|
|
617
|
+
else
|
|
618
|
+
deferred.push(next);
|
|
527
619
|
}
|
|
528
620
|
}
|
|
529
621
|
});
|
|
@@ -536,6 +628,11 @@ export function createRuntimeHost(options) {
|
|
|
536
628
|
for (const record of deferred)
|
|
537
629
|
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
538
630
|
deliveryId: record.deliveryId, messageId: record.messageId, status: "deferred", reason: TURN_END_RETRY_REASON });
|
|
631
|
+
for (const record of exhausted)
|
|
632
|
+
emit({ type: "delivery", agentId: agent.config.agentId,
|
|
633
|
+
deliveryId: record.deliveryId, messageId: record.messageId, status: "error", reason: record.reason });
|
|
634
|
+
if (deferred.length)
|
|
635
|
+
schedulePendingRetry(agent);
|
|
539
636
|
};
|
|
540
637
|
// Production callers persist the canonical Inbox before calling deliver(). If a
|
|
541
638
|
// concurrent drain wins between that append and ledger creation, close only the
|
|
@@ -734,9 +831,11 @@ export function createRuntimeHost(options) {
|
|
|
734
831
|
if (idleGate)
|
|
735
832
|
await idleGate;
|
|
736
833
|
}
|
|
737
|
-
const record = [...agent.records.values()].find((candidate) => candidate.status === "pending" || candidate.status === "submitting");
|
|
834
|
+
const record = [...agent.records.values()].find((candidate) => (candidate.status === "pending" && isRetryDue(candidate)) || candidate.status === "submitting");
|
|
738
835
|
if (record)
|
|
739
836
|
await submit(agent, record, false);
|
|
837
|
+
else
|
|
838
|
+
schedulePendingRetry(agent);
|
|
740
839
|
})();
|
|
741
840
|
agent.retryPendingInFlight = run;
|
|
742
841
|
try {
|
|
@@ -1510,6 +1609,8 @@ export function createRuntimeHost(options) {
|
|
|
1510
1609
|
clearInterval(previous.poller);
|
|
1511
1610
|
if (previous.retryTimer)
|
|
1512
1611
|
clearTimeout(previous.retryTimer);
|
|
1612
|
+
if (previous.deliveryRetryTimer)
|
|
1613
|
+
turnEndRetryScheduler.clearTimeout(previous.deliveryRetryTimer);
|
|
1513
1614
|
if (previous.stabilityTimer)
|
|
1514
1615
|
clearTimeout(previous.stabilityTimer);
|
|
1515
1616
|
const previousFailure = currentAuthFailure(previous);
|
|
@@ -1521,7 +1622,7 @@ export function createRuntimeHost(options) {
|
|
|
1521
1622
|
}, previousFailure));
|
|
1522
1623
|
const candidate = {
|
|
1523
1624
|
...previous, config, adapter, session, launchId: crypto.randomUUID(), busy: false, submitting: false,
|
|
1524
|
-
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null,
|
|
1625
|
+
starting: null, retryAfterSubmit: false, generation: 0, poller: null, retryTimer: null, deliveryRetryTimer: null,
|
|
1525
1626
|
recreateAttempts: 0, stabilityTimer: null, recreateReason: null, stopped: false,
|
|
1526
1627
|
disabledReason: null, configurationRecovery: null,
|
|
1527
1628
|
authFailureActive: scoped && previous.authFailureActive,
|
|
@@ -1614,6 +1715,7 @@ export function createRuntimeHost(options) {
|
|
|
1614
1715
|
if (pendingAfterCreate > 0) {
|
|
1615
1716
|
throw new RuntimeSessionResetError("inbox_backlog", `Agent ${agentId} received Inbox backlog during reset`, pendingAfterCreate);
|
|
1616
1717
|
}
|
|
1718
|
+
clearDeliveryRetryTimer(agent);
|
|
1617
1719
|
agent.generation += 1;
|
|
1618
1720
|
agent.launchId = crypto.randomUUID();
|
|
1619
1721
|
agent.config.sessionId = null;
|
|
@@ -1884,7 +1986,7 @@ export function createRuntimeHost(options) {
|
|
|
1884
1986
|
retryPendingInFlight: null,
|
|
1885
1987
|
records: new Map(persisted.records.map((record) => [record.deliveryId, record])),
|
|
1886
1988
|
byMessage: new Map(persisted.records.map((record) => [record.messageId, record.deliveryId])),
|
|
1887
|
-
generation: 0, poller: null, retryTimer: null, recreateAttempts: 0,
|
|
1989
|
+
generation: 0, poller: null, retryTimer: null, deliveryRetryTimer: null, recreateAttempts: 0,
|
|
1888
1990
|
stabilityTimer: null, recreateReason: null, stopped: false, disabledReason: null,
|
|
1889
1991
|
configurationRecovery: null, stateStore,
|
|
1890
1992
|
compactionBreaker: config.runtime === "pi" && stateStore
|
|
@@ -2056,6 +2158,7 @@ export function createRuntimeHost(options) {
|
|
|
2056
2158
|
clearInterval(agent.poller);
|
|
2057
2159
|
if (agent.retryTimer)
|
|
2058
2160
|
clearTimeout(agent.retryTimer);
|
|
2161
|
+
clearDeliveryRetryTimer(agent);
|
|
2059
2162
|
if (agent.stabilityTimer)
|
|
2060
2163
|
clearTimeout(agent.stabilityTimer);
|
|
2061
2164
|
if (agent.busy)
|