agent-tempo 2.0.0-beta.1 → 2.0.0-beta.2
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/dashboard/package.json +1 -1
- package/dist/activities/maestro.js +3 -0
- package/dist/activities/outbox.js +12 -0
- package/dist/activities/resolve.d.ts +18 -7
- package/dist/activities/resolve.js +58 -46
- package/dist/adapters/claude-code/adapter.js +7 -0
- package/dist/cli/command-center-command.js +15 -1
- package/dist/cli/help-text.js +2 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +14 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +38 -1
- package/dist/daemon.js +29 -0
- package/dist/http/event-types.d.ts +33 -0
- package/dist/http/server.js +10 -0
- package/dist/http/snapshot.js +3 -0
- package/dist/observability/nondeterminism-alarm.d.ts +113 -0
- package/dist/observability/nondeterminism-alarm.js +162 -0
- package/dist/server-tools.js +30 -29
- package/dist/server.js +42 -0
- package/dist/tools/action-guard.d.ts +23 -0
- package/dist/tools/action-guard.js +33 -0
- package/dist/tools/coat-check.d.ts +20 -0
- package/dist/tools/coat-check.js +129 -0
- package/dist/tools/gate.d.ts +13 -0
- package/dist/tools/gate.js +88 -0
- package/dist/tools/schedule.d.ts +18 -0
- package/dist/tools/schedule.js +93 -1
- package/dist/tools/stage.d.ts +17 -0
- package/dist/tools/stage.js +85 -1
- package/dist/tools/state.d.ts +14 -0
- package/dist/tools/state.js +95 -0
- package/dist/types.d.ts +39 -1
- package/dist/utils/orphan-guard.d.ts +37 -0
- package/dist/utils/orphan-guard.js +26 -0
- package/dist/utils/search-attributes.d.ts +1 -0
- package/dist/utils/search-attributes.js +9 -0
- package/dist/workflows/session.js +193 -43
- package/package.json +1 -1
- package/workflow-bundle.js +241 -45
|
@@ -70,6 +70,31 @@ async function agentSessionWorkflow(input) {
|
|
|
70
70
|
const DEFAULT_DRAINING_DEADLINE_MS = 5_000;
|
|
71
71
|
/** Max duration a messageId can stay in-flight before the safety timer ejects it. */
|
|
72
72
|
const PROCESSING_DEADLINE_MS = 15 * 60 * 1000;
|
|
73
|
+
/**
|
|
74
|
+
* #704 — max duration a session may sit in `booting` (no attachment claimed)
|
|
75
|
+
* before the watchdog fails the recruit. Default 180s (architect OQ-3: 120s
|
|
76
|
+
* floor; 180s clears a cold launch + cross-host recruit handshake). Workflows
|
|
77
|
+
* can't read process.env, so an operator/test override rides durable metadata
|
|
78
|
+
* (`bootingDeadlineMs`, sourced from `AGENT_TEMPO_BOOTING_DEADLINE_MS` at
|
|
79
|
+
* spawn). Only ARMED for headless adapters on a fresh (non-handoff) boot —
|
|
80
|
+
* see `armBootingWatchdog` below.
|
|
81
|
+
*/
|
|
82
|
+
const BOOTING_DEADLINE_MS = typeof input.metadata.bootingDeadlineMs === 'number' && input.metadata.bootingDeadlineMs > 0
|
|
83
|
+
? input.metadata.bootingDeadlineMs
|
|
84
|
+
: 180_000;
|
|
85
|
+
/**
|
|
86
|
+
* #704 Item 2 — generous main-loop wake backstop (architect-ruled FINAL).
|
|
87
|
+
* Every deadline-mutating handler now bumps `wakeEpoch`, so in steady state
|
|
88
|
+
* the loop wakes on exactly `nextDeadlineMs()`. This backstop is defense-in-
|
|
89
|
+
* depth for a FUTURE handler that mutates wake-relevant state but forgets the
|
|
90
|
+
* bump: the loop still re-evaluates at least every `BACKSTOP_MS` instead of
|
|
91
|
+
* sleeping forever (we deliberately do NOT add an `Infinity → no-timer` branch
|
|
92
|
+
* — a silent indefinite sleep is the exact failure class #704 exists to kill).
|
|
93
|
+
* A fallback-cap wake that finds actionable state emits a loud WARN (below) so
|
|
94
|
+
* a missed bump is detectable, not masked. 30min is large enough that it never
|
|
95
|
+
* fires in correct steady state yet bounds any regression.
|
|
96
|
+
*/
|
|
97
|
+
const BACKSTOP_MS = 30 * 60 * 1000;
|
|
73
98
|
// ── 2.0 clean-slate (#787) ──
|
|
74
99
|
// The replay-only `patched()` markers that protected in-flight 1.x sessions
|
|
75
100
|
// across rolling deploys are gone: 2.0 is a hard cutover (the #786 boot guard
|
|
@@ -188,6 +213,44 @@ async function agentSessionWorkflow(input) {
|
|
|
188
213
|
: undefined;
|
|
189
214
|
/** ISO timestamp of when the workflow most recently entered `detached`. */
|
|
190
215
|
let detachedSince = null;
|
|
216
|
+
// ── #704 — Booting attach-timeout watchdog ──
|
|
217
|
+
// A session that starts FRESH in `booting` (no attachment handoff) and never
|
|
218
|
+
// reaches `claimAttachment` within `BOOTING_DEADLINE_MS` is a failed recruit:
|
|
219
|
+
// the adapter never launched, wedged on a launch dialog, or crashed pre-attach.
|
|
220
|
+
// We ARM a deadline only when ALL THREE hold:
|
|
221
|
+
// 1. `startedFresh` — no handoff. A restart/migrate carries `currentAttachment`
|
|
222
|
+
// (and a non-`booting` phase), so its successor must NEVER arm — it already
|
|
223
|
+
// has an adapter contract. Detached/other CAN successors are likewise skipped.
|
|
224
|
+
// 2. `recruitedBy` present — this is an actual RECRUIT (a spawn is coming and is
|
|
225
|
+
// expected to attach). The watchdog is a failed-recruit detector — it even
|
|
226
|
+
// notifies `recruitedBy` ("recruit of <name> never attached"). Skeletons created
|
|
227
|
+
// WITHOUT a recruiter intentionally sit in `booting` awaiting a manual attach and
|
|
228
|
+
// must NOT be swept: from-upgrade re-attach skeletons (#786 — the designed
|
|
229
|
+
// await-per-player-restart behavior), conductor/`up`, manual self-bootstrap. A
|
|
230
|
+
// positive allowlist ("arm only real recruits") rather than a per-skeleton-path
|
|
231
|
+
// blocklist, so future adapterless-skeleton paths inherit the safe default.
|
|
232
|
+
// 3. `canBlockOnDialog !== true` — the adapter can't park on a blocking
|
|
233
|
+
// launch-time dialog. Interactive `claude-code` (dev-channels dialog) sets
|
|
234
|
+
// this true and is DISARMED until #890 dissolves the dialog: an operator-away
|
|
235
|
+
// false-kill of a legitimately-waiting recruit is worse than the hang.
|
|
236
|
+
// The structural `canBlockOnDialog` (resolved from the adapter descriptor at
|
|
237
|
+
// spawn, threaded via metadata) is the contract — NOT an `agentType` hardcode.
|
|
238
|
+
const startedFresh = !input.currentAttachment && (input.phase === undefined || input.phase === 'booting');
|
|
239
|
+
const armBootingWatchdog = startedFresh && !!input.metadata.recruitedBy && input.metadata.canBlockOnDialog !== true;
|
|
240
|
+
/**
|
|
241
|
+
* ISO timestamp of when this run entered `booting`, or `null` when the watchdog
|
|
242
|
+
* is disarmed (handoff / non-recruit skeleton / interactive / already attached).
|
|
243
|
+
* Cleared on the first successful fresh claim. Only non-null ⟹ armed, so
|
|
244
|
+
* `nextDeadlineMs()` and the main-loop reap can gate on `bootingSince !== null` alone.
|
|
245
|
+
*/
|
|
246
|
+
let bootingSince = armBootingWatchdog ? workflowNow().toISOString() : null;
|
|
247
|
+
if (startedFresh && input.metadata.recruitedBy && input.metadata.canBlockOnDialog === true) {
|
|
248
|
+
// Visibility for the known, bounded gap (companion brief §1): interactive
|
|
249
|
+
// claude-code RECRUITS keep today's indefinite-`booting` behavior until #890.
|
|
250
|
+
// (Non-recruit skeletons are disarmed for a different reason — no recruiter —
|
|
251
|
+
// and don't log this #890 notice.)
|
|
252
|
+
workflow_1.log.info('booting watchdog DISARMED for interactive claude-code recruit (canBlockOnDialog) — pending #890');
|
|
253
|
+
}
|
|
191
254
|
// ── Processing Lifecycle State (fixes #99) ──
|
|
192
255
|
// Tracks messages currently being processed by a blocking adapter. While non-empty,
|
|
193
256
|
// stale detection is suppressed AND the phase refines to `processing`.
|
|
@@ -270,6 +333,11 @@ async function agentSessionWorkflow(input) {
|
|
|
270
333
|
function nextDeadlineMs() {
|
|
271
334
|
const nowMs = workflowNow().getTime();
|
|
272
335
|
const candidates = [];
|
|
336
|
+
// #704 — booting attach-timeout. `bootingSince` is non-null only while the
|
|
337
|
+
// watchdog is armed AND the session is still booting (cleared on fresh claim).
|
|
338
|
+
if (phase === 'booting' && bootingSince) {
|
|
339
|
+
candidates.push(new Date(bootingSince).getTime() + BOOTING_DEADLINE_MS);
|
|
340
|
+
}
|
|
273
341
|
if (currentAttachment) {
|
|
274
342
|
candidates.push(new Date(currentAttachment.expiresAt).getTime());
|
|
275
343
|
}
|
|
@@ -534,6 +602,10 @@ async function agentSessionWorkflow(input) {
|
|
|
534
602
|
// Phase refinement: if we're attached (or awaiting), move to `processing`.
|
|
535
603
|
if (phase === 'attached' || phase === 'awaiting')
|
|
536
604
|
setPhase('processing');
|
|
605
|
+
// #704 Item 2 — setting `processingSince` adds a new `PROCESSING_DEADLINE_MS`
|
|
606
|
+
// deadline; bump `wakeEpoch` so the main loop arms it immediately rather than
|
|
607
|
+
// relying on the backstop cap (the historical reason for the 5-min cap).
|
|
608
|
+
wakeEpoch++;
|
|
537
609
|
}
|
|
538
610
|
lastActivityTime = workflowNow().getTime();
|
|
539
611
|
activityCount++;
|
|
@@ -564,6 +636,10 @@ async function agentSessionWorkflow(input) {
|
|
|
564
636
|
const outboxIdle = !outbox.some((e) => e.status === 'pending' || e.status === 'processing');
|
|
565
637
|
setPhase(outboxIdle ? 'awaiting' : 'attached');
|
|
566
638
|
}
|
|
639
|
+
// #704 Item 2 — clearing `processingSince` removes the processing deadline;
|
|
640
|
+
// bump `wakeEpoch` so the main loop re-evaluates (and dispatches any pending
|
|
641
|
+
// outbox) immediately rather than waiting on the backstop cap.
|
|
642
|
+
wakeEpoch++;
|
|
567
643
|
}
|
|
568
644
|
lastActivityTime = workflowNow().getTime();
|
|
569
645
|
activityCount++;
|
|
@@ -658,6 +734,11 @@ async function agentSessionWorkflow(input) {
|
|
|
658
734
|
(0, workflow_1.upsertSearchAttributes)({
|
|
659
735
|
AgentTempoAttachedHost: [''],
|
|
660
736
|
});
|
|
737
|
+
// #704 Item 1b — stamp the typed close-reason MEMO on this terminal
|
|
738
|
+
// completion. The bootstrap orphan-guard (`server.ts`) reads it via
|
|
739
|
+
// `describe().memo` to self-tombstone a late-launching orphan process whose
|
|
740
|
+
// run was destroyed and never recreated (no running run + this reason).
|
|
741
|
+
(0, workflow_1.upsertMemo)({ [search_attributes_1.MEMO_KEYS.closeReason]: 'destroyed' });
|
|
661
742
|
setPhase('gone');
|
|
662
743
|
// Inject a final audit message so the old adapter-completion path has something to show.
|
|
663
744
|
messages.push({
|
|
@@ -669,6 +750,10 @@ async function agentSessionWorkflow(input) {
|
|
|
669
750
|
});
|
|
670
751
|
lastActivityTime = workflowNow().getTime();
|
|
671
752
|
activityCount++;
|
|
753
|
+
// #704 Item 2 — `destroyRequested` (set above) is in the main-loop predicate,
|
|
754
|
+
// but bump `wakeEpoch` too so a loop asleep on a far deadline wakes promptly
|
|
755
|
+
// to run the terminal exit path.
|
|
756
|
+
wakeEpoch++;
|
|
672
757
|
});
|
|
673
758
|
(0, workflow_1.setHandler)(signals_1.isDestroyedQuery, () => destroyed || destroyRequested);
|
|
674
759
|
// ── Test-only CAN trigger (#226) ──
|
|
@@ -712,6 +797,10 @@ async function agentSessionWorkflow(input) {
|
|
|
712
797
|
currentAttachment.leaseMs = leaseMs;
|
|
713
798
|
lastActivityTime = nowMs;
|
|
714
799
|
activityCount++;
|
|
800
|
+
// #704 Item 2 — this mutates `expiresAt` (a `nextDeadlineMs()` input); bump
|
|
801
|
+
// `wakeEpoch` so the main loop re-evaluates the new lease deadline instead
|
|
802
|
+
// of relying on the backstop cap.
|
|
803
|
+
wakeEpoch++;
|
|
715
804
|
return attachmentTokenFrom(currentAttachment, leaseMs);
|
|
716
805
|
}
|
|
717
806
|
// Conflict: active lease held by someone else.
|
|
@@ -741,12 +830,19 @@ async function agentSessionWorkflow(input) {
|
|
|
741
830
|
drainingSince = null;
|
|
742
831
|
drainingDeadlineMs = null;
|
|
743
832
|
detachedSince = null;
|
|
833
|
+
// #704 Item 1a — first successful claim: disarm the booting watchdog so
|
|
834
|
+
// `nextDeadlineMs()` drops the booting deadline (the lease deadline takes over).
|
|
835
|
+
bootingSince = null;
|
|
744
836
|
setPhase('attached');
|
|
745
837
|
(0, workflow_1.upsertSearchAttributes)({
|
|
746
838
|
AgentTempoAttachedHost: [host],
|
|
747
839
|
});
|
|
748
840
|
lastActivityTime = nowMs;
|
|
749
841
|
activityCount++;
|
|
842
|
+
// #704 Item 2 — a fresh claim moves a `booting`/`detached` session to a
|
|
843
|
+
// finite lease deadline (often from `+Infinity`); bump `wakeEpoch` so the
|
|
844
|
+
// main loop picks up the new deadline immediately.
|
|
845
|
+
wakeEpoch++;
|
|
750
846
|
return attachmentTokenFrom(newAttachment, leaseMs);
|
|
751
847
|
}, {
|
|
752
848
|
validator: ({ host, leaseMs, protocolVersion }) => {
|
|
@@ -1432,52 +1528,36 @@ async function agentSessionWorkflow(input) {
|
|
|
1432
1528
|
// timer would leave the workflow in `draining` until that old timer fired.
|
|
1433
1529
|
const epochAtWait = wakeEpoch;
|
|
1434
1530
|
const deadlineMs = nextDeadlineMs();
|
|
1435
|
-
//
|
|
1436
|
-
//
|
|
1437
|
-
//
|
|
1438
|
-
//
|
|
1439
|
-
//
|
|
1440
|
-
//
|
|
1441
|
-
//
|
|
1442
|
-
//
|
|
1443
|
-
//
|
|
1444
|
-
//
|
|
1445
|
-
//
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1448
|
-
|
|
1449
|
-
//
|
|
1450
|
-
// Without the fallback wake, a workflow waiting in `condition(predicate)` on
|
|
1451
|
-
// an `Infinity` deadline (booting / detached, no draining, no processing)
|
|
1452
|
-
// never re-evaluates `nextDeadlineMs()` after one of these handlers fires —
|
|
1453
|
-
// the freshly-set lease-expiry timer is never picked up, lease expiry is
|
|
1454
|
-
// never reaped, and the workflow stalls until external state forces the
|
|
1455
|
-
// predicate true. Smoking-gun test that fails without the fallback:
|
|
1456
|
-
// `test/session-phase-processing.test.ts:54` "attached -> processing ->
|
|
1457
|
-
// awaiting via processingStart/End (#117 fix)" — times out at 10s because
|
|
1458
|
-
// the loop never makes progress after `processingStart` lands on a fresh
|
|
1459
|
-
// claim.
|
|
1460
|
-
//
|
|
1461
|
-
// Removing this fallback safely is a "main-loop wake-discipline cleanup"
|
|
1462
|
-
// separate from the audit's framing — adds `wakeEpoch++` to each affected
|
|
1463
|
-
// handler, gates with `patched()` markers for replay-determinism (live
|
|
1464
|
-
// workflow histories already recorded the existing `Timer 5min` events),
|
|
1465
|
-
// and adds a regression test covering the handler-induced-deadline pickup.
|
|
1466
|
-
// Estimated 4–6 handler edits + tests, separate dedicated PR. See the
|
|
1467
|
-
// 2026-04-26 forensics for the full mechanism walkthrough — link from this
|
|
1468
|
-
// file's PR history.
|
|
1469
|
-
//
|
|
1470
|
-
// Until that cleanup happens, DO NOT remove this fallback. The
|
|
1471
|
-
// `Math.min(deadlineMs, 5 * 60 * 1000)` cap is part of the same mechanism:
|
|
1472
|
-
// it ensures every deadline (even hour-long lease-expiry timers) is
|
|
1473
|
-
// re-evaluated at least every 5 min so handler-induced deadline shortenings
|
|
1474
|
-
// can't be missed.
|
|
1475
|
-
const conditionPromise = (0, workflow_1.condition)(() => destroyRequested ||
|
|
1531
|
+
// #704 Item 2 — wake discipline. Every handler that mutates a `nextDeadlineMs()`
|
|
1532
|
+
// input now bumps `wakeEpoch` (claim renew+fresh, processingStart/End, the
|
|
1533
|
+
// draining/detach paths, destroy), so in steady state the loop wakes on exactly
|
|
1534
|
+
// `nextDeadlineMs()`. `BACKSTOP_MS` is defense-in-depth for a FUTURE handler that
|
|
1535
|
+
// mutates wake-relevant state but forgets the bump: the loop still re-evaluates
|
|
1536
|
+
// at least every 30 min instead of sleeping forever. We deliberately keep the
|
|
1537
|
+
// backstop even when `deadlineMs === Infinity` (idle booting-disarmed / detached)
|
|
1538
|
+
// — a silent indefinite sleep is the exact failure class #704 exists to kill, so
|
|
1539
|
+
// there is NO `Infinity → no-timer` branch. A backstop wake that finds actionable
|
|
1540
|
+
// state emits a loud WARN below so a missed bump is detectable, not masked.
|
|
1541
|
+
// Canary: `test/session-phase-processing.test.ts` ("attached → processing →
|
|
1542
|
+
// awaiting via processingStart/End") fails if a deadline-mutating handler drops
|
|
1543
|
+
// its bump.
|
|
1544
|
+
const wokeByPredicate = await (0, workflow_1.condition)(() => destroyRequested ||
|
|
1476
1545
|
canDispatch() ||
|
|
1477
1546
|
hasPendingStop() ||
|
|
1478
1547
|
phase === 'gone' ||
|
|
1479
|
-
wakeEpoch !== epochAtWait,
|
|
1480
|
-
|
|
1548
|
+
wakeEpoch !== epochAtWait, Math.min(deadlineMs, BACKSTOP_MS));
|
|
1549
|
+
// #704 Item 2 — missed-bump breadcrumb. If we woke on the backstop cap (NOT the
|
|
1550
|
+
// predicate) yet a deadline is already overdue or the predicate is now actionable,
|
|
1551
|
+
// a handler likely mutated wake-relevant state without bumping `wakeEpoch`.
|
|
1552
|
+
// Surface it loudly instead of letting the backstop silently mask the regression.
|
|
1553
|
+
if (!wokeByPredicate) {
|
|
1554
|
+
const overdue = nextDeadlineMs() <= 0;
|
|
1555
|
+
const actionable = destroyRequested || canDispatch() || hasPendingStop() || phase === 'gone';
|
|
1556
|
+
if (overdue || actionable) {
|
|
1557
|
+
workflow_1.log.warn(`main-loop woke via fallback backstop with actionable state — possible missed ` +
|
|
1558
|
+
`wakeEpoch bump (phase=${phase}, overdue=${overdue}, actionable=${actionable})`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1481
1561
|
if (destroyRequested)
|
|
1482
1562
|
break;
|
|
1483
1563
|
// ── §9.5.a: Lease expiry — reap attachment and transition to `detached`. ──
|
|
@@ -1497,6 +1577,70 @@ async function agentSessionWorkflow(input) {
|
|
|
1497
1577
|
});
|
|
1498
1578
|
workflow_1.log.warn(`lease expired for attachment ${reaped.attachmentId} (host=${reaped.hostname})`);
|
|
1499
1579
|
}
|
|
1580
|
+
// ── §9.5.a2: booting attach-timeout (#704). ──
|
|
1581
|
+
// A fresh, armed session that never reached `claimAttachment` within
|
|
1582
|
+
// `BOOTING_DEADLINE_MS` is a failed recruit (adapter never launched / wedged /
|
|
1583
|
+
// crashed pre-attach). Fail it LOUDLY: sweep any orphan process, notify the
|
|
1584
|
+
// recruiter, stamp the close-reason tombstone MEMO, and COMPLETE terminal
|
|
1585
|
+
// `gone`. `bootingSince !== null` ⟹ the watchdog is armed (headless adapter on
|
|
1586
|
+
// a fresh, non-handoff boot). The bootstrap orphan-guard backstops the case
|
|
1587
|
+
// where the swept process (or a never-swept one) re-launches later.
|
|
1588
|
+
if (phase === 'booting' &&
|
|
1589
|
+
bootingSince !== null &&
|
|
1590
|
+
workflowNow().getTime() - new Date(bootingSince).getTime() >= BOOTING_DEADLINE_MS) {
|
|
1591
|
+
lastDetachReason = 'boot-timeout';
|
|
1592
|
+
workflow_1.log.warn(`boot-timeout: session never attached within ${Math.round(BOOTING_DEADLINE_MS / 1000)}s — failing recruit`);
|
|
1593
|
+
// Best-effort orphan sweep. At the deadline the spawned process usually
|
|
1594
|
+
// EXISTS (still booting), so this command-line kill is MORE likely to land
|
|
1595
|
+
// than the destroy-time sweep. No-op if nothing launched.
|
|
1596
|
+
const killHost = preferredHost ?? input.metadata.hostname;
|
|
1597
|
+
if (killHost) {
|
|
1598
|
+
try {
|
|
1599
|
+
const killResult = await getHardTerminateProxyForDestroy(killHost)({
|
|
1600
|
+
ensemble: input.metadata.ensemble,
|
|
1601
|
+
playerName: input.metadata.playerId,
|
|
1602
|
+
agent: (input.metadata.agentType ?? 'claude'),
|
|
1603
|
+
workDir: input.metadata.workDir,
|
|
1604
|
+
});
|
|
1605
|
+
workflow_1.log.info(`boot-timeout hard-terminate on ${killHost}: strategy=${killResult.strategy}, ` +
|
|
1606
|
+
`killedPids=[${killResult.killedPids.join(',')}]`);
|
|
1607
|
+
}
|
|
1608
|
+
catch (err) {
|
|
1609
|
+
workflow_1.log.warn(`boot-timeout hard-terminate failed on ${killHost} (best-effort): ` +
|
|
1610
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
// Notify the recruiter (if any) — reuses the outbox `deliverCue` activity.
|
|
1614
|
+
const recruiter = input.metadata.recruitedBy;
|
|
1615
|
+
if (recruiter) {
|
|
1616
|
+
try {
|
|
1617
|
+
await deliverCue({
|
|
1618
|
+
ensemble: input.metadata.ensemble,
|
|
1619
|
+
fromPlayerId: input.metadata.playerId,
|
|
1620
|
+
targetPlayerId: recruiter,
|
|
1621
|
+
message: `Recruit of "${input.metadata.playerId}" never attached within ` +
|
|
1622
|
+
`${Math.round(BOOTING_DEADLINE_MS / 1000)}s — failed; the spawned process (if any) was swept.`,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
catch (err) {
|
|
1626
|
+
workflow_1.log.warn(`boot-timeout recruiter-notify failed (best-effort): ` +
|
|
1627
|
+
`${err instanceof Error ? err.message : String(err)}`);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
// Tombstone MEMO (shared with the orphan-guard) + terminal `gone`.
|
|
1631
|
+
(0, workflow_1.upsertMemo)({ [search_attributes_1.MEMO_KEYS.closeReason]: 'boot-timeout' });
|
|
1632
|
+
lastAdapterMeta = lastAdapterMeta ?? {
|
|
1633
|
+
hostname: killHost ?? input.metadata.hostname,
|
|
1634
|
+
adapterId: '',
|
|
1635
|
+
};
|
|
1636
|
+
bootingSince = null;
|
|
1637
|
+
setPhase('gone');
|
|
1638
|
+
(0, workflow_1.upsertSearchAttributes)({ AgentTempoAttachedHost: [''] });
|
|
1639
|
+
// Route through the terminal exit path (mirrors `destroy`): isDestroyed
|
|
1640
|
+
// queries read true, then the loop breaks to COMPLETE.
|
|
1641
|
+
destroyRequested = true;
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1500
1644
|
// ── §9.5.b: processingDeadline — force exit from `processing` if a messageId is wedged. ──
|
|
1501
1645
|
if (processingSince !== null &&
|
|
1502
1646
|
workflowNow().getTime() - new Date(processingSince).getTime() > PROCESSING_DEADLINE_MS) {
|
|
@@ -1770,6 +1914,12 @@ async function agentSessionWorkflow(input) {
|
|
|
1770
1914
|
adapterId: currentAttachment.adapterId,
|
|
1771
1915
|
};
|
|
1772
1916
|
lastDetachReason = 'spawn-failed';
|
|
1917
|
+
// #704 Item 2 — no `wakeEpoch++` needed here despite clearing
|
|
1918
|
+
// `nextDeadlineMs()` inputs (currentAttachment/processingSince/draining):
|
|
1919
|
+
// this rollback runs INLINE in the main-loop body (outbox dispatch), not
|
|
1920
|
+
// in a signal/update handler, so the very next loop iteration recomputes
|
|
1921
|
+
// `nextDeadlineMs()` before the next `condition()` wait. (The bump
|
|
1922
|
+
// discipline is for HANDLERS that mutate these while the loop is parked.)
|
|
1773
1923
|
currentAttachment = null;
|
|
1774
1924
|
inFlightMessages.clear();
|
|
1775
1925
|
processingSince = null;
|