@ours.network/fleet 0.19.0-nightly.8 → 1.0.0
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/README.md +96 -11
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +12 -0
- package/dist/application/role-creation-service.js +3 -1
- package/dist/application/types.d.ts +11 -0
- package/dist/briefing.d.ts +1 -1
- package/dist/briefing.js +103 -10
- package/dist/build-info.json +5 -5
- package/dist/cli.js +41 -11
- package/dist/config.d.ts +21 -7
- package/dist/config.js +32 -18
- package/dist/creation.d.ts +1 -1
- package/dist/docs.d.ts +2 -2
- package/dist/docs.js +145 -24
- package/dist/doctor.js +10 -10
- package/dist/duration.js +1 -1
- package/dist/fleet-proxy.d.ts +5 -0
- package/dist/harness/acp-agent.js +11 -6
- package/dist/harness/claude-code.js +201 -8
- package/dist/harness/codex.d.ts +4 -1
- package/dist/harness/codex.js +72 -13
- package/dist/harness/types.d.ts +57 -6
- package/dist/isolation/bubblewrap.d.ts +4 -4
- package/dist/isolation/bubblewrap.js +4 -4
- package/dist/isolation/policy.d.ts +1 -1
- package/dist/isolation/policy.js +2 -2
- package/dist/isolation/registry.d.ts +3 -3
- package/dist/isolation/registry.js +5 -5
- package/dist/isolation/resources.d.ts +1 -1
- package/dist/isolation/resources.js +1 -1
- package/dist/isolation/types.d.ts +3 -3
- package/dist/loops/manager.d.ts +30 -1
- package/dist/loops/manager.js +69 -6
- package/dist/loops/state.d.ts +18 -0
- package/dist/loops/state.js +4 -0
- package/dist/model-env.d.ts +71 -0
- package/dist/model-env.js +106 -0
- package/dist/monitor.d.ts +2 -2
- package/dist/monitor.js +4 -4
- package/dist/ops.d.ts +2 -2
- package/dist/ops.js +9 -9
- package/dist/owner-channel/channel.d.ts +17 -0
- package/dist/owner-channel/channel.js +104 -23
- package/dist/owner-channel/commands.d.ts +9 -0
- package/dist/owner-channel/commands.js +226 -99
- package/dist/owner-channel/notices.d.ts +7 -0
- package/dist/owner-channel/notices.js +9 -0
- package/dist/rooms-tasks/cli.js +733 -221
- package/dist/rooms-tasks/close.d.ts +35 -0
- package/dist/rooms-tasks/close.js +182 -0
- package/dist/rooms-tasks/config.js +8 -5
- package/dist/rooms-tasks/cowork-adapter.d.ts +22 -0
- package/dist/rooms-tasks/cowork-adapter.js +116 -0
- package/dist/rooms-tasks/external-worker.d.ts +2 -0
- package/dist/rooms-tasks/external-worker.js +40 -0
- package/dist/rooms-tasks/index.d.ts +2 -0
- package/dist/rooms-tasks/index.js +2 -0
- package/dist/rooms-tasks/markdown.d.ts +49 -0
- package/dist/rooms-tasks/markdown.js +206 -0
- package/dist/rooms-tasks/member-startup.d.ts +51 -0
- package/dist/rooms-tasks/member-startup.js +151 -0
- package/dist/rooms-tasks/provision.d.ts +8 -0
- package/dist/rooms-tasks/provision.js +363 -97
- package/dist/rooms-tasks/room-state.d.ts +13 -1
- package/dist/rooms-tasks/room-state.js +149 -1
- package/dist/rooms-tasks/task-state.d.ts +13 -2
- package/dist/rooms-tasks/task-state.js +131 -3
- package/dist/rooms-tasks/terminal.d.ts +23 -0
- package/dist/rooms-tasks/terminal.js +56 -0
- package/dist/rooms-tasks/types.d.ts +109 -4
- package/dist/rooms-tasks/types.js +1 -1
- package/dist/runner.d.ts +48 -0
- package/dist/runner.js +252 -100
- package/dist/session/acp.d.ts +106 -2
- package/dist/session/acp.js +217 -14
- package/dist/session/activity.d.ts +31 -0
- package/dist/session/activity.js +48 -0
- package/dist/session/control.d.ts +1 -1
- package/dist/session/conversation-normalizer.d.ts +7 -1
- package/dist/session/conversation-normalizer.js +154 -11
- package/dist/session/conversation-store.js +1 -1
- package/dist/session/conversation-types.d.ts +25 -6
- package/dist/session/types.d.ts +35 -0
- package/dist/spawn.d.ts +4 -2
- package/dist/spawn.js +39 -26
- package/dist/supervisor/launchd.d.ts +2 -2
- package/dist/supervisor/launchd.js +4 -4
- package/dist/supervisor/systemd.js +4 -4
- package/dist/supervisor/types.d.ts +1 -1
- package/dist/temp-lifecycle.d.ts +14 -2
- package/dist/temp-lifecycle.js +90 -0
- package/dist/watchdog/alerts.d.ts +2 -2
- package/dist/watchdog/alerts.js +2 -2
- package/dist/watchdog/briefing.d.ts +5 -5
- package/dist/watchdog/briefing.js +13 -6
- package/dist/watchdog/config.js +1 -1
- package/dist/watchdog/query.d.ts +2 -2
- package/dist/watchdog/query.js +2 -2
- package/dist/watchdog/report.js +2 -2
- package/dist/watchdog/run.d.ts +2 -2
- package/dist/watchdog/run.js +6 -6
- package/dist/watchdog/scheduler.d.ts +7 -7
- package/dist/watchdog/scheduler.js +10 -10
- package/dist/watchdog/service.d.ts +4 -4
- package/dist/watchdog/service.js +5 -6
- package/dist/watchdog/store.d.ts +4 -4
- package/dist/watchdog/store.js +4 -4
- package/dist/web/runtime.js +1 -1
- package/dist/web/server.js +1 -1
- package/dist/web/topology-promote.js +1 -1
- package/dist/web-app/assets/{TerminalView-BAVk1Bot.js → TerminalView-C_G1ID2P.js} +1 -1
- package/dist/web-app/assets/{index-C3S-xFRU.js → index-BCBK78hw.js} +5 -5
- package/dist/web-app/index.html +1 -1
- package/dist/worklog.d.ts +7 -1
- package/dist/worklog.js +191 -39
- package/package.json +1 -1
package/dist/session/acp.d.ts
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import * as acp from '@agentclientprotocol/sdk';
|
|
2
2
|
import type { CommonPermissions } from '../config.js';
|
|
3
|
+
import type { AcpMcpServer } from '../harness/types.js';
|
|
3
4
|
import { ConversationEventStore } from './conversation-store.js';
|
|
4
5
|
import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
5
6
|
import type { ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent, RuntimeSelectorMetadata, SessionHandle, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
|
|
6
7
|
/** Bound safe-boundary waiting without turning a hung tool into cancellation. */
|
|
7
8
|
export declare const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120000;
|
|
9
|
+
/**
|
|
10
|
+
* How long a steering-started turn is presumed to still own the adapter after
|
|
11
|
+
* its last update. Such a turn has no prompt id, so it never reports a
|
|
12
|
+
* stopReason and there is no exact end to observe — silence is the only signal
|
|
13
|
+
* available, and this is the bound that turns it into a decision.
|
|
14
|
+
*
|
|
15
|
+
* Sized from the fleet's own scheduled-run history: across 1513 completed
|
|
16
|
+
* scheduled runs the longest silence WITHIN a working turn was 120.2 s (p99
|
|
17
|
+
* 41.0 s; 5 runs above 60 s). A shorter grace would release the lease while the
|
|
18
|
+
* adapter is still working and re-admit a prompt into a busy turn, which is the
|
|
19
|
+
* FLEET-003 failure itself. The costs are deliberately asymmetric: holding too
|
|
20
|
+
* long skips one best-effort maintenance tick, releasing too early SIGTERMs a
|
|
21
|
+
* live role.
|
|
22
|
+
*/
|
|
23
|
+
export declare const STEERING_OCCUPANCY_IDLE_MS = 150000;
|
|
8
24
|
/** Server-generated typed provenance followed by the exact human-authored body. */
|
|
9
25
|
export declare function promptContentBlocks(text: string, origin?: PromptOrigin): acp.ContentBlock[];
|
|
10
26
|
export declare function runtimeSelector(options: acp.SessionConfigOption[] | null | undefined, category: string): RuntimeSelectorMetadata | undefined;
|
|
@@ -20,6 +36,22 @@ export interface AcpSessionOptions {
|
|
|
20
36
|
modeId?: string;
|
|
21
37
|
/** Adapter-resolved live permission policy; separate from ACP agent-specific session modes. */
|
|
22
38
|
permissionMode?: NonNullable<SessionSnapshot['permissionMode']>;
|
|
39
|
+
/** Adapter-authenticated request-metadata vocabulary; never inferred from ACP `_meta`. */
|
|
40
|
+
permissionMetadataSource?: 'codex-acp';
|
|
41
|
+
/**
|
|
42
|
+
* MCP servers the ROLE declares, for every session/new, resume and load. Empty
|
|
43
|
+
* or omitted sends `[]`, which is what fleet has always sent and leaves the
|
|
44
|
+
* agent's own configuration untouched.
|
|
45
|
+
*/
|
|
46
|
+
mcpServers?: AcpMcpServer[];
|
|
47
|
+
/**
|
|
48
|
+
* Adapter-supplied `_meta` for session/new — the only route by which a
|
|
49
|
+
* capability the CLI takes as a flag reaches an agent that accepts none.
|
|
50
|
+
* Per-agent vocabulary, so the ADAPTER decides whether there is anything to
|
|
51
|
+
* send; this layer only forwards it. Never sent on resume or load: it carries
|
|
52
|
+
* session-creation options the agent has already applied.
|
|
53
|
+
*/
|
|
54
|
+
sessionMeta?: Record<string, unknown>;
|
|
23
55
|
log(line: string): void;
|
|
24
56
|
/** Test seam for the cancel-escalation grace period; production uses the default. */
|
|
25
57
|
cancelGraceMs?: number;
|
|
@@ -31,6 +63,8 @@ export interface AcpSessionOptions {
|
|
|
31
63
|
controllerGraceMs?: number;
|
|
32
64
|
/** Test seam; production uses AFTER_TOOL_BOUNDARY_TIMEOUT_MS. */
|
|
33
65
|
afterToolBoundaryTimeoutMs?: number;
|
|
66
|
+
/** Test seam; production uses STEERING_OCCUPANCY_IDLE_MS. */
|
|
67
|
+
steeringOccupancyIdleMs?: number;
|
|
34
68
|
}
|
|
35
69
|
/**
|
|
36
70
|
* Classify an ACP `stopReason` into a terminal outcome. A refusal and a
|
|
@@ -49,6 +83,8 @@ export declare class AcpSession implements SessionHandle {
|
|
|
49
83
|
private readonly child;
|
|
50
84
|
private readonly events;
|
|
51
85
|
private readonly conversation;
|
|
86
|
+
/** Cursor before this runner generation began; older durable events stay off the live console. */
|
|
87
|
+
private readonly conversationStartCursor?;
|
|
52
88
|
/** New on every runner start; permission/turn IDs from prior generations are stale. */
|
|
53
89
|
private readonly sessionGeneration;
|
|
54
90
|
/** True while `session/load` replays history as ordinary updates. */
|
|
@@ -58,6 +94,12 @@ export declare class AcpSession implements SessionHandle {
|
|
|
58
94
|
private connection;
|
|
59
95
|
private sessionId?;
|
|
60
96
|
private readiness;
|
|
97
|
+
/**
|
|
98
|
+
* Last non-replayed session update from the agent. `readiness` cannot answer
|
|
99
|
+
* "is this agent working" for a steered turn (FLEET-002), and this is the
|
|
100
|
+
* evidence that can.
|
|
101
|
+
*/
|
|
102
|
+
private lastUpdateAt?;
|
|
61
103
|
private lastError?;
|
|
62
104
|
private promptTail;
|
|
63
105
|
private queueDepth;
|
|
@@ -73,6 +115,13 @@ export declare class AcpSession implements SessionHandle {
|
|
|
73
115
|
private cancelEscalation?;
|
|
74
116
|
private cancelForceKill?;
|
|
75
117
|
private cancelRecoveryReason?;
|
|
118
|
+
/**
|
|
119
|
+
* Held while a steering-started turn is believed to own the adapter. It is a
|
|
120
|
+
* lease, not a latch: `steeringRelease` always fires, so the role can never be
|
|
121
|
+
* stranded busy by a wake whose turn ended without telling anyone.
|
|
122
|
+
*/
|
|
123
|
+
private steeringOccupied;
|
|
124
|
+
private steeringRelease?;
|
|
76
125
|
/**
|
|
77
126
|
* Rejects the moment the adapter process is gone. Every in-flight ACP request
|
|
78
127
|
* races it, so a dead adapter can never leave a turn — and therefore a
|
|
@@ -87,13 +136,27 @@ export declare class AcpSession implements SessionHandle {
|
|
|
87
136
|
private constructor();
|
|
88
137
|
static start(options: AcpSessionOptions): Promise<AcpSession>;
|
|
89
138
|
/**
|
|
90
|
-
* Honest restart recovery
|
|
139
|
+
* Honest restart recovery: a prompt that was admitted but never
|
|
91
140
|
* started is safe to dispatch again; a turn that had already started may
|
|
92
141
|
* have caused side effects, so it is closed as `unknown_after_restart` —
|
|
93
142
|
* never silently replayed.
|
|
94
143
|
*/
|
|
95
144
|
private recoverOpenPrompts;
|
|
96
145
|
isAlive(): boolean;
|
|
146
|
+
/**
|
|
147
|
+
* Take the occupancy lease for a turn the adapter started on its own behalf.
|
|
148
|
+
* Refreshed by every adapter update, so it tracks work actually happening
|
|
149
|
+
* rather than a fixed guess at how long a wake takes.
|
|
150
|
+
*/
|
|
151
|
+
private holdSteeringOccupancy;
|
|
152
|
+
private refreshSteeringOccupancy;
|
|
153
|
+
/**
|
|
154
|
+
* Every exit from occupancy comes through here, including the ones that are
|
|
155
|
+
* not the timer: a real turn boundary, close, and adapter exit. A lease that
|
|
156
|
+
* can leak is worse than the bug it fixes — it would leave the role reporting
|
|
157
|
+
* `running` forever and starve scheduled admission permanently.
|
|
158
|
+
*/
|
|
159
|
+
private releaseSteeringOccupancy;
|
|
97
160
|
snapshot(): SessionSnapshot;
|
|
98
161
|
private toolCall;
|
|
99
162
|
private reserveTool;
|
|
@@ -126,11 +189,33 @@ export declare class AcpSession implements SessionHandle {
|
|
|
126
189
|
* for it is what turned a busy agent into a timeout and then into "dead".
|
|
127
190
|
*/
|
|
128
191
|
queuePrompt(text: string, options?: SubmitPromptOptions): Promise<QueuedPrompt>;
|
|
192
|
+
/**
|
|
193
|
+
* Prepare the session for a prompt that asked to pre-empt current work.
|
|
194
|
+
*
|
|
195
|
+
* The old behaviour was one unconditional `session/cancel` notification
|
|
196
|
+
* followed immediately by `session/prompt`. That is what produced the owner's
|
|
197
|
+
* "request failed before completion":
|
|
198
|
+
*
|
|
199
|
+
* - `cancelActive` only awaits settlement when `this.activeTurn` is set, and
|
|
200
|
+
* a turn the ADAPTER started (steering's `startedNewTurn`) is never tracked
|
|
201
|
+
* here. So the cancel raced the adapter's own transcript repair and the new
|
|
202
|
+
* prompt landed while the last assistant message still held an unresolved
|
|
203
|
+
* `tool_use` — rejected with `stop_reason=tool_use`.
|
|
204
|
+
* - With nothing running at all, it still sent the cancel, and the prompt
|
|
205
|
+
* landed on a bare interrupted user message — rejected with
|
|
206
|
+
* `stop_reason=null`.
|
|
207
|
+
*
|
|
208
|
+
* So: never cancel across a tool boundary, and never cancel something whose
|
|
209
|
+
* settlement cannot be awaited. Everything else is queued, which the ACP queue
|
|
210
|
+
* already does correctly. The returned state is what the caller may claim to a
|
|
211
|
+
* human — `interrupted` only when a turn really was cancelled.
|
|
212
|
+
*/
|
|
213
|
+
private prepareInterruptingDelivery;
|
|
129
214
|
/**
|
|
130
215
|
* Durably record a prompt admission BEFORE acceptance is returned. Browser
|
|
131
216
|
* admissions are transactional — a prompt the ledger cannot hold is refused,
|
|
132
217
|
* because an acknowledged-then-lost prompt is worse than an error. Every
|
|
133
|
-
* other source degrades to best-effort so the agent keeps working
|
|
218
|
+
* other source degrades to best-effort so the agent keeps working.
|
|
134
219
|
*/
|
|
135
220
|
private admitToLedger;
|
|
136
221
|
/** Idempotent browser prompt admission (control v3 `submit_prompt_v2`). */
|
|
@@ -173,6 +258,15 @@ export declare class AcpSession implements SessionHandle {
|
|
|
173
258
|
private settlePendingAutomatically;
|
|
174
259
|
exitResult(): ExitRecord | null;
|
|
175
260
|
close(): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* The role's declared MCP servers, or `[]`.
|
|
263
|
+
*
|
|
264
|
+
* Sent on resume and load as well as on new: the agent builds its server set
|
|
265
|
+
* once per session, so a resumed session that omitted them would come back
|
|
266
|
+
* without the tools the role's config declares — which is exactly the shape of
|
|
267
|
+
* silent drop this plumbing exists to end.
|
|
268
|
+
*/
|
|
269
|
+
private declaredMcpServers;
|
|
176
270
|
private initialize;
|
|
177
271
|
private captureRuntimeMetadata;
|
|
178
272
|
private runPrompt;
|
|
@@ -185,6 +279,15 @@ export declare class AcpSession implements SessionHandle {
|
|
|
185
279
|
*/
|
|
186
280
|
private settleAutomatically;
|
|
187
281
|
private withinAutomaticBoundary;
|
|
282
|
+
/**
|
|
283
|
+
* Codex ACP 1.1.7 marks its protected MCP elicitation bridge on a locationless
|
|
284
|
+
* execute request. The marker is meaningful only together with the runner's
|
|
285
|
+
* independently supplied, adapter-authenticated metadata vocabulary and effective
|
|
286
|
+
* mode: an arbitrary ACP process cannot gain this path by copying `_meta` alone.
|
|
287
|
+
* Exact option ids/kinds bind recognition to the protected-MCP shape and keep
|
|
288
|
+
* malformed requests on the ordinary fail-closed path.
|
|
289
|
+
*/
|
|
290
|
+
private isEffectiveCodexProtectedMcpApproval;
|
|
188
291
|
private recordUpdate;
|
|
189
292
|
/**
|
|
190
293
|
* Codex ACP's phase extension is the only currently supported visibility
|
|
@@ -199,5 +302,6 @@ export declare class AcpSession implements SessionHandle {
|
|
|
199
302
|
}): ConversationHandlePage;
|
|
200
303
|
conversationSnapshot(): ConversationSnapshot;
|
|
201
304
|
subscribeConversation(listener: Parameters<ConversationEventStore['subscribe']>[0]): () => void;
|
|
305
|
+
private isCurrentConversationEvent;
|
|
202
306
|
private fail;
|
|
203
307
|
}
|
package/dist/session/acp.js
CHANGED
|
@@ -12,10 +12,25 @@ const CANCEL_SETTLE_GRACE_MS = 15_000;
|
|
|
12
12
|
const CANCEL_TERMINATE_GRACE_MS = 5_000;
|
|
13
13
|
/** A permission no human answered is eventually a decision nobody made. */
|
|
14
14
|
const PERMISSION_TIMEOUT_MS = 10 * 60_000;
|
|
15
|
-
/**
|
|
15
|
+
/** Wait 10–15 seconds before a vanished controller triggers the unattended policy. */
|
|
16
16
|
const CONTROLLER_GRACE_MS = 12_000;
|
|
17
17
|
/** Bound safe-boundary waiting without turning a hung tool into cancellation. */
|
|
18
18
|
export const AFTER_TOOL_BOUNDARY_TIMEOUT_MS = 120_000;
|
|
19
|
+
/**
|
|
20
|
+
* How long a steering-started turn is presumed to still own the adapter after
|
|
21
|
+
* its last update. Such a turn has no prompt id, so it never reports a
|
|
22
|
+
* stopReason and there is no exact end to observe — silence is the only signal
|
|
23
|
+
* available, and this is the bound that turns it into a decision.
|
|
24
|
+
*
|
|
25
|
+
* Sized from the fleet's own scheduled-run history: across 1513 completed
|
|
26
|
+
* scheduled runs the longest silence WITHIN a working turn was 120.2 s (p99
|
|
27
|
+
* 41.0 s; 5 runs above 60 s). A shorter grace would release the lease while the
|
|
28
|
+
* adapter is still working and re-admit a prompt into a busy turn, which is the
|
|
29
|
+
* FLEET-003 failure itself. The costs are deliberately asymmetric: holding too
|
|
30
|
+
* long skips one best-effort maintenance tick, releasing too early SIGTERMs a
|
|
31
|
+
* live role.
|
|
32
|
+
*/
|
|
33
|
+
export const STEERING_OCCUPANCY_IDLE_MS = 150_000;
|
|
19
34
|
const TERMINAL_TOOL_STATUSES = new Set(['completed', 'failed']);
|
|
20
35
|
const SCHEDULED_LOOP_REDACTION = '[scheduled-loop content redacted]';
|
|
21
36
|
const OWNER_COMMENTARY_REDACTION = '[assistant commentary redacted]';
|
|
@@ -136,7 +151,7 @@ function canonicallyWithin(root, candidates) {
|
|
|
136
151
|
* Map typed prompt provenance to the conversation ledger's source vocabulary.
|
|
137
152
|
* Only operator-authored local sources may persist prompt bodies; external
|
|
138
153
|
* E2E bodies (owner channel, monitor wakes) and scheduled-loop content are
|
|
139
|
-
* recorded as digest/size placeholders
|
|
154
|
+
* recorded as digest/size placeholders.
|
|
140
155
|
*/
|
|
141
156
|
function conversationSource(origin) {
|
|
142
157
|
switch (origin?.kind) {
|
|
@@ -193,6 +208,8 @@ export class AcpSession {
|
|
|
193
208
|
child;
|
|
194
209
|
events;
|
|
195
210
|
conversation;
|
|
211
|
+
/** Cursor before this runner generation began; older durable events stay off the live console. */
|
|
212
|
+
conversationStartCursor;
|
|
196
213
|
/** New on every runner start; permission/turn IDs from prior generations are stale. */
|
|
197
214
|
sessionGeneration = randomUUID();
|
|
198
215
|
/** True while `session/load` replays history as ordinary updates. */
|
|
@@ -202,6 +219,12 @@ export class AcpSession {
|
|
|
202
219
|
connection;
|
|
203
220
|
sessionId;
|
|
204
221
|
readiness = 'starting';
|
|
222
|
+
/**
|
|
223
|
+
* Last non-replayed session update from the agent. `readiness` cannot answer
|
|
224
|
+
* "is this agent working" for a steered turn (FLEET-002), and this is the
|
|
225
|
+
* evidence that can.
|
|
226
|
+
*/
|
|
227
|
+
lastUpdateAt;
|
|
205
228
|
lastError;
|
|
206
229
|
promptTail = Promise.resolve();
|
|
207
230
|
queueDepth = 0;
|
|
@@ -217,6 +240,13 @@ export class AcpSession {
|
|
|
217
240
|
cancelEscalation;
|
|
218
241
|
cancelForceKill;
|
|
219
242
|
cancelRecoveryReason;
|
|
243
|
+
/**
|
|
244
|
+
* Held while a steering-started turn is believed to own the adapter. It is a
|
|
245
|
+
* lease, not a latch: `steeringRelease` always fires, so the role can never be
|
|
246
|
+
* stranded busy by a wake whose turn ended without telling anyone.
|
|
247
|
+
*/
|
|
248
|
+
steeringOccupied = false;
|
|
249
|
+
steeringRelease;
|
|
220
250
|
/**
|
|
221
251
|
* Rejects the moment the adapter process is gone. Every in-flight ACP request
|
|
222
252
|
* races it, so a dead adapter can never leave a turn — and therefore a
|
|
@@ -237,6 +267,7 @@ export class AcpSession {
|
|
|
237
267
|
this.conversation = new ConversationEventStore(join(options.stateDir, '.conversation'), {
|
|
238
268
|
roleId: options.name, log: line => options.log(`[${options.name}] ${line}`),
|
|
239
269
|
});
|
|
270
|
+
this.conversationStartCursor = this.conversation.lastCursor();
|
|
240
271
|
this.sessionFile = join(options.stateDir, '.acp-session-id');
|
|
241
272
|
this.terminated = new Promise((_resolve, reject) => { this.terminate = reject; });
|
|
242
273
|
// Nothing awaits this promise until a request races it; an unobserved
|
|
@@ -247,6 +278,7 @@ export class AcpSession {
|
|
|
247
278
|
if (this.cancelForceKill)
|
|
248
279
|
clearTimeout(this.cancelForceKill);
|
|
249
280
|
this.cancelForceKill = undefined;
|
|
281
|
+
this.releaseSteeringOccupancy('adapter exited');
|
|
250
282
|
// Record the child's real exit code/signal. The tmux path can only see a
|
|
251
283
|
// shell's `$?`; here the truth is available, so keep it.
|
|
252
284
|
const classified = classifyChildExit(code, signal);
|
|
@@ -306,7 +338,7 @@ export class AcpSession {
|
|
|
306
338
|
}
|
|
307
339
|
}
|
|
308
340
|
/**
|
|
309
|
-
* Honest restart recovery
|
|
341
|
+
* Honest restart recovery: a prompt that was admitted but never
|
|
310
342
|
* started is safe to dispatch again; a turn that had already started may
|
|
311
343
|
* have caused side effects, so it is closed as `unknown_after_restart` —
|
|
312
344
|
* never silently replayed.
|
|
@@ -347,17 +379,60 @@ export class AcpSession {
|
|
|
347
379
|
// terminal fact (signal exits deliberately leave exitCode null).
|
|
348
380
|
return this.child.exitCode === null && (this.child.signalCode ?? null) === null;
|
|
349
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* Take the occupancy lease for a turn the adapter started on its own behalf.
|
|
384
|
+
* Refreshed by every adapter update, so it tracks work actually happening
|
|
385
|
+
* rather than a fixed guess at how long a wake takes.
|
|
386
|
+
*/
|
|
387
|
+
holdSteeringOccupancy() {
|
|
388
|
+
if (this.closing || !this.isAlive())
|
|
389
|
+
return;
|
|
390
|
+
this.steeringOccupied = true;
|
|
391
|
+
this.refreshSteeringOccupancy();
|
|
392
|
+
}
|
|
393
|
+
refreshSteeringOccupancy() {
|
|
394
|
+
if (!this.steeringOccupied)
|
|
395
|
+
return;
|
|
396
|
+
if (this.steeringRelease)
|
|
397
|
+
clearTimeout(this.steeringRelease);
|
|
398
|
+
this.steeringRelease = setTimeout(() => this.releaseSteeringOccupancy('adapter silent'), this.options.steeringOccupancyIdleMs ?? STEERING_OCCUPANCY_IDLE_MS);
|
|
399
|
+
this.steeringRelease.unref?.();
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Every exit from occupancy comes through here, including the ones that are
|
|
403
|
+
* not the timer: a real turn boundary, close, and adapter exit. A lease that
|
|
404
|
+
* can leak is worse than the bug it fixes — it would leave the role reporting
|
|
405
|
+
* `running` forever and starve scheduled admission permanently.
|
|
406
|
+
*/
|
|
407
|
+
releaseSteeringOccupancy(reason) {
|
|
408
|
+
if (this.steeringRelease)
|
|
409
|
+
clearTimeout(this.steeringRelease);
|
|
410
|
+
this.steeringRelease = undefined;
|
|
411
|
+
if (!this.steeringOccupied)
|
|
412
|
+
return;
|
|
413
|
+
this.steeringOccupied = false;
|
|
414
|
+
this.options.log(`[${this.options.name}] steering-started turn no longer holds the adapter (${reason})`);
|
|
415
|
+
}
|
|
350
416
|
snapshot() {
|
|
351
417
|
return {
|
|
352
418
|
backend: 'acp',
|
|
353
419
|
alive: this.isAlive(),
|
|
354
|
-
|
|
420
|
+
// A steering-started turn is real work with no prompt id. Reporting the
|
|
421
|
+
// session idle while it runs is what let the arbiter admit a scheduled
|
|
422
|
+
// prompt into a busy adapter, whose `session/prompt` then never returned
|
|
423
|
+
// a stopReason and ended in a cancellation deadline and a SIGTERM.
|
|
424
|
+
readiness: this.readiness === 'idle' && this.steeringOccupied
|
|
425
|
+
? 'running' : this.readiness,
|
|
355
426
|
sessionId: this.sessionId,
|
|
356
427
|
lastError: this.lastError,
|
|
357
428
|
pendingPermissionId: this.pendingPermissions.keys().next().value,
|
|
358
429
|
runtimeModel: this.runtimeModel,
|
|
359
430
|
reasoningEffort: this.reasoningEffort,
|
|
360
431
|
permissionMode: this.options.permissionMode,
|
|
432
|
+
activity: {
|
|
433
|
+
activeToolCalls: this.activeToolCalls.size,
|
|
434
|
+
...(this.lastUpdateAt ? { lastUpdateAt: this.lastUpdateAt } : {}),
|
|
435
|
+
},
|
|
361
436
|
};
|
|
362
437
|
}
|
|
363
438
|
toolCall(toolCallId) {
|
|
@@ -526,15 +601,19 @@ export class AcpSession {
|
|
|
526
601
|
throw new SessionControlError('control-unavailable', 'ACP adapter restart is in progress after the cancellation deadline', ACP_CANCEL_DEADLINE_EXCEEDED);
|
|
527
602
|
if (this.closing || !this.sessionId || !this.isAlive())
|
|
528
603
|
throw new SessionControlError('offline', this.lastError ?? 'ACP session is offline');
|
|
529
|
-
|
|
530
|
-
await this.
|
|
604
|
+
const delivery = options.interrupt
|
|
605
|
+
? await this.prepareInterruptingDelivery(options.interruptSource ?? 'local-console')
|
|
606
|
+
: undefined;
|
|
531
607
|
// Interrupting delivery must still use steering when supported. With no
|
|
532
608
|
// live turn, the extension starts one and acknowledges `startedNewTurn`
|
|
533
609
|
// immediately; a normal session/prompt would keep the monitor blocked until
|
|
534
610
|
// the entire wake-triggered turn terminated.
|
|
535
611
|
if (options.steer && this.steeringSupported) {
|
|
536
612
|
const promptId = randomUUID();
|
|
537
|
-
return {
|
|
613
|
+
return {
|
|
614
|
+
promptId, queuedBehind: 0, completion: this.steerPrompt(text), origin: options.origin,
|
|
615
|
+
...(delivery ? { delivery } : {}),
|
|
616
|
+
};
|
|
538
617
|
}
|
|
539
618
|
const promptId = randomUUID();
|
|
540
619
|
const queuedBehind = this.queueDepth;
|
|
@@ -546,13 +625,52 @@ export class AcpSession {
|
|
|
546
625
|
this.queueDepth = Math.max(0, this.queueDepth - 1);
|
|
547
626
|
return turnResult(false, 'failed', error?.message ?? String(error));
|
|
548
627
|
});
|
|
549
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
promptId, queuedBehind, completion, origin: options.origin,
|
|
630
|
+
delivery: delivery ?? (queuedBehind > 0 ? 'queued' : 'started'),
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Prepare the session for a prompt that asked to pre-empt current work.
|
|
635
|
+
*
|
|
636
|
+
* The old behaviour was one unconditional `session/cancel` notification
|
|
637
|
+
* followed immediately by `session/prompt`. That is what produced the owner's
|
|
638
|
+
* "request failed before completion":
|
|
639
|
+
*
|
|
640
|
+
* - `cancelActive` only awaits settlement when `this.activeTurn` is set, and
|
|
641
|
+
* a turn the ADAPTER started (steering's `startedNewTurn`) is never tracked
|
|
642
|
+
* here. So the cancel raced the adapter's own transcript repair and the new
|
|
643
|
+
* prompt landed while the last assistant message still held an unresolved
|
|
644
|
+
* `tool_use` — rejected with `stop_reason=tool_use`.
|
|
645
|
+
* - With nothing running at all, it still sent the cancel, and the prompt
|
|
646
|
+
* landed on a bare interrupted user message — rejected with
|
|
647
|
+
* `stop_reason=null`.
|
|
648
|
+
*
|
|
649
|
+
* So: never cancel across a tool boundary, and never cancel something whose
|
|
650
|
+
* settlement cannot be awaited. Everything else is queued, which the ACP queue
|
|
651
|
+
* already does correctly. The returned state is what the caller may claim to a
|
|
652
|
+
* human — `interrupted` only when a turn really was cancelled.
|
|
653
|
+
*/
|
|
654
|
+
async prepareInterruptingDelivery(source) {
|
|
655
|
+
if (!this.sessionId)
|
|
656
|
+
return 'started';
|
|
657
|
+
// No fleet-tracked turn to await. Either the session is idle — cancelling it
|
|
658
|
+
// corrupts the transcript for no gain — or the adapter is running a turn
|
|
659
|
+
// fleet never started, whose settlement nothing here can wait for. Queue in
|
|
660
|
+
// both cases: the ACP queue already orders this correctly.
|
|
661
|
+
if (!this.activeTurn)
|
|
662
|
+
return this.activeToolCalls.size > 0 ? 'deferred' : 'started';
|
|
663
|
+
// A tracked turn IS safe to cancel: cancelActive settles pending permissions
|
|
664
|
+
// and awaits the turn's own settlement before this returns, so the prompt
|
|
665
|
+
// below cannot race the adapter's transcript repair.
|
|
666
|
+
await this.cancelActive(source);
|
|
667
|
+
return 'interrupted';
|
|
550
668
|
}
|
|
551
669
|
/**
|
|
552
670
|
* Durably record a prompt admission BEFORE acceptance is returned. Browser
|
|
553
671
|
* admissions are transactional — a prompt the ledger cannot hold is refused,
|
|
554
672
|
* because an acknowledged-then-lost prompt is worse than an error. Every
|
|
555
|
-
* other source degrades to best-effort so the agent keeps working
|
|
673
|
+
* other source degrades to best-effort so the agent keeps working.
|
|
556
674
|
*/
|
|
557
675
|
admitToLedger(promptId, text, queuedBehind, options) {
|
|
558
676
|
const { source, persistBody } = conversationSource(options.origin);
|
|
@@ -855,6 +973,7 @@ export class AcpSession {
|
|
|
855
973
|
if (this.controllerGrace)
|
|
856
974
|
clearTimeout(this.controllerGrace);
|
|
857
975
|
this.controllerGrace = undefined;
|
|
976
|
+
this.releaseSteeringOccupancy('session closed');
|
|
858
977
|
for (const [permissionId, pending] of [...this.pendingPermissions])
|
|
859
978
|
this.settlePendingAutomatically(permissionId, pending, 'cancelled', undefined, 'the session closed while this request was pending');
|
|
860
979
|
this.releaseAllTools();
|
|
@@ -872,6 +991,17 @@ export class AcpSession {
|
|
|
872
991
|
});
|
|
873
992
|
this.conversation.close();
|
|
874
993
|
}
|
|
994
|
+
/**
|
|
995
|
+
* The role's declared MCP servers, or `[]`.
|
|
996
|
+
*
|
|
997
|
+
* Sent on resume and load as well as on new: the agent builds its server set
|
|
998
|
+
* once per session, so a resumed session that omitted them would come back
|
|
999
|
+
* without the tools the role's config declares — which is exactly the shape of
|
|
1000
|
+
* silent drop this plumbing exists to end.
|
|
1001
|
+
*/
|
|
1002
|
+
declaredMcpServers() {
|
|
1003
|
+
return this.options.mcpServers ?? [];
|
|
1004
|
+
}
|
|
875
1005
|
async initialize() {
|
|
876
1006
|
const initialized = await this.connection.agent.request(acp.methods.agent.initialize, {
|
|
877
1007
|
protocolVersion: acp.PROTOCOL_VERSION,
|
|
@@ -891,7 +1021,7 @@ export class AcpSession {
|
|
|
891
1021
|
const resumed = await this.connection.agent.request(acp.methods.agent.session.resume, {
|
|
892
1022
|
sessionId: persisted,
|
|
893
1023
|
cwd: this.options.cwd,
|
|
894
|
-
mcpServers:
|
|
1024
|
+
mcpServers: this.declaredMcpServers(),
|
|
895
1025
|
});
|
|
896
1026
|
this.captureRuntimeMetadata(resumed.configOptions);
|
|
897
1027
|
this.sessionId = persisted;
|
|
@@ -904,7 +1034,7 @@ export class AcpSession {
|
|
|
904
1034
|
const loaded = await this.connection.agent.request(acp.methods.agent.session.load, {
|
|
905
1035
|
sessionId: persisted,
|
|
906
1036
|
cwd: this.options.cwd,
|
|
907
|
-
mcpServers:
|
|
1037
|
+
mcpServers: this.declaredMcpServers(),
|
|
908
1038
|
});
|
|
909
1039
|
this.captureRuntimeMetadata(loaded.configOptions);
|
|
910
1040
|
}
|
|
@@ -916,7 +1046,8 @@ export class AcpSession {
|
|
|
916
1046
|
else {
|
|
917
1047
|
const created = await this.connection.agent.request(acp.methods.agent.session.new, {
|
|
918
1048
|
cwd: this.options.cwd,
|
|
919
|
-
mcpServers:
|
|
1049
|
+
mcpServers: this.declaredMcpServers(),
|
|
1050
|
+
...(this.options.sessionMeta ? { _meta: this.options.sessionMeta } : {}),
|
|
920
1051
|
});
|
|
921
1052
|
this.sessionId = created.sessionId;
|
|
922
1053
|
this.captureRuntimeMetadata(created.configOptions);
|
|
@@ -1013,6 +1144,10 @@ export class AcpSession {
|
|
|
1013
1144
|
}
|
|
1014
1145
|
finally {
|
|
1015
1146
|
this.releaseAllTools();
|
|
1147
|
+
// A turn this client owned has ended, so the adapter has reported a
|
|
1148
|
+
// boundary: whatever a steering call started before it is over too. This
|
|
1149
|
+
// is the release path that does not depend on the silence timer.
|
|
1150
|
+
this.releaseSteeringOccupancy('turn boundary');
|
|
1016
1151
|
if (this.activeTurn?.id === turnId) {
|
|
1017
1152
|
this.activeTurn.settle();
|
|
1018
1153
|
if (this.cancelEscalation)
|
|
@@ -1035,6 +1170,12 @@ export class AcpSession {
|
|
|
1035
1170
|
]);
|
|
1036
1171
|
if (response.outcome === 'failed')
|
|
1037
1172
|
return turnResult(false, 'failed', 'ACP steering failed');
|
|
1173
|
+
// `injected` joined a turn this client already owns and will settle.
|
|
1174
|
+
// `startedNewTurn` created one nobody owns: the adapter is working and
|
|
1175
|
+
// will never answer for it, so admission has to learn about it here or
|
|
1176
|
+
// not at all.
|
|
1177
|
+
if (response.outcome === 'startedNewTurn')
|
|
1178
|
+
this.holdSteeringOccupancy();
|
|
1038
1179
|
return turnResult(true, 'inconclusive', response.outcome);
|
|
1039
1180
|
}
|
|
1040
1181
|
catch (error) {
|
|
@@ -1062,6 +1203,17 @@ export class AcpSession {
|
|
|
1062
1203
|
// Permission is part of the tool lifecycle. Reserve before any policy or
|
|
1063
1204
|
// human decision so a monitor wake cannot slip between request and answer.
|
|
1064
1205
|
this.reservePermission(toolCallId, permissionId);
|
|
1206
|
+
if (this.isEffectiveCodexProtectedMcpApproval(params)) {
|
|
1207
|
+
// Protected MCP approval is already the tool's narrow gate. Never turn
|
|
1208
|
+
// this one decision into an adapter-wide standing grant.
|
|
1209
|
+
const option = choose(['allow_once']);
|
|
1210
|
+
const response = this.settleAutomatically(params, option, 'allowed', 'permissionMode.fleetMode=allow', 'the trusted Codex adapter authenticated a protected MCP approval request');
|
|
1211
|
+
if (option)
|
|
1212
|
+
this.allowPermission(toolCallId, permissionId);
|
|
1213
|
+
else
|
|
1214
|
+
this.releasePermission(toolCallId, permissionId);
|
|
1215
|
+
return Promise.resolve(response);
|
|
1216
|
+
}
|
|
1065
1217
|
if (this.options.permissions.approval === 'allow' && this.withinAutomaticBoundary(params)) {
|
|
1066
1218
|
const option = choose(['allow_always', 'allow_once']);
|
|
1067
1219
|
const response = this.settleAutomatically(params, option, 'allowed', 'permissions.approval=allow', `the request is inside the ${this.options.permissions.filesystem} boundary`);
|
|
@@ -1188,7 +1340,34 @@ export class AcpSession {
|
|
|
1188
1340
|
const cwd = resolve(this.options.cwd);
|
|
1189
1341
|
return canonicallyWithin(cwd, locations.map(location => resolve(location.path)));
|
|
1190
1342
|
}
|
|
1343
|
+
/**
|
|
1344
|
+
* Codex ACP 1.1.7 marks its protected MCP elicitation bridge on a locationless
|
|
1345
|
+
* execute request. The marker is meaningful only together with the runner's
|
|
1346
|
+
* independently supplied, adapter-authenticated metadata vocabulary and effective
|
|
1347
|
+
* mode: an arbitrary ACP process cannot gain this path by copying `_meta` alone.
|
|
1348
|
+
* Exact option ids/kinds bind recognition to the protected-MCP shape and keep
|
|
1349
|
+
* malformed requests on the ordinary fail-closed path.
|
|
1350
|
+
*/
|
|
1351
|
+
isEffectiveCodexProtectedMcpApproval(params) {
|
|
1352
|
+
const locations = params.toolCall.locations ?? [];
|
|
1353
|
+
return this.options.permissionMetadataSource === 'codex-acp'
|
|
1354
|
+
&& this.options.permissionMode?.fleetMode === 'allow'
|
|
1355
|
+
&& params.toolCall.kind === 'execute'
|
|
1356
|
+
&& params.toolCall.status === 'pending'
|
|
1357
|
+
&& locations.length === 0
|
|
1358
|
+
&& params._meta?.is_mcp_tool_approval === true
|
|
1359
|
+
&& params.options.some(option => option.optionId === 'allow_once' && option.kind === 'allow_once')
|
|
1360
|
+
&& params.options.some(option => option.optionId === 'decline' && option.kind === 'reject_once');
|
|
1361
|
+
}
|
|
1191
1362
|
recordUpdate(update) {
|
|
1363
|
+
// Replayed history is not current activity: `session/load` would otherwise
|
|
1364
|
+
// make a cold session look like it had just been working. The same reason
|
|
1365
|
+
// keeps it from extending the steering lease, which is evidence the adapter
|
|
1366
|
+
// is working right now — for a steering-started turn, the only evidence.
|
|
1367
|
+
if (!this.replaying) {
|
|
1368
|
+
this.lastUpdateAt = new Date().toISOString();
|
|
1369
|
+
this.refreshSteeringOccupancy();
|
|
1370
|
+
}
|
|
1192
1371
|
const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
|
|
1193
1372
|
const messagePhase = update.sessionUpdate === 'agent_message_chunk'
|
|
1194
1373
|
? this.codexMessagePhase(update) : undefined;
|
|
@@ -1288,7 +1467,25 @@ export class AcpSession {
|
|
|
1288
1467
|
}
|
|
1289
1468
|
// ── conversation ledger access (SessionHandle) ─────────────────────────────
|
|
1290
1469
|
conversationPage(request = {}) {
|
|
1291
|
-
|
|
1470
|
+
const floor = Number(this.conversationStartCursor ?? 0);
|
|
1471
|
+
const requested = Number(request.after ?? 0);
|
|
1472
|
+
let after = String(Math.max(Number.isSafeInteger(floor) ? floor : 0, Number.isSafeInteger(requested) ? requested : 0));
|
|
1473
|
+
const limit = Math.min(Math.max(request.limit ?? 200, 1), 1_000);
|
|
1474
|
+
let page = this.conversation.page({ after, limit });
|
|
1475
|
+
let visible = page.events.filter(event => this.isCurrentConversationEvent(event));
|
|
1476
|
+
// A resumed adapter may replay a page made entirely of prior session/load
|
|
1477
|
+
// history. Advance over it without exposing it or making the browser stop
|
|
1478
|
+
// before later current-session records.
|
|
1479
|
+
while (!visible.length && page.hasMore && page.nextCursor && page.nextCursor !== after) {
|
|
1480
|
+
after = page.nextCursor;
|
|
1481
|
+
page = this.conversation.page({ after, limit });
|
|
1482
|
+
visible = page.events.filter(event => this.isCurrentConversationEvent(event));
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
...page,
|
|
1486
|
+
events: visible,
|
|
1487
|
+
snapshot: this.conversationSnapshot(),
|
|
1488
|
+
};
|
|
1292
1489
|
}
|
|
1293
1490
|
conversationSnapshot() {
|
|
1294
1491
|
return {
|
|
@@ -1300,7 +1497,13 @@ export class AcpSession {
|
|
|
1300
1497
|
};
|
|
1301
1498
|
}
|
|
1302
1499
|
subscribeConversation(listener) {
|
|
1303
|
-
return this.conversation.subscribe(
|
|
1500
|
+
return this.conversation.subscribe(event => {
|
|
1501
|
+
if (this.isCurrentConversationEvent(event))
|
|
1502
|
+
listener(event);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
isCurrentConversationEvent(event) {
|
|
1506
|
+
return event.sessionGeneration === this.sessionGeneration && event.source !== 'agent_replay';
|
|
1304
1507
|
}
|
|
1305
1508
|
fail(error) {
|
|
1306
1509
|
this.lastError = error?.message ?? String(error);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { SessionActivity } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* How long after the agent's last session update it still counts as working.
|
|
4
|
+
*
|
|
5
|
+
* FLEET-002: a wake delivered by ACP steering answers `startedNewTurn` and runs
|
|
6
|
+
* an entire turn that fleet never receives a `session/prompt` response for (ACP
|
|
7
|
+
* has no turn-end session update), so `readiness` stays `idle` throughout. Tool
|
|
8
|
+
* reservations and update recency are the only activity evidence fleet holds.
|
|
9
|
+
* The trade-off is deliberate and one-directional: at worst a role reads busy
|
|
10
|
+
* for one window after it genuinely stopped, instead of reading ready — or
|
|
11
|
+
* being classified stalled — while it is executing tools.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ACTIVITY_WINDOW_MS = 60000;
|
|
14
|
+
export type ActivityState = 'active' | 'quiet' | 'unobservable';
|
|
15
|
+
export interface ObservedActivity {
|
|
16
|
+
state: ActivityState;
|
|
17
|
+
activeToolCalls?: number;
|
|
18
|
+
lastUpdateAt?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Classify agent-side activity. `unobservable` is NOT `quiet`: a backend that
|
|
22
|
+
* cannot see the agent (tmux) has no evidence, and no evidence must never be
|
|
23
|
+
* reported as "doing nothing".
|
|
24
|
+
*/
|
|
25
|
+
export declare function classifyActivity(activity: SessionActivity | undefined, now?: number): ObservedActivity;
|
|
26
|
+
/**
|
|
27
|
+
* One operator-facing line that never lets turn occupancy pose as liveness:
|
|
28
|
+
* the readiness value is labelled as the turn field it is, and the activity
|
|
29
|
+
* verdict is stated separately with the evidence behind it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function describeSessionState(readiness: string | undefined, activity: SessionActivity | undefined, now?: number): string;
|