agent-tempo 1.7.0 → 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/CLAUDE.md +10 -13
- package/README.md +21 -15
- package/dashboard/dist/assets/index-BbR7zIdK.js.map +1 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/hard-terminate.d.ts +2 -2
- package/dist/activities/hard-terminate.js +3 -9
- package/dist/activities/maestro.d.ts +8 -13
- package/dist/activities/maestro.js +3 -10
- package/dist/activities/outbox.js +16 -2
- package/dist/activities/resolve.d.ts +18 -7
- package/dist/activities/resolve.js +58 -46
- package/dist/adapters/base.js +3 -0
- package/dist/adapters/claude-code/adapter.js +7 -0
- package/dist/cli/command-center-command.js +15 -1
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.js +79 -11
- package/dist/cli/daemon-command.js +19 -33
- package/dist/cli/help-text.js +9 -7
- package/dist/cli/home-command.d.ts +26 -0
- package/dist/cli/home-command.js +156 -0
- package/dist/cli/removed-verbs.d.ts +0 -4
- package/dist/cli/removed-verbs.js +23 -12
- package/dist/cli/sa-preflight.d.ts +0 -14
- package/dist/cli/sa-preflight.js +1 -17
- package/dist/cli/startup.d.ts +1 -10
- package/dist/cli/startup.js +0 -40
- package/dist/cli.js +34 -80
- package/dist/client/core.js +13 -15
- package/dist/client/index.d.ts +2 -1
- package/dist/client/interface.d.ts +5 -14
- package/dist/client/with-spawn.d.ts +9 -4
- package/dist/client/with-spawn.js +7 -10
- package/dist/config.d.ts +14 -13
- package/dist/config.js +14 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.js +60 -1
- package/dist/daemon.js +81 -0
- package/dist/http/auth.d.ts +3 -21
- package/dist/http/auth.js +10 -31
- package/dist/http/event-types.d.ts +38 -4
- package/dist/http/orphans.d.ts +3 -3
- package/dist/http/orphans.js +3 -3
- package/dist/http/server.d.ts +0 -5
- package/dist/http/server.js +79 -29
- package/dist/http/snapshot.js +3 -0
- package/dist/http/writes.d.ts +1 -1
- package/dist/http/writes.js +25 -2
- package/dist/observability/nondeterminism-alarm.d.ts +113 -0
- package/dist/observability/nondeterminism-alarm.js +162 -0
- package/dist/palette/index.d.ts +6 -7
- package/dist/palette/index.js +6 -7
- package/dist/pi/mission-control/actions.d.ts +67 -1
- package/dist/pi/mission-control/actions.js +61 -0
- package/dist/pi/mission-control/extension.d.ts +77 -0
- package/dist/pi/mission-control/extension.js +171 -0
- package/dist/pi/workflow-client.d.ts +14 -16
- package/dist/pi/workflow-client.js +18 -44
- 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 +54 -7
- package/dist/upgrade/boot-guard.d.ts +95 -0
- package/dist/upgrade/boot-guard.js +88 -0
- package/dist/upgrade/from-upgrade.d.ts +95 -0
- package/dist/upgrade/from-upgrade.js +330 -0
- package/dist/upgrade/phase-engine.js +4 -1
- package/dist/utils/duration.d.ts +3 -6
- package/dist/utils/duration.js +3 -6
- package/dist/utils/format-hosts.d.ts +1 -1
- package/dist/utils/format-hosts.js +0 -2
- package/dist/utils/grpc-shutdown-guard.js +0 -4
- package/dist/utils/hosts.d.ts +4 -10
- package/dist/utils/hosts.js +12 -28
- package/dist/utils/orphan-guard.d.ts +37 -0
- package/dist/utils/orphan-guard.js +26 -0
- package/dist/utils/search-attributes.d.ts +28 -18
- package/dist/utils/search-attributes.js +48 -18
- package/dist/utils/visibility-deadline.d.ts +16 -0
- package/dist/utils/visibility-deadline.js +16 -0
- package/dist/workflows/maestro-signals.d.ts +0 -13
- package/dist/workflows/maestro-signals.js +6 -14
- package/dist/workflows/maestro.js +27 -32
- package/dist/workflows/scheduler.js +7 -1
- package/dist/workflows/session.js +237 -153
- package/dist/workflows/signals.d.ts +9 -1
- package/dist/workflows/signals.js +8 -6
- package/package.json +1 -6
- package/workflow-bundle.js +478 -225
- package/dist/cli/legacy-migration.d.ts +0 -35
- package/dist/cli/legacy-migration.js +0 -341
- package/dist/client/ensure-conductor-spawned.d.ts +0 -35
- package/dist/client/ensure-conductor-spawned.js +0 -48
- package/dist/tui/App.d.ts +0 -85
- package/dist/tui/App.js +0 -1791
- package/dist/tui/bootstrap-types.d.ts +0 -46
- package/dist/tui/bootstrap-types.js +0 -7
- package/dist/tui/commands.d.ts +0 -71
- package/dist/tui/commands.js +0 -1375
- package/dist/tui/components/ChatView.d.ts +0 -35
- package/dist/tui/components/ChatView.js +0 -54
- package/dist/tui/components/CommandPalette.d.ts +0 -21
- package/dist/tui/components/CommandPalette.js +0 -67
- package/dist/tui/components/ConversationStream.d.ts +0 -114
- package/dist/tui/components/ConversationStream.js +0 -307
- package/dist/tui/components/CreateEnsembleWizard.d.ts +0 -19
- package/dist/tui/components/CreateEnsembleWizard.js +0 -223
- package/dist/tui/components/DestroyConfirmModal.d.ts +0 -17
- package/dist/tui/components/DestroyConfirmModal.js +0 -62
- package/dist/tui/components/ErrorView.d.ts +0 -31
- package/dist/tui/components/ErrorView.js +0 -129
- package/dist/tui/components/HomeView.d.ts +0 -54
- package/dist/tui/components/HomeView.js +0 -306
- package/dist/tui/components/LoadLineupModal.d.ts +0 -18
- package/dist/tui/components/LoadLineupModal.js +0 -79
- package/dist/tui/components/MainView.d.ts +0 -21
- package/dist/tui/components/MainView.js +0 -107
- package/dist/tui/components/NewEnsembleModal.d.ts +0 -9
- package/dist/tui/components/NewEnsembleModal.js +0 -73
- package/dist/tui/components/Picker.d.ts +0 -23
- package/dist/tui/components/Picker.js +0 -70
- package/dist/tui/components/PlayerDetailView.d.ts +0 -26
- package/dist/tui/components/PlayerDetailView.js +0 -118
- package/dist/tui/components/PromptArea.d.ts +0 -50
- package/dist/tui/components/PromptArea.js +0 -303
- package/dist/tui/components/RecruitWizard.d.ts +0 -17
- package/dist/tui/components/RecruitWizard.js +0 -221
- package/dist/tui/components/RestoreConfirmModal.d.ts +0 -18
- package/dist/tui/components/RestoreConfirmModal.js +0 -71
- package/dist/tui/components/ScheduleWizard.d.ts +0 -19
- package/dist/tui/components/ScheduleWizard.js +0 -259
- package/dist/tui/components/Splash.d.ts +0 -23
- package/dist/tui/components/Splash.js +0 -221
- package/dist/tui/components/StatusBar.d.ts +0 -48
- package/dist/tui/components/StatusBar.js +0 -128
- package/dist/tui/components/StatusOverlay.d.ts +0 -15
- package/dist/tui/components/StatusOverlay.js +0 -76
- package/dist/tui/components/TitleBar.d.ts +0 -10
- package/dist/tui/components/TitleBar.js +0 -21
- package/dist/tui/index.d.ts +0 -15
- package/dist/tui/index.js +0 -162
- package/dist/tui/ink-context.d.ts +0 -18
- package/dist/tui/ink-context.js +0 -59
- package/dist/tui/ink-loader.d.ts +0 -26
- package/dist/tui/ink-loader.js +0 -42
- package/dist/tui/removed-commands.d.ts +0 -9
- package/dist/tui/removed-commands.js +0 -22
- package/dist/tui/sse-handler.d.ts +0 -52
- package/dist/tui/sse-handler.js +0 -157
- package/dist/tui/store.d.ts +0 -598
- package/dist/tui/store.js +0 -753
- package/dist/tui/utils/format.d.ts +0 -56
- package/dist/tui/utils/format.js +0 -155
- package/dist/tui/utils/fullscreen.d.ts +0 -23
- package/dist/tui/utils/fullscreen.js +0 -71
- package/dist/tui/utils/history.d.ts +0 -10
- package/dist/tui/utils/history.js +0 -85
- package/dist/tui/utils/platform.d.ts +0 -45
- package/dist/tui/utils/platform.js +0 -258
- package/dist/tui/utils/theme.d.ts +0 -21
- package/dist/tui/utils/theme.js +0 -24
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.agentSessionWorkflow = agentSessionWorkflow;
|
|
4
4
|
const workflow_1 = require("@temporalio/workflow");
|
|
5
5
|
const search_attributes_1 = require("../utils/search-attributes");
|
|
6
|
+
const constants_1 = require("../constants");
|
|
6
7
|
const common_1 = require("@temporalio/common");
|
|
7
8
|
/**
|
|
8
9
|
* Workflow-deterministic clock. The Temporal TS SDK intercepts `new Date()` at the
|
|
@@ -61,15 +62,6 @@ async function agentSessionWorkflow(input) {
|
|
|
61
62
|
// PR-C commit 6 (#119a): each attachment carries its own `leaseMs` (negotiated at
|
|
62
63
|
// claim time). No workflow-side default constant — heartbeats extend `expiresAt`
|
|
63
64
|
// by `currentAttachment.leaseMs`.
|
|
64
|
-
/**
|
|
65
|
-
* Legacy CAN-extension constant. Retained solely so sessions that ran on a
|
|
66
|
-
* pre-#249 workflow bundle replay deterministically: the `patched()` branch at
|
|
67
|
-
* the CAN-boundary extension site selects this constant on the non-patched
|
|
68
|
-
* side, matching the exact arg sequence those histories recorded. New runs
|
|
69
|
-
* (and all post-#249 CAN transitions) use `currentAttachment.leaseMs` instead
|
|
70
|
-
* — see the call site below.
|
|
71
|
-
*/
|
|
72
|
-
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
73
65
|
/**
|
|
74
66
|
* Default grace period for `draining → detached` transition after requestDetach. Used when a
|
|
75
67
|
* `requestDetach` signal omits `deadlineMs`. Per-signal overrides are honored via the
|
|
@@ -78,65 +70,51 @@ async function agentSessionWorkflow(input) {
|
|
|
78
70
|
const DEFAULT_DRAINING_DEADLINE_MS = 5_000;
|
|
79
71
|
/** Max duration a messageId can stay in-flight before the safety timer ejects it. */
|
|
80
72
|
const PROCESSING_DEADLINE_MS = 15 * 60 * 1000;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// Readers go through the dual-read helpers in utils/search-attributes.ts
|
|
113
|
-
// (memo preferred, SA fallback). Filter SAs (Ensemble, PlayerId, Hostname,
|
|
114
|
-
// AttachedHost, AttachmentState) are untouched — they appear in visibility
|
|
115
|
-
// query expressions and MUST stay search attributes.
|
|
116
|
-
const saDiet = (0, workflow_1.patched)('v1.8-sa-diet');
|
|
117
|
-
// T0.1 (#748) — the observation-path memo extension: workDir / agentType /
|
|
118
|
-
// gitBranch join the memo so the cloud-profile maestro scan can read the
|
|
119
|
-
// full player row from visibility list results (zero per-player queries).
|
|
120
|
-
// Payload-only upsertMemo change, but gated behind its own marker per the
|
|
121
|
-
// repo's conservative determinism policy. All three pass the low-churn bar
|
|
122
|
-
// (workDir/agentType set-at-start; gitBranch via rare updateMetadata).
|
|
123
|
-
const memoObservationFields = (0, workflow_1.patched)('v1.8-memo-observation-fields');
|
|
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;
|
|
98
|
+
// ── 2.0 clean-slate (#787) ──
|
|
99
|
+
// The replay-only `patched()` markers that protected in-flight 1.x sessions
|
|
100
|
+
// across rolling deploys are gone: 2.0 is a hard cutover (the #786 boot guard
|
|
101
|
+
// refuses to replay any 1.x history), so there are no pre-patch histories left
|
|
102
|
+
// to protect. The SA-diet (#747) and observation-field memo (#748) behavior is
|
|
103
|
+
// now unconditional — the new path IS the only path.
|
|
124
104
|
/**
|
|
125
105
|
* T0.5 — the memo mirror of the migrated read-only metadata fields.
|
|
126
106
|
* Shared by the run-start upsert and the updateMetadata handler so the
|
|
127
107
|
* two write sites can't drift. Key names come from the shared
|
|
128
108
|
* {@link MEMO_KEYS} registry (also used by the client-side
|
|
129
|
-
* `workflow.start({ memo })` seeds and the
|
|
109
|
+
* `workflow.start({ memo })` seeds and the memo readers).
|
|
130
110
|
*/
|
|
131
111
|
const metaMemo = () => ({
|
|
132
112
|
...(input.metadata.gitRoot ? { [search_attributes_1.MEMO_KEYS.gitRoot]: input.metadata.gitRoot } : {}),
|
|
133
113
|
...(input.metadata.playerType ? { [search_attributes_1.MEMO_KEYS.playerType]: input.metadata.playerType } : {}),
|
|
134
114
|
[search_attributes_1.MEMO_KEYS.isConductor]: input.metadata.isConductor === true,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
...(input.metadata.gitBranch ? { [search_attributes_1.MEMO_KEYS.gitBranch]: input.metadata.gitBranch } : {}),
|
|
139
|
-
} : {}),
|
|
115
|
+
[search_attributes_1.MEMO_KEYS.workDir]: input.metadata.workDir,
|
|
116
|
+
[search_attributes_1.MEMO_KEYS.agentType]: input.metadata.agentType || 'claude',
|
|
117
|
+
...(input.metadata.gitBranch ? { [search_attributes_1.MEMO_KEYS.gitBranch]: input.metadata.gitBranch } : {}),
|
|
140
118
|
});
|
|
141
119
|
// Ensure search attributes are always current — critical when reconnecting
|
|
142
120
|
// via WorkflowIdConflictPolicy.USE_EXISTING, which skips the attributes
|
|
@@ -145,17 +123,20 @@ async function agentSessionWorkflow(input) {
|
|
|
145
123
|
AgentTempoEnsemble: [input.metadata.ensemble],
|
|
146
124
|
AgentTempoPlayerId: [input.metadata.playerId],
|
|
147
125
|
AgentTempoHostname: [input.metadata.hostname],
|
|
148
|
-
...(saDiet ? {} : {
|
|
149
|
-
...(input.metadata.gitRoot ? { AgentTempoGitRoot: [input.metadata.gitRoot] } : {}),
|
|
150
|
-
...(input.metadata.playerType ? { AgentTempoPlayerType: [input.metadata.playerType] } : {}),
|
|
151
|
-
AgentTempoIsConductor: [input.metadata.isConductor === true],
|
|
152
|
-
}),
|
|
153
126
|
// v0.25 attachment search attributes — initial values for a fresh/restored workflow.
|
|
154
127
|
// Updated on every phase transition below.
|
|
155
128
|
AgentTempoAttachedHost: [input.currentAttachment?.hostname ?? ''],
|
|
156
129
|
AgentTempoAttachmentState: [input.phase ?? 'booting'],
|
|
157
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [input.currentAttachment?.attachmentId ?? ''] }),
|
|
158
130
|
});
|
|
131
|
+
// #786 — 2.0 cutover protocol STAMP. Authoritative from the constant (this IS
|
|
132
|
+
// the 2.0 bundle → always PROTOCOL_VERSION, regardless of `input.protocol`).
|
|
133
|
+
// Re-upserted on EVERY run, incl. continueAsNew successors, so visibility
|
|
134
|
+
// always shows the stamp for the daemon boot guard — no need to thread a memo
|
|
135
|
+
// arg through the CAN payload. A 1.x run never executes this, so the guard
|
|
136
|
+
// sees its memo as undefined → un-stamped → refuse. Unconditional (no
|
|
137
|
+
// `patched()` marker): #786 is the first change on the 2.0 line, so there are
|
|
138
|
+
// no pre-stamp v2 histories to protect from this new command.
|
|
139
|
+
(0, workflow_1.upsertMemo)({ [search_attributes_1.MEMO_KEYS.protocol]: constants_1.PROTOCOL_VERSION });
|
|
159
140
|
// ── State (carried across continue-as-new) ──
|
|
160
141
|
let part = input.part ?? input.autoSummary ?? 'No description set';
|
|
161
142
|
// T0.5 (#747) — memo carrier for the migrated read-only fields + part.
|
|
@@ -164,9 +145,7 @@ async function agentSessionWorkflow(input) {
|
|
|
164
145
|
// `memo` option so the fields are visible before the first workflow task
|
|
165
146
|
// (and on standard-visibility servers whose list results may lag memo
|
|
166
147
|
// upserts — see PR #747 T0.5 notes). Low-churn fields ONLY.
|
|
167
|
-
|
|
168
|
-
(0, workflow_1.upsertMemo)({ ...metaMemo(), [search_attributes_1.MEMO_KEYS.part]: part });
|
|
169
|
-
}
|
|
148
|
+
(0, workflow_1.upsertMemo)({ ...metaMemo(), [search_attributes_1.MEMO_KEYS.part]: part });
|
|
170
149
|
const messages = input.messages ?? [];
|
|
171
150
|
const sentMessages = input.sentMessages ?? [];
|
|
172
151
|
const outbox = input.outbox ?? [];
|
|
@@ -234,6 +213,44 @@ async function agentSessionWorkflow(input) {
|
|
|
234
213
|
: undefined;
|
|
235
214
|
/** ISO timestamp of when the workflow most recently entered `detached`. */
|
|
236
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
|
+
}
|
|
237
254
|
// ── Processing Lifecycle State (fixes #99) ──
|
|
238
255
|
// Tracks messages currently being processed by a blocking adapter. While non-empty,
|
|
239
256
|
// stale detection is suppressed AND the phase refines to `processing`.
|
|
@@ -316,6 +333,11 @@ async function agentSessionWorkflow(input) {
|
|
|
316
333
|
function nextDeadlineMs() {
|
|
317
334
|
const nowMs = workflowNow().getTime();
|
|
318
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
|
+
}
|
|
319
341
|
if (currentAttachment) {
|
|
320
342
|
candidates.push(new Date(currentAttachment.expiresAt).getTime());
|
|
321
343
|
}
|
|
@@ -408,7 +430,7 @@ async function agentSessionWorkflow(input) {
|
|
|
408
430
|
// #399 W2 — every inbound cue counts as received traffic.
|
|
409
431
|
receivedCount++;
|
|
410
432
|
// Track inbound messages that expect a response (default: true for backward compat)
|
|
411
|
-
if (
|
|
433
|
+
if (msg.responseRequested !== false) {
|
|
412
434
|
lastInboundRRTime = workflowNow().getTime();
|
|
413
435
|
}
|
|
414
436
|
});
|
|
@@ -418,8 +440,7 @@ async function agentSessionWorkflow(input) {
|
|
|
418
440
|
// daemon aggregate — T0.1) can read it from visibility list results
|
|
419
441
|
// instead of a per-player getPart query. Low-churn by nature: part
|
|
420
442
|
// changes when a player re-describes its work, not per message.
|
|
421
|
-
|
|
422
|
-
(0, workflow_1.upsertMemo)({ [search_attributes_1.MEMO_KEYS.part]: newPart });
|
|
443
|
+
(0, workflow_1.upsertMemo)({ [search_attributes_1.MEMO_KEYS.part]: newPart });
|
|
423
444
|
lastActivityTime = workflowNow().getTime();
|
|
424
445
|
activityCount++;
|
|
425
446
|
lastOutboundTime = workflowNow().getTime();
|
|
@@ -461,7 +482,6 @@ async function agentSessionWorkflow(input) {
|
|
|
461
482
|
lastActivityTime = workflowNow().getTime();
|
|
462
483
|
activityCount++;
|
|
463
484
|
});
|
|
464
|
-
(0, workflow_1.setHandler)(signals_1.pendingResetQuery, () => pendingReset);
|
|
465
485
|
(0, workflow_1.setHandler)(signals_1.ackResetSignal, (resetId) => {
|
|
466
486
|
// Race-safe: only clear if the ack matches the current pending reset, so a
|
|
467
487
|
// newer reset landing during the extension's wipe isn't silently dropped.
|
|
@@ -494,16 +514,10 @@ async function agentSessionWorkflow(input) {
|
|
|
494
514
|
AgentTempoEnsemble: [input.metadata.ensemble],
|
|
495
515
|
AgentTempoPlayerId: [input.metadata.playerId],
|
|
496
516
|
AgentTempoHostname: [input.metadata.hostname],
|
|
497
|
-
...(saDiet ? {} : {
|
|
498
|
-
...(input.metadata.gitRoot ? { AgentTempoGitRoot: [input.metadata.gitRoot] } : {}),
|
|
499
|
-
...(input.metadata.playerType ? { AgentTempoPlayerType: [input.metadata.playerType] } : {}),
|
|
500
|
-
AgentTempoIsConductor: [input.metadata.isConductor === true],
|
|
501
|
-
}),
|
|
502
517
|
});
|
|
503
518
|
// T0.5 — keep the memo mirror current (low-churn: metadata updates are
|
|
504
519
|
// rare lifecycle events, not per-message traffic).
|
|
505
|
-
|
|
506
|
-
(0, workflow_1.upsertMemo)(metaMemo());
|
|
520
|
+
(0, workflow_1.upsertMemo)(metaMemo());
|
|
507
521
|
lastActivityTime = workflowNow().getTime();
|
|
508
522
|
activityCount++;
|
|
509
523
|
});
|
|
@@ -588,6 +602,10 @@ async function agentSessionWorkflow(input) {
|
|
|
588
602
|
// Phase refinement: if we're attached (or awaiting), move to `processing`.
|
|
589
603
|
if (phase === 'attached' || phase === 'awaiting')
|
|
590
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++;
|
|
591
609
|
}
|
|
592
610
|
lastActivityTime = workflowNow().getTime();
|
|
593
611
|
activityCount++;
|
|
@@ -618,6 +636,10 @@ async function agentSessionWorkflow(input) {
|
|
|
618
636
|
const outboxIdle = !outbox.some((e) => e.status === 'pending' || e.status === 'processing');
|
|
619
637
|
setPhase(outboxIdle ? 'awaiting' : 'attached');
|
|
620
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++;
|
|
621
643
|
}
|
|
622
644
|
lastActivityTime = workflowNow().getTime();
|
|
623
645
|
activityCount++;
|
|
@@ -711,8 +733,12 @@ async function agentSessionWorkflow(input) {
|
|
|
711
733
|
}
|
|
712
734
|
(0, workflow_1.upsertSearchAttributes)({
|
|
713
735
|
AgentTempoAttachedHost: [''],
|
|
714
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [''] }),
|
|
715
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' });
|
|
716
742
|
setPhase('gone');
|
|
717
743
|
// Inject a final audit message so the old adapter-completion path has something to show.
|
|
718
744
|
messages.push({
|
|
@@ -724,6 +750,10 @@ async function agentSessionWorkflow(input) {
|
|
|
724
750
|
});
|
|
725
751
|
lastActivityTime = workflowNow().getTime();
|
|
726
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++;
|
|
727
757
|
});
|
|
728
758
|
(0, workflow_1.setHandler)(signals_1.isDestroyedQuery, () => destroyed || destroyRequested);
|
|
729
759
|
// ── Test-only CAN trigger (#226) ──
|
|
@@ -767,6 +797,10 @@ async function agentSessionWorkflow(input) {
|
|
|
767
797
|
currentAttachment.leaseMs = leaseMs;
|
|
768
798
|
lastActivityTime = nowMs;
|
|
769
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++;
|
|
770
804
|
return attachmentTokenFrom(currentAttachment, leaseMs);
|
|
771
805
|
}
|
|
772
806
|
// Conflict: active lease held by someone else.
|
|
@@ -796,16 +830,33 @@ async function agentSessionWorkflow(input) {
|
|
|
796
830
|
drainingSince = null;
|
|
797
831
|
drainingDeadlineMs = null;
|
|
798
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;
|
|
799
836
|
setPhase('attached');
|
|
800
837
|
(0, workflow_1.upsertSearchAttributes)({
|
|
801
838
|
AgentTempoAttachedHost: [host],
|
|
802
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [newAttachment.attachmentId] }),
|
|
803
839
|
});
|
|
804
840
|
lastActivityTime = nowMs;
|
|
805
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++;
|
|
806
846
|
return attachmentTokenFrom(newAttachment, leaseMs);
|
|
807
847
|
}, {
|
|
808
|
-
validator: ({ leaseMs }) => {
|
|
848
|
+
validator: ({ host, leaseMs, protocolVersion }) => {
|
|
849
|
+
// #786 — cross-host cutover safety, checked FIRST (pure, synchronous, no
|
|
850
|
+
// IO, no history event — rejects pre-admission). A v1 adapter omits
|
|
851
|
+
// `protocolVersion` (→ undefined); any value other than PROTOCOL_VERSION is
|
|
852
|
+
// a stale/foreign adapter that must not claim this 2.0 workflow. Actionable
|
|
853
|
+
// error names the host + the fix.
|
|
854
|
+
if (protocolVersion !== constants_1.PROTOCOL_VERSION) {
|
|
855
|
+
throw common_1.ApplicationFailure.nonRetryable(`claimAttachment rejected on ${(0, workflow_1.workflowInfo)().workflowId}: adapter on host ` +
|
|
856
|
+
`'${host}' speaks protocol ${protocolVersion ?? '(v1/unset)'}, but this is a ` +
|
|
857
|
+
`protocol-${constants_1.PROTOCOL_VERSION} (2.0) workflow. Upgrade the agent-tempo install on ` +
|
|
858
|
+
`that host to 2.0 (it cannot drive a 2.0 session).`, 'ProtocolMismatch');
|
|
859
|
+
}
|
|
809
860
|
if (!Number.isInteger(leaseMs) || leaseMs < 1_000 || leaseMs > 600_000) {
|
|
810
861
|
throw common_1.ApplicationFailure.nonRetryable(`leaseMs must be between 1000 and 600000, got ${leaseMs}`, 'InvalidLease');
|
|
811
862
|
}
|
|
@@ -881,7 +932,7 @@ async function agentSessionWorkflow(input) {
|
|
|
881
932
|
// mismatch any history that recorded the race — conservative tiebreak.
|
|
882
933
|
// Evaluated lazily (the v0.26-can-lease-from-attachment single-use
|
|
883
934
|
// precedent): only histories that hit forceDetach record the marker.
|
|
884
|
-
if (
|
|
935
|
+
if (currentAttachment?.attachmentId !== reaped.attachmentId) {
|
|
885
936
|
workflow_1.log.info(`forceDetach: attachment changed during hard-terminate await ` +
|
|
886
937
|
`(expected ${reaped.attachmentId}, now ${currentAttachment?.attachmentId ?? 'none'}) — ` +
|
|
887
938
|
`not clobbering the fresh claim`);
|
|
@@ -898,7 +949,6 @@ async function agentSessionWorkflow(input) {
|
|
|
898
949
|
setPhase('detached');
|
|
899
950
|
(0, workflow_1.upsertSearchAttributes)({
|
|
900
951
|
AgentTempoAttachedHost: [''],
|
|
901
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [''] }),
|
|
902
952
|
});
|
|
903
953
|
// #159 Gap 1b: wake the main loop — `phase === 'detached'` isn't in the predicate
|
|
904
954
|
// and the condition would otherwise sleep on the now-stale lease-expiry deadline.
|
|
@@ -1025,7 +1075,6 @@ async function agentSessionWorkflow(input) {
|
|
|
1025
1075
|
setPhase('detached');
|
|
1026
1076
|
(0, workflow_1.upsertSearchAttributes)({
|
|
1027
1077
|
AgentTempoAttachedHost: [''],
|
|
1028
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [''] }),
|
|
1029
1078
|
});
|
|
1030
1079
|
lastActivityTime = workflowNow().getTime();
|
|
1031
1080
|
activityCount++;
|
|
@@ -1356,12 +1405,10 @@ async function agentSessionWorkflow(input) {
|
|
|
1356
1405
|
// live handler. Reconciled entries keep the ORIGINAL receipt time as
|
|
1357
1406
|
// `reportedAt` and carry `reconciled: true`.
|
|
1358
1407
|
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
|
|
1363
|
-
// dependency on replay. Conservative tiebreak per the ruling: marker.
|
|
1364
|
-
if ((0, workflow_1.patched)('v0.27-stage-reconcile-reports')) {
|
|
1408
|
+
// Stage reconciliation — fold each player's latest result/blocker report
|
|
1409
|
+
// into stage status, then apply the stage's completion/failure transitions.
|
|
1410
|
+
// (2.0: was patched()-gated for 1.x replay determinism; now unconditional — #787.)
|
|
1411
|
+
{
|
|
1365
1412
|
const nowMs = workflowNow().getTime();
|
|
1366
1413
|
for (const playerEntry of entry.players) {
|
|
1367
1414
|
// Most recent stage-relevant (result|blocker) report from this
|
|
@@ -1481,52 +1528,36 @@ async function agentSessionWorkflow(input) {
|
|
|
1481
1528
|
// timer would leave the workflow in `draining` until that old timer fired.
|
|
1482
1529
|
const epochAtWait = wakeEpoch;
|
|
1483
1530
|
const deadlineMs = nextDeadlineMs();
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
|
|
1498
|
-
//
|
|
1499
|
-
// Without the fallback wake, a workflow waiting in `condition(predicate)` on
|
|
1500
|
-
// an `Infinity` deadline (booting / detached, no draining, no processing)
|
|
1501
|
-
// never re-evaluates `nextDeadlineMs()` after one of these handlers fires —
|
|
1502
|
-
// the freshly-set lease-expiry timer is never picked up, lease expiry is
|
|
1503
|
-
// never reaped, and the workflow stalls until external state forces the
|
|
1504
|
-
// predicate true. Smoking-gun test that fails without the fallback:
|
|
1505
|
-
// `test/session-phase-processing.test.ts:54` "attached -> processing ->
|
|
1506
|
-
// awaiting via processingStart/End (#117 fix)" — times out at 10s because
|
|
1507
|
-
// the loop never makes progress after `processingStart` lands on a fresh
|
|
1508
|
-
// claim.
|
|
1509
|
-
//
|
|
1510
|
-
// Removing this fallback safely is a "main-loop wake-discipline cleanup"
|
|
1511
|
-
// separate from the audit's framing — adds `wakeEpoch++` to each affected
|
|
1512
|
-
// handler, gates with `patched()` markers for replay-determinism (live
|
|
1513
|
-
// workflow histories already recorded the existing `Timer 5min` events),
|
|
1514
|
-
// and adds a regression test covering the handler-induced-deadline pickup.
|
|
1515
|
-
// Estimated 4–6 handler edits + tests, separate dedicated PR. See the
|
|
1516
|
-
// 2026-04-26 forensics for the full mechanism walkthrough — link from this
|
|
1517
|
-
// file's PR history.
|
|
1518
|
-
//
|
|
1519
|
-
// Until that cleanup happens, DO NOT remove this fallback. The
|
|
1520
|
-
// `Math.min(deadlineMs, 5 * 60 * 1000)` cap is part of the same mechanism:
|
|
1521
|
-
// it ensures every deadline (even hour-long lease-expiry timers) is
|
|
1522
|
-
// re-evaluated at least every 5 min so handler-induced deadline shortenings
|
|
1523
|
-
// can't be missed.
|
|
1524
|
-
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 ||
|
|
1525
1545
|
canDispatch() ||
|
|
1526
1546
|
hasPendingStop() ||
|
|
1527
1547
|
phase === 'gone' ||
|
|
1528
|
-
wakeEpoch !== epochAtWait,
|
|
1529
|
-
|
|
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
|
+
}
|
|
1530
1561
|
if (destroyRequested)
|
|
1531
1562
|
break;
|
|
1532
1563
|
// ── §9.5.a: Lease expiry — reap attachment and transition to `detached`. ──
|
|
@@ -1543,10 +1574,73 @@ async function agentSessionWorkflow(input) {
|
|
|
1543
1574
|
setPhase('detached');
|
|
1544
1575
|
(0, workflow_1.upsertSearchAttributes)({
|
|
1545
1576
|
AgentTempoAttachedHost: [''],
|
|
1546
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [''] }),
|
|
1547
1577
|
});
|
|
1548
1578
|
workflow_1.log.warn(`lease expired for attachment ${reaped.attachmentId} (host=${reaped.hostname})`);
|
|
1549
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
|
+
}
|
|
1550
1644
|
// ── §9.5.b: processingDeadline — force exit from `processing` if a messageId is wedged. ──
|
|
1551
1645
|
if (processingSince !== null &&
|
|
1552
1646
|
workflowNow().getTime() - new Date(processingSince).getTime() > PROCESSING_DEADLINE_MS) {
|
|
@@ -1607,7 +1701,6 @@ async function agentSessionWorkflow(input) {
|
|
|
1607
1701
|
setPhase('detached');
|
|
1608
1702
|
(0, workflow_1.upsertSearchAttributes)({
|
|
1609
1703
|
AgentTempoAttachedHost: [''],
|
|
1610
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [''] }),
|
|
1611
1704
|
});
|
|
1612
1705
|
if (reaped) {
|
|
1613
1706
|
workflow_1.log.info(`drainingDeadline exceeded (${Math.round(window / 1000)}s); ` +
|
|
@@ -1821,6 +1914,12 @@ async function agentSessionWorkflow(input) {
|
|
|
1821
1914
|
adapterId: currentAttachment.adapterId,
|
|
1822
1915
|
};
|
|
1823
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.)
|
|
1824
1923
|
currentAttachment = null;
|
|
1825
1924
|
inFlightMessages.clear();
|
|
1826
1925
|
processingSince = null;
|
|
@@ -1830,7 +1929,6 @@ async function agentSessionWorkflow(input) {
|
|
|
1830
1929
|
setPhase('detached');
|
|
1831
1930
|
(0, workflow_1.upsertSearchAttributes)({
|
|
1832
1931
|
AgentTempoAttachedHost: [''],
|
|
1833
|
-
...(saDiet ? {} : { AgentTempoAttachmentId: [''] }),
|
|
1834
1932
|
});
|
|
1835
1933
|
workflow_1.log.warn(`spawn failed for "${entry.targetName}"; rolled back attachment ${entry.attachmentId} → detached`);
|
|
1836
1934
|
}
|
|
@@ -1873,25 +1971,11 @@ async function agentSessionWorkflow(input) {
|
|
|
1873
1971
|
// matches what the adapter signed up for and covers at least one full heartbeat
|
|
1874
1972
|
// interval for every adapter class.
|
|
1875
1973
|
//
|
|
1876
|
-
// The `patched()` gate keeps replay of pre-#249 workflow runs deterministic:
|
|
1877
|
-
// histories that CAN'd on the old bundle recorded `extendAttachmentForCAN(…, 30_000, …)`,
|
|
1878
|
-
// so replaying those runs must pick the legacy constant. New runs (and in-flight
|
|
1879
|
-
// runs that CAN *after* the deploy) take the patched branch.
|
|
1880
|
-
//
|
|
1881
1974
|
// Math lives in `./attachment-math.ts` for direct unit testability (#127).
|
|
1882
|
-
//
|
|
1883
|
-
//
|
|
1884
|
-
// position it was introduced in — relocating it inside the
|
|
1885
|
-
// `currentAttachment ?` branch would skip marker recording on histories
|
|
1886
|
-
// that hit the CAN site with a null attachment, risking replay
|
|
1887
|
-
// non-determinism against those recordings. The dead-code cleanup is
|
|
1888
|
-
// strictly the removal of the `?? HEARTBEAT_INTERVAL_MS` fallback that
|
|
1889
|
-
// used to sit inside the ternary: on the patched branch it never fires
|
|
1890
|
-
// (Attachment.leaseMs is required), and on the pre-patched branch the
|
|
1891
|
-
// fallback is replaced by the bare constant — same value either way.
|
|
1892
|
-
const usePatchedLease = (0, workflow_1.patched)('v0.26-can-lease-from-attachment');
|
|
1975
|
+
// (2.0: the v0.26-can-lease-from-attachment patched() gate is gone — #787 —
|
|
1976
|
+
// so CAN always extends by `currentAttachment.leaseMs` = 3 × heartbeatMs.)
|
|
1893
1977
|
const extendedAttachment = currentAttachment
|
|
1894
|
-
? (0, attachment_math_1.extendAttachmentForCAN)(currentAttachment,
|
|
1978
|
+
? (0, attachment_math_1.extendAttachmentForCAN)(currentAttachment, currentAttachment.leaseMs, workflowNow().getTime())
|
|
1895
1979
|
: undefined;
|
|
1896
1980
|
await (0, workflow_1.continueAsNew)({
|
|
1897
1981
|
...input,
|