operon-agents 0.1.0-alpha.2 → 0.1.0-alpha.21

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/dist/harness.d.ts CHANGED
@@ -11,31 +11,25 @@
11
11
  * low-level "assemble it yourself / stateless server" path, import from
12
12
  * `operon-agents/core` instead.
13
13
  */
14
- import { type AgentEvent, type EventPublicationMode, type AgentEventListener, type AgentInput, SessionProjection, type SessionSnapshot, type SnapshotOptions, type DirectoryEntry, type ProjectionObservation, type ProjectionListener, type ApprovalRequest, type ApprovalResponse, type Capability, type InterruptAnswer, type ChatModel, ListenerSink, type PermissionManagerOptions, type PermissionMode, type PendingRunInterrupt, type QuestionRequest, type QuestionResult, type Responder, type RunHandle, type RunResult, type SessionStore, type SessionHandle, type SessionSummary, type ListSessionsFilter, type OpenSessionOptions, type DeleteSessionOptions, type ThinkingLevel, type Tool, type Logger, type ExternalOriginMetadataValue, type SteerChannel, type SteerOrigin, Agent, Runner, Session, McpOAuthService, type HookDef, type McpServerConfig, type PluginManager, type ApprovalRequestOptions, type Machine, type MachineFactory, type SessionRepository, type ResolvedAgentProfile, type SubagentProvider, type ModelRuntime } from "operon-agents-core";
14
+ import { type AgentEvent, type EventPublicationMode, type TelemetryService, type AgentEventListener, type AgentInput, SessionProjection, type SessionSnapshot, type SnapshotOptions, type DirectoryEntry, type ProjectionObservation, type ProjectionListener, type ApprovalRequest, type ApprovalResponse, type Capability, type InterruptAnswer, type ChatModel, ListenerSink, type PermissionManagerOptions, type PermissionMode, type PendingRunInterrupt, type QuestionRequest, type QuestionResult, type Responder, type RunHandle, type RunResult, type SessionStore, type SessionHandle, type SessionSummary, type ListSessionsFilter, type OpenSessionOptions, type DeleteSessionOptions, type ThinkingLevel, type Tool, type SteerMessage, type SteerOptions, type SteerOrigin, type SteerReceipt, Agent, Runner, Session, McpOAuthService, SkillRegistry, type HookDef, type McpServerConfig, type McpTransportKind, type McpCredentialProvider, type PluginManager, type ApprovalRequestOptions, type Environment, type EnvironmentFactory, type SessionRepository, type ResolvedAgentProfile, type SubagentProvider } from "operon-agents-core";
15
+ import type { SkillsService, CompactionService, BackgroundManager, McpServersHandle, GoalStore, PlanMode, Logger, ModelRuntime, TracingProcessor } from "operon-agents-core";
16
+ import { type WorkspaceMcpPool } from "./workspace-mcp.ts";
15
17
  import { HarnessExtensionManager, ServiceRegistry, type ExtensionDefinition, type ServiceOptions } from "./extensions/index.ts";
16
- import { type CommandResult } from "operon-agents-core";
18
+ import { type CommandResult, type TodoItem } from "operon-agents-core";
17
19
  export type ApprovalHandler = (request: ApprovalRequest, options?: ApprovalRequestOptions) => Promise<ApprovalResponse> | ApprovalResponse;
18
20
  export type QuestionHandler = (request: QuestionRequest, options?: {
19
21
  readonly signal?: AbortSignal;
20
22
  }) => Promise<QuestionResult> | QuestionResult;
21
23
  export type HarnessSessionState = "idle" | "running" | "interrupted" | "closed";
22
- export type DeliveryMode = "auto" | "steer" | "follow_up";
23
- export interface DeliveryOptions {
24
- readonly source: string;
25
- readonly actor?: string;
26
- readonly metadata?: Readonly<Record<string, ExternalOriginMetadataValue>>;
27
- /** auto: steer a running turn, or start an idle one. */
28
- readonly mode?: DeliveryMode;
29
- }
30
- export interface DeliveryReceipt {
31
- readonly deliveryId: string;
32
- readonly sessionId: string;
33
- readonly acceptedAt: number;
34
- readonly status: "started" | "queued";
35
- readonly channel: "turn" | SteerChannel;
36
- readonly steerId?: string;
37
- /** Present when delivery started an idle session. */
38
- readonly completion?: Promise<RunResult>;
24
+ /** Test-only (exported via `operon-agents/internal`): shrink the close deadlines. */
25
+ export declare function setHarnessCloseTimeoutsForTest(ms: {
26
+ runSettle?: number;
27
+ }): void;
28
+ /** A slash command as listed by `HarnessSession.listCommands`. */
29
+ export interface CommandInfo {
30
+ readonly name: string;
31
+ readonly aliases: readonly string[];
32
+ readonly description: string;
39
33
  }
40
34
  export interface HarnessSessionStatus {
41
35
  readonly state: HarnessSessionState;
@@ -43,10 +37,43 @@ export interface HarnessSessionStatus {
43
37
  readonly hasQueuedMessages: boolean;
44
38
  }
45
39
  export interface DefaultCapabilitiesOptions {
40
+ /**
41
+ * The harness's process-shared parts. When it carries a set of MCP connections, a skill scan or
42
+ * an OAuth service, the bundle uses THOSE instead of building per-session ones. Without it
43
+ * everything here is built per session.
44
+ */
45
+ readonly shared?: HarnessParts;
46
+ /**
47
+ * The session brought its own environment (`createSession({ environment })`). The shared
48
+ * `skillRegistry` was scanned through the HARNESS's environment and does not describe this one,
49
+ * so the skill scan runs per session through the session's environment instead — the catalog
50
+ * follows the filesystem the session's tools actually operate. Default false.
51
+ */
52
+ readonly ownEnvironment?: boolean;
46
53
  /** Context window budget used to size compaction. Defaults to 200_000. */
47
54
  readonly maxContextTokens?: number;
48
- /** Workspace MCP servers. When set (or with `pluginManager`), an MCP capability is included. */
55
+ /** Harness-level MCP servers. When set (or with `pluginManager`), an MCP capability is included. */
49
56
  readonly mcpServers?: Record<string, McpServerConfig>;
57
+ /**
58
+ * MCP servers private to the session being built — `SessionCapabilityContext.mcpServers`, i.e.
59
+ * what the caller passed to `createSession({ mcpServers })`. They are layered OVER the
60
+ * harness's shared servers (`HarnessParts.mcpServers`) and over `mcpServers` above: same name, the
61
+ * session's wins for this session only. Built, connected and shut down with the session.
62
+ */
63
+ readonly sessionMcpServers?: Record<string, McpServerConfig>;
64
+ /**
65
+ * Share MCP connections per DIRECTORY instead of building them per session: every server this
66
+ * bundle would otherwise connect for the session — `mcpServers`, the plugins', and `servers`
67
+ * here (the directory's own, e.g. its `mcp.json`) — is borrowed from `pool` under `workDir`, so
68
+ * the sessions open in one directory run one set. `sessionMcpServers` stay the session's own,
69
+ * layered over it. Ignored when `shared.mcpServers` is set (a process-wide set already covers
70
+ * every directory) and for a session on its own environment. See {@link WorkspaceMcpPool}.
71
+ */
72
+ readonly workspaceMcp?: {
73
+ readonly pool: WorkspaceMcpPool;
74
+ readonly workDir: string;
75
+ readonly servers?: Record<string, McpServerConfig>;
76
+ };
50
77
  /**
51
78
  * Installed-plugin manager. When set, the framework self-drives the plugin's contributions into
52
79
  * this capability set: its skills (merged into the skills capability), its MCP servers (merged
@@ -67,6 +94,30 @@ export interface DefaultCapabilitiesOptions {
67
94
  * server. Only consulted when an MCP capability is built.
68
95
  */
69
96
  readonly oauthService?: McpOAuthService;
97
+ /**
98
+ * MCP transports this host will run. Omitted means both, which is right for a local host.
99
+ *
100
+ * A SERVER host should pass `["http"]`. An stdio server is a child process of the harness
101
+ * process, so it neither follows the session's environment into a sandbox nor survives the
102
+ * replica model a server is deployed under — and `transport` defaults to `"stdio"`, so
103
+ * leaving this unset means a config that merely forgot the field spawns a process on the
104
+ * server. `McpTransportNotPermittedError` names the offending server at startup.
105
+ *
106
+ * The same reasoning as Invariant 7 (cron is a local-only capability): what a host is willing
107
+ * to run is the host's statement, not the kernel's.
108
+ */
109
+ readonly allowedMcpTransports?: readonly McpTransportKind[];
110
+ /** Credentials the host hands out per connect for this session's http MCP servers. See
111
+ * `McpCredentialProvider`. */
112
+ readonly mcpCredentials?: McpCredentialProvider;
113
+ /**
114
+ * The session belongs to one tenant among many (a managed host's default): nothing the process
115
+ * shares reaches it — no process-wide MCP set, skill scan, OAuth service or directory pool —
116
+ * MCP runs over http only, and no server may read its token from the process environment.
117
+ * Each is refused rather than skipped where skipping would hide a misconfiguration. See
118
+ * docs/server-profile.md.
119
+ */
120
+ readonly strictIsolation?: boolean;
70
121
  }
71
122
  /**
72
123
  * The standard coding-agent capability bundle (goal / plan / todo / skills /
@@ -74,24 +125,62 @@ export interface DefaultCapabilitiesOptions {
74
125
  * on the agent (`filesystemTools()`), not a capability; so is AskUserQuestion
75
126
  * (`askUserQuestionTool`, wired through ToolRunContext.responder).
76
127
  *
77
- * With `mcpServers` and/or `pluginManager`, an MCP capability is added; with `pluginManager`, the
128
+ * With `mcpServers`, `sessionMcpServers` and/or `pluginManager`, an MCP capability is added; with `pluginManager`, the
78
129
  * plugin's skills/MCP/session-start are wired in too. Intended to be called fresh per session (via
79
130
  * a harness capability factory) so per-session state (goal/plan/todo/…) and the plugin set are
80
131
  * isolated per session, mirroring a fresh-Session-per-create model.
81
132
  */
82
133
  export declare function defaultCapabilities(options?: DefaultCapabilitiesOptions): Capability[];
134
+ /**
135
+ * The process-lived objects a harness is built on — what `HarnessOptions.harness` returns.
136
+ * Each one is optional; what is absent simply has no provider, and the harness falls back to
137
+ * its own default (an in-memory repository, a silent logger, a `LocalEnvironment` per session).
138
+ */
139
+ export interface HarnessParts {
140
+ readonly logger?: Logger;
141
+ readonly sessionRepository?: SessionRepository;
142
+ readonly modelRuntime?: ModelRuntime;
143
+ /** The default environment for sessions that bring none: an instance shared by all of them, or
144
+ * a factory called once per session. */
145
+ readonly environment?: Environment | EnvironmentFactory;
146
+ readonly tracing?: TracingProcessor;
147
+ readonly telemetry?: TelemetryService;
148
+ readonly pluginManager?: PluginManager;
149
+ readonly eventPublication?: EventPublicationMode;
150
+ /** One set of MCP connections for every session in this process. */
151
+ readonly mcpServers?: McpServersHandle;
152
+ /** One skill scan for every session in this process. */
153
+ readonly skillRegistry?: SkillRegistry;
154
+ readonly oauthService?: McpOAuthService;
155
+ /**
156
+ * Tears down whatever this bag opened — the MCP connections, a pool, a log handle. Called by
157
+ * `harness.close()` after every session is closed. Without it, anything the preset connected
158
+ * outlives the harness that asked for it.
159
+ */
160
+ close?(): void | Promise<void>;
161
+ }
83
162
  export interface HarnessOptions<TContext = unknown> {
84
163
  /** Model for the default agent. Either a constructed `ChatModel` or an id resolved via `resolveModel`. */
85
164
  readonly model: string | ChatModel;
86
165
  /** Resolve string model ids (needed only if `model`/agent models are strings). */
87
166
  readonly resolveModel?: (modelId: string) => ChatModel | Promise<ChatModel>;
88
167
  /**
89
- * The model registry backing `resolveModel`. Only needed to let extensions register or override
90
- * providers at runtime (`ctx.actions.registerProvider`). Harness-global by nature: one runtime
91
- * serves every session, so a registration is visible to all of them. Without it those two
92
- * actions throw and everything else works unchanged.
168
+ * The process-lived parts this harness runs on, built once. Everything a session needs that is
169
+ * NOT per-session lives here: the repository (disk locally, Pg/Redis on a server; in-memory
170
+ * when absent), the logger, the model runtime, the default environment, one set of MCP
171
+ * connections, one skill scan. Runs before the by-value extensions' `harness` halves, so they
172
+ * can consume what it returns.
173
+ *
174
+ * There is no tier between this and a session. A shared object is shared by the WHOLE process
175
+ * or it is built per session — nothing is shared by "the sessions in one directory".
93
176
  */
94
- readonly modelRuntime?: ModelRuntime;
177
+ readonly harness?: () => HarnessParts | Promise<HarnessParts>;
178
+ /**
179
+ * Session-tier composition: called once per session being opened. Return its capabilities.
180
+ * Defaults to `defaultCapabilities()`. Always called fresh per session, so per-session state
181
+ * (goal/plan/todo/background/skills/mcp) is isolated by construction.
182
+ */
183
+ readonly session?: (ctx: SessionCapabilityContext) => readonly Capability[] | Promise<readonly Capability[]>;
95
184
  /** Default working directory for new sessions. Defaults to `process.cwd()`. */
96
185
  readonly workDir?: string;
97
186
  /**
@@ -100,15 +189,15 @@ export interface HarnessOptions<TContext = unknown> {
100
189
  * workflow would. Nested spawns are exempt by design (they would deadlock a shared pool).
101
190
  */
102
191
  readonly maxConcurrentSubagents?: number;
103
- /**
104
- * Session repository (the fleet's store + index) — the harness's only storage knob. Injecting one
105
- * is how a deployment plugs in storage: `DiskSessionRepository(homeDir)` for local disk, Pg/Redis
106
- * on a server. Omit for in-memory sessions (`MemorySessionRepository`).
107
- */
108
- readonly repository?: SessionRepository;
109
192
  /** Event durability policy. Defaults to `immediate` for low-latency local applications.
110
193
  * Managed servers override each opened session to `committed`. */
111
194
  readonly eventPublication?: EventPublicationMode;
195
+ /**
196
+ * Product telemetry (docs/telemetry.md). Process-lifetime: create ONE service, attach the
197
+ * appenders once, pass the same instance to every harness. Absent = nothing is counted.
198
+ * The harness never shuts it down; the product does, before exit.
199
+ */
200
+ readonly telemetry?: TelemetryService;
112
201
  /**
113
202
  * Extra subagent profiles merged into the builtin `coder`/`explore`/`plan` fleet. The SOURCE is
114
203
  * the composition root's concern — disk (`loadAgentProfiles`) locally, external on a server —
@@ -128,13 +217,13 @@ export interface HarnessOptions<TContext = unknown> {
128
217
  */
129
218
  readonly context?: TContext;
130
219
  /**
131
- * Process-level shared services, registered into the harness's `ServiceRegistry` at
132
- * construction. An extension consumes one by naming it in `uses` and receiving it as
133
- * `ctx.services[name]` -- a stable handle that resolves the current provider on every call,
134
- * which is what makes a host-side
135
- * `harness.services.replace()` land without touching any session. A plain value registers
136
- * as-is (not replaceable); wrap it as `{ instance, replaceable, dispose }` to opt into hot
137
- * swapping. Design: docs/architecture.md §5.5.
220
+ * Process-level shared services BY NAME, registered into the harness scope at construction
221
+ * (extension services are the one string-keyed corner: an extension is loaded by its id and
222
+ * names what it consumes in `uses`). An extension receives one as `ctx.services[name]` -- a
223
+ * stable handle that resolves the current provider on every call, which is what makes a
224
+ * host-side `harness.services.replace()` land without touching any session. A plain value
225
+ * registers as-is (not replaceable); wrap it as `{ instance, replaceable, dispose }` to opt
226
+ * into hot swapping. Design: docs/architecture.md §5.5.
138
227
  */
139
228
  readonly services?: Record<string, unknown | ({
140
229
  readonly instance: unknown;
@@ -143,15 +232,15 @@ export interface HarnessOptions<TContext = unknown> {
143
232
  * Opt-in to FILE extensions, both tiers, from this directory (one folder per extension).
144
233
  * The single explicit switch: without it no file-extension machinery exists ("default
145
234
  * closed"); with it, `harness.extensions` loads/reloads/unloads them — each load being the
146
- * manual approval. Extensions with a `create` half publish process-level services and may
235
+ * manual approval. Extensions with a `harness` half publish process-level services and may
147
236
  * embed a session half; see docs/architecture.md §5.4-5.5. (Unrelated to
148
237
  * `pluginManager`, the Codex-compatible skill/MCP plugin system.)
149
238
  */
150
239
  readonly extensionDir?: string;
151
240
  /**
152
- * Root of the per-extension data folders handed to `create` halves as `host.dataDir`
241
+ * Root of the per-extension data folders handed to `harness` halves as `host.dataDir`
153
242
  * (`<root>/<id>`). Defaults to `<extensionDir>/.data` when `extensionDir` is set; by value
154
- * only, set it to give `create` halves a place for their files (absent ⇒ `dataDir` is
243
+ * only, set it to give `harness` halves a place for their files (absent ⇒ `dataDir` is
155
244
  * `undefined`). Kept outside the code folders so an update never touches state.
156
245
  */
157
246
  readonly extensionDataDir?: string;
@@ -185,45 +274,21 @@ export interface HarnessOptions<TContext = unknown> {
185
274
  * `subagentProvider: null`, which necessarily takes both tools with it.
186
275
  */
187
276
  readonly workflowTool?: boolean;
188
- /**
189
- * Machine for sessions — one shared `Machine` (the sandbox case), or a
190
- * {@link MachineFactory} for a per-session one. Defaults to a `LocalMachine`
191
- * rooted at each session's own `workDir`.
192
- */
193
- readonly machine?: Machine | MachineFactory;
194
- /**
195
- * Capabilities for sessions. Defaults to `defaultCapabilities()`. Pass a factory (sync or async)
196
- * to build a FRESH capability set per session — so per-session state (goal/plan/todo/background/
197
- * skills/mcp) is isolated and re-reads sources like a plugin manager each session. A plain array
198
- * is shared across the harness's sessions (fine only when state needn't be isolated).
199
- *
200
- * The factory receives the session it is being built for, so capabilities can be
201
- * scoped to it — see {@link SessionCapabilityContext}. Existing zero-argument
202
- * factories keep working unchanged.
203
- */
204
- readonly capabilities?: readonly Capability[] | ((ctx: SessionCapabilityContext) => readonly Capability[] | Promise<readonly Capability[]>);
205
277
  /**
206
278
  * Programmatic runtime extensions, registered once here — the by-value twin of `extensionDir`.
207
- * Every definition's `setup` runs in every session; a session varies that with
279
+ * Every definition's `session` runs in every session; a session varies that with
208
280
  * `createSession({ params })` (a value configures, `false` skips). A definition with a
209
- * `create` half has it run here, once, and the result registered as a service under its `id`
281
+ * `harness` half has it run here, once, and the result registered as a service under its `id`
210
282
  * before any session opens; `harness.close()` unregisters (and disposes) those services.
211
283
  * Each definition's `uses` is checked at this point, in order — a consumer comes after its
212
- * provider (or after `services`). Synchronous `create`s finish before `createHarness`
213
- * returns; an async one makes sessions wait for it, so `create` must not open a session of
284
+ * provider (or after `services`). Synchronous `harness`s finish before `createHarness`
285
+ * returns; an async one makes sessions wait for it, so `harness` must not open a session of
214
286
  * its own. Intentionally independent from Codex-compatible plugins (`pluginManager`): those
215
287
  * are skills/MCP/hooks metadata, extensions are trusted host code.
216
288
  */
217
289
  readonly extensions?: readonly ExtensionDefinition[];
218
290
  /** Permission config. Defaults to `{ mode: "yolo" }`. */
219
291
  readonly permission?: PermissionManagerOptions;
220
- /**
221
- * Logger for loop diagnostics (LLM request/response, tool I/O, compaction). Defaults to the
222
- * `AGENTS_LOG` env var (`AGENTS_LOG=debug` builds a console logger); omit both to stay silent.
223
- */
224
- readonly logger?: Logger;
225
- /** Context budget for the default compaction capability. */
226
- readonly maxContextTokens?: number;
227
292
  readonly maxTurns?: number;
228
293
  readonly maxStepsPerTurn?: number;
229
294
  }
@@ -233,9 +298,9 @@ export interface HarnessOptions<TContext = unknown> {
233
298
  * Exists so conversation-scoped resources don't force conversation-scoped harnesses.
234
299
  * MCP is the motivating case: a host that gives each conversation its own server
235
300
  * (a REPL kernel keyed by conversation id, say) used to need a whole separate
236
- * harness per conversation, duplicating the workspace's agent profile, tool palette,
237
- * plugins and skills along with it. Session-scoped servers keep the harness at
238
- * workspace scope, where it belongs — `mcpServers` here is the session-level
301
+ * harness per conversation, duplicating the agent profile, tool palette, plugins and
302
+ * skills along with it. Session-level servers keep one harness serving every
303
+ * conversation — `mcpServers` here is the session-level
239
304
  * counterpart of the harness-level `DefaultCapabilitiesOptions.mcpServers`.
240
305
  */
241
306
  export interface SessionCapabilityContext {
@@ -243,6 +308,14 @@ export interface SessionCapabilityContext {
243
308
  readonly workDir: string;
244
309
  /** Session-scoped MCP servers, as given to createSession / resumeSession / forkSession. */
245
310
  readonly mcpServers?: Record<string, McpServerConfig>;
311
+ /** The session brought its own environment (`{ environment }` on this open): harness-shared objects
312
+ * derived from the harness's environment (the skill scan) do not apply to it. */
313
+ readonly ownEnvironment: boolean;
314
+ /** {@link OpenSessionOptionsBase.mcpCredentials} for this open. */
315
+ readonly mcpCredentials?: McpCredentialProvider;
316
+ /** {@link OpenSessionOptionsBase.strictIsolation} for this open. A custom factory owns honouring
317
+ * it the way `defaultCapabilities` does. */
318
+ readonly strictIsolation: boolean;
246
319
  }
247
320
  /**
248
321
  * Options common to every way of opening a session.
@@ -258,20 +331,32 @@ export interface SessionCapabilityContext {
258
331
  interface OpenSessionOptionsBase<TContext = unknown> {
259
332
  /** Override the agent for this session only. */
260
333
  readonly agent?: Agent<TContext>;
261
- /** Override the execution machine for this session only. Lifecycle remains host-owned. */
262
- readonly machine?: Machine | MachineFactory;
334
+ /** Override the execution environment for this session only. Lifecycle remains host-owned. */
335
+ readonly environment?: Environment | EnvironmentFactory;
263
336
  /** Runtime-only application context for this session. Overrides `HarnessOptions.context`. */
264
337
  readonly context?: TContext;
265
338
  /**
266
339
  * MCP servers scoped to this session, surfaced to the capability factory via
267
340
  * {@link SessionCapabilityContext}. Merge policy is the factory's call — the
268
- * harness only carries them across.
341
+ * harness only carries them across. `defaultCapabilities` (and so the local preset)
342
+ * layers them OVER the harness's shared servers, a reused name shadowing the
343
+ * shared one for this session; a custom factory can decide otherwise.
269
344
  */
270
345
  readonly mcpServers?: Record<string, McpServerConfig>;
346
+ /**
347
+ * Where this session's http MCP servers get their credentials, asked on every connect: the
348
+ * seam a host builds a per-user vault on. Not persisted — a resume passes it again.
349
+ */
350
+ readonly mcpCredentials?: McpCredentialProvider;
351
+ /**
352
+ * This session is one tenant's among many: see `DefaultCapabilitiesOptions.strictIsolation`.
353
+ * A managed host's worker sets it on every open unless it runs single-tenant.
354
+ */
355
+ readonly strictIsolation?: boolean;
271
356
  /**
272
357
  * Per-session argument for each registered extension, by extension id — handed to that
273
- * extension's `setup(api, ctx)` as `ctx.params`. A value of `false` skips the extension for
274
- * this session entirely (its `setup` never runs). Persisted with the session, so a
358
+ * extension's `session(api, ctx)` as `ctx.params`. A value of `false` skips the extension for
359
+ * this session entirely (its `session` never runs). Persisted with the session, so a
275
360
  * `resumeSession` or a reload re-applies the same params without the caller repeating them.
276
361
  * This is the per-session knob: definitions themselves are registered once, on the harness.
277
362
  */
@@ -317,6 +402,9 @@ declare class MutableResponder implements Responder {
317
402
  /** No handler ⇒ not a live approver ⇒ the run interrupts durably (resume later via
318
403
  * `HarnessSession.resume`) instead of auto-rejecting the tool. */
319
404
  isLiveApprover(): boolean;
405
+ /** Same rule for questions: no handler ⇒ nobody attached ⇒ AskUserQuestion suspends durably
406
+ * (the question is on the paused run's pending list) instead of being dismissed unanswered. */
407
+ isLiveQuestioner(): boolean;
320
408
  requestApproval(request: ApprovalRequest, options?: ApprovalRequestOptions): Promise<ApprovalResponse>;
321
409
  requestQuestion(request: QuestionRequest, options?: {
322
410
  readonly signal?: AbortSignal;
@@ -346,22 +434,34 @@ export declare class HarnessSession<TContext = unknown> {
346
434
  private readonly responder;
347
435
  private readonly onClosed;
348
436
  private runContext;
349
- private currentRun;
350
437
  /** Barrier gate (docs/architecture.md §5.5): while set, no NEW run starts on this
351
438
  * session — prompt/resume park at it, promptStream starts lazily behind it. The in-flight
352
439
  * run is never touched. */
353
440
  private runGate;
354
- /** Resolves when the latest run settles — the "arrived at the boundary" signal a barrier
355
- * coordinator awaits. Undefined while idle. */
356
- private runSettled;
357
- private readonly runSettles;
441
+ /**
442
+ * Every run this facade has begun and not yet seen settle, by its controller. More than one
443
+ * entry means runs are queued behind the core's run lock (`prompt` while a turn is in flight):
444
+ * "busy" is this set being non-empty, `cancel()` aborts ALL of them, and the barrier's
445
+ * quiescence is ALL of them settling — tracking only the last-begun run would abort the
446
+ * queued one and leave the running one untouched.
447
+ */
448
+ private readonly runs;
358
449
  private lastRunInterrupted;
359
- private closed;
360
- private deliveryCounter;
450
+ /** open → closing (runs being drained, resources going down) → closed. Never goes back. */
451
+ private lifecycle;
452
+ /** The one close in flight (or finished): every `close()` call returns THIS promise. */
453
+ private closing;
454
+ /** Resolves the moment close() begins — what wakes a caller parked at the barrier gate so it
455
+ * can fail with the closed error instead of hanging on a gate nobody will release. */
456
+ private notifyClosing;
457
+ private readonly closingNotified;
361
458
  /** A wake is already scheduled on the microtask queue; more enqueues fold into it. */
362
459
  private wakeScheduled;
363
460
  /** Per-session turn cap; the Runner's own config is the fallback. */
364
461
  private readonly maxTurns;
462
+ /** Builds the capability an extension attached to THIS session runs as — bound to the
463
+ * session's harness reach and its per-session params. */
464
+ private readonly mountExtension;
365
465
  constructor(args: {
366
466
  core: Session;
367
467
  agent: Agent<TContext>;
@@ -373,17 +473,26 @@ export declare class HarnessSession<TContext = unknown> {
373
473
  context?: TContext;
374
474
  maxTurns?: number;
375
475
  interrupted?: boolean;
376
- onClosed: (id: string) => void;
476
+ mountExtension: (definition: ExtensionDefinition) => Capability;
477
+ onClosed: (id: string) => void | Promise<void>;
377
478
  });
378
479
  /**
379
480
  * Start a turn to consume queued follow-ups, once the current synchronous work is done.
380
481
  *
381
482
  * Deferred to a microtask for two reasons: several enqueues in one stack fold into a single
382
- * wake, and a settle that lands in a finishing run's own stack sees `currentRun` already
483
+ * wake, and a settle that lands in a finishing run's own stack sees its run already
383
484
  * cleared. Re-checked at fire time because either can change in between — and re-checked
384
485
  * again after every run, since the queue may have filled during one whose drain had passed.
385
486
  */
386
487
  private scheduleIdleWake;
488
+ /** Not open: closing or closed. Every run entry point refuses from here on. */
489
+ private get closed();
490
+ /**
491
+ * Park at the barrier gate until released — or until this session starts closing, in which
492
+ * case fail with the closed error rather than wait on a gate no coordinator will release.
493
+ * Re-checked in a loop: a new hold can land between a release and this task resuming.
494
+ */
495
+ private parkAtGate;
387
496
  /** Run options every run on this session carries — currently the session's turn cap. */
388
497
  private get runOverrides();
389
498
  /** Current runtime-only application context supplied to Agent callbacks. */
@@ -396,6 +505,10 @@ export declare class HarnessSession<TContext = unknown> {
396
505
  setContext(context: TContext | undefined): void;
397
506
  private beginRun;
398
507
  private endRun;
508
+ /** A run is in flight or queued behind the core's run lock. */
509
+ private hasActiveRuns;
510
+ /** Settles when every run begun so far has settled (trivially, when idle). */
511
+ private allRunsSettled;
399
512
  /**
400
513
  * Barrier support (docs/architecture.md §5.5): refuse NEW runs until released and
401
514
  * report when this session reaches its run boundary. Installing the gate is synchronous — a
@@ -413,41 +526,32 @@ export declare class HarnessSession<TContext = unknown> {
413
526
  private runPrompt;
414
527
  /**
415
528
  * Hand a message to the frame running at `address` in this session (`main` for the root agent,
416
- * `main/<agentId>` for a subagent); `false` when nobody is there. The seam an out-of-process
417
- * coordinator uses to address a specific subagent.
529
+ * `main/<agentId>` for a subagent); undefined when nobody is there, or the session is paused
530
+ * on a question. The seam an out-of-process coordinator uses to address a specific subagent.
531
+ *
532
+ * The receipt's `journaled` settles once the `steer.queued` record is on disk — the moment a
533
+ * caller may answer "accepted" to someone else and mean it survives a crash. A running frame
534
+ * drains the message at its next boundary; an idle root frame is woken to take it.
418
535
  */
419
- steerTo(address: string, content: string, origin: SteerOrigin): boolean;
536
+ steerTo(address: string, content: string, origin: SteerOrigin, options?: SteerOptions): SteerReceipt | undefined;
420
537
  /**
421
- * Deliver an externally-originated message. A running session receives it through the
422
- * requested SteerBus channel; an idle session starts a new turn immediately.
538
+ * Put a message whose `steer.queued` record ALREADY exists back on the bus.
423
539
  *
424
- * ACCEPTANCE IS DURABLE BEFORE THIS RESOLVES. The input is journaled as `inbox.received`
425
- * and that write is awaited before a receipt exists, so the receipt means "this survives a
426
- * crash", not merely "this reached a process". Previously the message lived only in memory
427
- * (SteerBus, or an argument to `runPrompt`) until the run that consumed it journaled it —
428
- * a crash in that window lost a delivery the caller had already been told was accepted.
429
- *
430
- * The provenance of what the model ultimately sees is still persisted on the eventual
431
- * `message.appended` record: acceptance and processing are journaled separately because
432
- * they can legitimately differ (capability rewrite, guardrail rejection, or a capability
433
- * answering the prompt outright).
540
+ * For whoever holds a reopened session and finds, in its log, accepted messages no turn ever
541
+ * consumed — a `steer.queued` with no `message.appended` carrying its `steerId`. That happens
542
+ * when acceptance and execution are different processes (a managed API node writes the record,
543
+ * a worker runs it later, possibly on another machine) or when the process that accepted a
544
+ * message died before a turn drained it. Nothing is journaled again: the record and its
545
+ * `steer.queued` event exist from the first acceptance. Run state is read NOW, not then — a
546
+ * running turn drains it at its next boundary, an idle session is woken to take it.
434
547
  */
435
- deliver(input: string, options: DeliveryOptions): Promise<DeliveryReceipt>;
548
+ requeue(item: SteerMessage): SteerReceipt;
436
549
  /**
437
- * Dispatch an input whose acceptance is ALREADY journaled.
438
- *
439
- * A worker draining a session's inbox calls this directly: the `inbox.received` record exists,
440
- * so going back through `deliver` would write a second one and count the delivery twice. The
441
- * split is the whole reason acceptance and dispatch are separable — accepting is a durable
442
- * write that must happen before a receipt exists, dispatching is what someone holding the
443
- * session's lease does with it afterwards, possibly on another machine, possibly much later.
444
- *
445
- * Run state is read HERE, not at acceptance: by now a turn may have started or ended, so
446
- * whether this steers work in flight or starts a fresh turn can only be decided now.
550
+ * Settles when no run is in flight. Yields once first, so a run that an enqueue on the
551
+ * previous line scheduled (the idle wake fires from a microtask) is counted as in flight
552
+ * rather than missed. A run that starts afterwards is not waited for.
447
553
  */
448
- dispatchAccepted(input: string, origin: SteerOrigin & {
449
- readonly kind: "external";
450
- }, acceptedAt?: number): DeliveryReceipt;
554
+ whenIdle(): Promise<void>;
451
555
  /**
452
556
  * Resume a durably-interrupted run. When a `prompt` returns `status:"interrupted"` — which
453
557
  * happens when no live approval handler is registered (`no live responder ⇒ durable`) — the run's
@@ -473,18 +577,19 @@ export declare class HarnessSession<TContext = unknown> {
473
577
  /** Synthesize the terminal RunResult for a guardrail tripwire (facade contract). */
474
578
  private guardrailBlocked;
475
579
  /**
476
- * Inject a user message into the in-flight (or next) turn. Returns the steer's correlation
477
- * id: a `steer.queued` event carries it now, and the consuming `message.appended` event's
478
- * `origin.steerId` matches it when the model actually sees the message.
580
+ * Inject a user message into the in-flight (or next) turn. The receipt's `steerId` is the
581
+ * correlation key: a `steer.queued` event carries it now, and the consuming `message.appended`
582
+ * event's `origin.steerId` matches it when the model actually sees the message; `journaled`
583
+ * settles once the `steer.queued` record is durable.
479
584
  */
480
- steer(input: string): string;
585
+ steer(input: string): SteerReceipt;
481
586
  /**
482
587
  * Queue a user message for a new turn after the current run's active turn finishes.
483
- * Returns the steer's correlation id (see `steer`), or null when the session has no
484
- * active run — use `prompt` instead in that case.
588
+ * Returns the receipt (see `steer`), or null when the session has no active run — use
589
+ * `prompt` instead in that case.
485
590
  */
486
- followUp(input: string): string | null;
487
- /** Abort the current run. */
591
+ followUp(input: string): SteerReceipt | null;
592
+ /** Abort every run this session has accepted: the one in flight AND any queued behind it. */
488
593
  cancel(): void;
489
594
  /** Subscribe to this session's event stream. Returns an unsubscribe fn. */
490
595
  onEvent(listener: AgentEventListener): () => void;
@@ -506,33 +611,45 @@ export declare class HarnessSession<TContext = unknown> {
506
611
  setThinking(level: ThinkingLevel): void;
507
612
  /** In-memory switch is immediate; await the returned promise to confirm it was journaled. */
508
613
  setPermissionMode(mode: PermissionMode): Promise<void>;
509
- createGoal(...args: Parameters<Session["createGoal"]>): ReturnType<Session["createGoal"]>;
510
- getGoal(): ReturnType<Session["getGoal"]>;
511
- setPlanMode(...args: Parameters<Session["setPlanMode"]>): ReturnType<Session["setPlanMode"]>;
512
- getPlan(): ReturnType<Session["getPlan"]>;
513
- compact(...args: Parameters<Session["compact"]>): ReturnType<Session["compact"]>;
514
- listSkills(): ReturnType<Session["listSkills"]>;
515
- activateSkill(...args: Parameters<Session["activateSkill"]>): ReturnType<Session["activateSkill"]>;
516
- listPlugins(): ReturnType<Session["listPlugins"]>;
517
- installPlugin(...args: Parameters<Session["installPlugin"]>): ReturnType<Session["installPlugin"]>;
518
- listBackgroundTasks(...args: Parameters<Session["listBackgroundTasks"]>): ReturnType<Session["listBackgroundTasks"]>;
614
+ /** The model this session runs on, as last set; undefined means the agent's own. */
615
+ get modelSetting(): string | ChatModel | undefined;
616
+ /** The thinking level this session runs at, as last set; undefined means the model's default. */
617
+ get thinkingSetting(): ThinkingLevel | undefined;
618
+ /** The permission mode in force right now — what a status line reads instead of tracking it. */
619
+ get permissionMode(): PermissionMode | undefined;
620
+ get goal(): GoalStore;
621
+ get plan(): PlanMode;
622
+ get skills(): SkillsService;
623
+ get plugins(): PluginManager;
624
+ get compaction(): CompactionService;
625
+ /** PROBE tier: absent without the background capability — see `Session.background`. */
626
+ get background(): BackgroundManager | undefined;
627
+ /** PROBE tier: absent without an MCP capability. */
628
+ get mcp(): McpServersHandle | undefined;
519
629
  getRecords(...args: Parameters<Session["getRecords"]>): ReturnType<Session["getRecords"]>;
520
- listSubagents(...args: Parameters<Session["listSubagents"]>): ReturnType<Session["listSubagents"]>;
521
- reconcileSubagents(...args: Parameters<Session["reconcileSubagents"]>): ReturnType<Session["reconcileSubagents"]>;
630
+ getAuditRecords(): ReturnType<Session["getAuditRecords"]>;
522
631
  getContextBreakdown(): ReturnType<Session["getContextBreakdown"]>;
632
+ /** Where this session's tools run. A host uses it to run a command in the same place the agent
633
+ * does — one shell, one filesystem, one working directory. */
634
+ get environment(): Environment;
635
+ /** The current todo list (the `TodoList` tool's latest state); empty without the todo capability. */
636
+ getTodos(): readonly TodoItem[];
637
+ /** Every slash command this session answers to — the static registry plus the ones its
638
+ * capabilities and extensions contribute — for a UI's palette and autocompletion. */
639
+ listCommands(): readonly CommandInfo[];
523
640
  /**
524
641
  * Add an extension mid-session. Applied at the next quiet point: immediately when idle,
525
642
  * at the run's stop boundary when one is in flight — its tools appear at the next run's
526
643
  * assembly. The change is journaled; it does NOT survive `resumeSession` (re-attach, or
527
644
  * register it in `createHarness({ extensions })`, which every open re-evaluates). A definition
528
- * with a `create` half is accepted only once registered — its service must already exist.
645
+ * with a `harness` half is accepted only once registered — its service must already exist.
529
646
  */
530
647
  attachExtension(definition: ExtensionDefinition): Promise<void>;
531
648
  /** Remove an extension mid-session: `session.end("detach")` fires, then its scope unwinds. */
532
649
  detachExtension(extensionId: string): Promise<void>;
533
- /** Ids of currently attached extensions; empty when the session has no extensions capability. */
650
+ /** Ids of currently attached extensions, in mount order. */
534
651
  attachedExtensionIds(): readonly string[];
535
- /** Currently attached extensions with what each `uses`; empty without an extensions capability. */
652
+ /** Currently attached extensions with what each `uses`. */
536
653
  attachedExtensions(): readonly {
537
654
  readonly id: string;
538
655
  readonly uses: readonly string[];
@@ -547,38 +664,35 @@ export declare class HarnessSession<TContext = unknown> {
547
664
  * facade methods (e.g. cron's — see `cronExtension`). */
548
665
  extensionHandle<T = unknown>(extensionId: string): T | undefined;
549
666
  private extensionRuntime;
550
- pauseGoal(...args: Parameters<Session["pauseGoal"]>): ReturnType<Session["pauseGoal"]>;
551
- resumeGoal(...args: Parameters<Session["resumeGoal"]>): ReturnType<Session["resumeGoal"]>;
552
- cancelGoal(...args: Parameters<Session["cancelGoal"]>): ReturnType<Session["cancelGoal"]>;
553
- setGoalBudget(...args: Parameters<Session["setGoalBudget"]>): ReturnType<Session["setGoalBudget"]>;
554
- clearPlan(...args: Parameters<Session["clearPlan"]>): ReturnType<Session["clearPlan"]>;
555
- pendingCompaction(...args: Parameters<Session["pendingCompaction"]>): ReturnType<Session["pendingCompaction"]>;
556
- cancelCompaction(...args: Parameters<Session["cancelCompaction"]>): ReturnType<Session["cancelCompaction"]>;
557
- readBackgroundTaskOutput(...args: Parameters<Session["readBackgroundTaskOutput"]>): ReturnType<Session["readBackgroundTaskOutput"]>;
558
- readBackgroundTaskOutputDelta(...args: Parameters<Session["readBackgroundTaskOutputDelta"]>): ReturnType<Session["readBackgroundTaskOutputDelta"]>;
559
- stopBackgroundTask(...args: Parameters<Session["stopBackgroundTask"]>): ReturnType<Session["stopBackgroundTask"]>;
560
- /** Move a running detachable tool call (bash/subagent/workflow) into a background task. */
561
- detachTool(...args: Parameters<Session["detachTool"]>): ReturnType<Session["detachTool"]>;
562
- getPluginInfo(...args: Parameters<Session["getPluginInfo"]>): ReturnType<Session["getPluginInfo"]>;
563
- setPluginEnabled(...args: Parameters<Session["setPluginEnabled"]>): ReturnType<Session["setPluginEnabled"]>;
564
- setPluginMcpServerEnabled(...args: Parameters<Session["setPluginMcpServerEnabled"]>): ReturnType<Session["setPluginMcpServerEnabled"]>;
565
- removePlugin(...args: Parameters<Session["removePlugin"]>): ReturnType<Session["removePlugin"]>;
566
- reloadPlugins(...args: Parameters<Session["reloadPlugins"]>): ReturnType<Session["reloadPlugins"]>;
567
- listMcpServers(...args: Parameters<Session["listMcpServers"]>): ReturnType<Session["listMcpServers"]>;
568
- reconnectMcpServer(...args: Parameters<Session["reconnectMcpServer"]>): ReturnType<Session["reconnectMcpServer"]>;
569
- /** Close the session and release its resources. */
667
+ /**
668
+ * Close the session and release its resources. Order: stop accepting runs, abort the ones
669
+ * accepted (in flight and queued) and wait for them to settle — their tools' finally blocks,
670
+ * their journal writes — then detach the projection and take the core down. Every call
671
+ * returns the SAME promise, so nobody returns while the teardown is still running.
672
+ *
673
+ * The wait for runs is bounded (`CLOSE_RUN_WAIT_MS`, 5s): a run that ignores its abort signal
674
+ * is logged and left behind, and the teardown continues — the deadline caps the wait for
675
+ * runs, not the whole close (flushes and disposes carry their own deadlines).
676
+ */
570
677
  close(): Promise<void>;
678
+ private runClose;
571
679
  }
572
680
  /** Manages a fleet of in-process sessions over a shared engine config. */
573
681
  export declare class Harness<TContext = unknown> {
574
682
  private readonly options;
575
683
  private readonly runner;
576
- private readonly repo;
577
684
  /** Concrete tools the default profile's tool NAMES resolve against (keyed by schema name). */
578
685
  private readonly toolPalette;
579
686
  private readonly activeSessions;
580
- /** Process-level shared services. The host replaces providers here (`services.replace`);
581
- * sessions only ever see handles (`ctx.shared`, `ctx.services`). */
687
+ /** Opens in flight by session id (`resumeSession`): concurrent callers share ONE open — the
688
+ * whole of it, reconcile and failure cleanup included — instead of each building an instance. */
689
+ private readonly opening;
690
+ /** The one close in flight (or finished): every `close()` call returns THIS promise. */
691
+ private closing;
692
+ /** The process-lived objects this harness runs on (`HarnessOptions.harness`), plus defaults. */
693
+ private parts;
694
+ /** Process-level extension services by name (a facade over `scope`). The host replaces
695
+ * providers here (`services.replace`); sessions only ever see handles (`ctx.shared`, `ctx.services`). */
582
696
  readonly services: ServiceRegistry;
583
697
  /** The one in-flight reshape barrier (mutex: a second barrier operation rejects). Sessions
584
698
  * appearing while it holds are gated at birth — see the tail of `openFromStore`. */
@@ -586,7 +700,7 @@ export declare class Harness<TContext = unknown> {
586
700
  /** File-extension manager (both tiers) — present only when the host opted in with
587
701
  * `extensionDir`. NOT the Codex-compatible skill/MCP plugin system (`pluginManager`). */
588
702
  readonly extensions: HarnessExtensionManager | undefined;
589
- /** Service ids published by by-value `create` halves, in creation order — unregistered (and
703
+ /** Service ids published by by-value `harness` halves, in creation order — unregistered (and
590
704
  * disposed) by `close()`, newest first. */
591
705
  private readonly createdServices;
592
706
  /** The by-value definitions this harness currently mounts into newborn sessions, in
@@ -597,26 +711,39 @@ export declare class Harness<TContext = unknown> {
597
711
  private readonly valueDefs;
598
712
  /** Root of extensions' data folders (`host.dataDir` = `<root>/<id>`); absent ⇒ none handed out. */
599
713
  private readonly dataRoot;
600
- /** Settles once every by-value `create` half has published its service; sessions open after it. */
714
+ /** Settles once every by-value `harness` half has published its service; sessions open after it. */
601
715
  private readonly sharedReady;
602
- /** The extension whose `create` is running right now — its `createSession` must refuse. */
716
+ /** The extension whose `harness` is running right now — its `createSession` must refuse. */
603
717
  private runningCreate;
604
718
  constructor(options: HarnessOptions<TContext>);
605
719
  /**
606
- * Run one by-value definition's `create` half against a staging host: the service is COLLECTED,
607
- * not registered, so a throwing `create` publishes nothing. Stays synchronous when `create` is
720
+ * Which session-facing parts the `harness` hook built for the whole process — what a
721
+ * multi-tenant host must not have, since every session on the process would share them.
722
+ * Infrastructure (repository, logger, model runtime) is not listed: sharing it is the point.
723
+ */
724
+ processSharedParts(): Promise<readonly ("mcpServers" | "oauthService" | "skillRegistry")[]>;
725
+ /** The session repository, once the `harness` hook has had its say. */
726
+ private repository;
727
+ /** The fallback repository, built once so two opens share one in-memory store. */
728
+ private memoryRepository;
729
+ /** The harness logger: the preset's, else the `AGENTS_LOG` env logger, else silent. */
730
+ private get logger();
731
+ private defaultLogger;
732
+ /**
733
+ * Run one by-value definition's `harness` half against a staging host: the service is COLLECTED,
734
+ * not registered, so a throwing `harness` publishes nothing. Stays synchronous when `harness` is
608
735
  * — what lets a synchronous extension's service exist the moment `createHarness` returns.
609
736
  * Shared by construction and `replaceExtension`.
610
737
  */
611
738
  private stageValue;
612
739
  /**
613
740
  * Register the by-value definitions, in order: check each one's `uses` against the registry
614
- * (a consumer must come after its provider), then run its `create` half if it has one and
741
+ * (a consumer must come after its provider), then run its `harness` half if it has one and
615
742
  * register the result as a service under the extension's `id`. Stays synchronous as long as
616
- * every `create` is — so `createHarness` returns with a synchronous extension's service
617
- * already registered; the first async `create` turns the rest of the sequence into a promise
743
+ * every `harness` is — so `createHarness` returns with a synchronous extension's service
744
+ * already registered; the first async `harness` turns the rest of the sequence into a promise
618
745
  * chain that `openFromStore` awaits. Staging (`stageDefinition`) collects before registering,
619
- * so a throwing `create` publishes nothing.
746
+ * so a throwing `harness` publishes nothing.
620
747
  */
621
748
  private registerDefinitions;
622
749
  /**
@@ -626,12 +753,25 @@ export declare class Harness<TContext = unknown> {
626
753
  * session's capabilities.
627
754
  *
628
755
  * Built fresh per session. Runtime-specific prompt data is not captured here: `instructions`
629
- * resolves the current frame's machine + cwd through the Session cache, so root/subagent/worktree
630
- * prompts stay isolated without coupling Agent lifetime to machine lifetime.
756
+ * resolves the current frame's environment + cwd through the Session cache, so root/subagent/worktree
757
+ * prompts stay isolated without coupling Agent lifetime to environment lifetime.
631
758
  */
632
759
  private buildDefaultAgent;
633
760
  get sessions(): ReadonlyMap<string, HarnessSession<TContext>>;
634
761
  createSession(opts?: CreateSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
762
+ /**
763
+ * Register an open in flight under its id for as long as it runs. Every path that builds a
764
+ * session goes through here — create, resume, fork — so a `resumeSession` for an id that is
765
+ * being created joins that open instead of building a twin over the same store, and
766
+ * `closeSession` / `harness.close()` wait for it instead of closing its scope under it.
767
+ */
768
+ private trackOpen;
769
+ /**
770
+ * The open session with this id, or reopen it from the store. Concurrent calls for one id share
771
+ * a single open (`opening`): the second caller waits for the first's — reconcile included — and
772
+ * receives the same instance, never a second one that a close would then unregister. An
773
+ * instance that is closing is not handed out: the reopen waits for its close to finish first.
774
+ */
635
775
  resumeSession(id: string, opts?: ResumeSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
636
776
  /** Fork an existing session into a new one (copies its log + state) and open it. */
637
777
  forkSession(sourceId: string, opts?: ForkSessionOptions<TContext>): Promise<HarnessSession<TContext>>;
@@ -643,7 +783,8 @@ export declare class Harness<TContext = unknown> {
643
783
  inspectSession(id: string, options?: OpenSessionOptions): Promise<SessionHandle | undefined>;
644
784
  /** Read one catalog row without the O(N) `listSessions()` scan. */
645
785
  getSessionSummary(id: string): Promise<SessionSummary | undefined>;
646
- /** Close one open session by id (no-op if it isn't open). */
786
+ /** Close one open session by id (no-op if it isn't open). An open in flight for this id is
787
+ * waited for and then closed, so a close/delete never races a half-built session. */
647
788
  closeSession(id: string): Promise<void>;
648
789
  /**
649
790
  * Delete a session after releasing any open in-process handle. Soft by default — the durable
@@ -653,12 +794,23 @@ export declare class Harness<TContext = unknown> {
653
794
  /** Undo a soft delete. No-op when the session is absent or was never deleted. */
654
795
  restoreSession(id: string): Promise<void>;
655
796
  listSessions(filter?: ListSessionsFilter): Promise<readonly SessionSummary[]>;
797
+ /** Set a session's title (an empty string clears it). Works on open and closed sessions alike. */
798
+ renameSession(id: string, title: string): Promise<void>;
799
+ /**
800
+ * Close every open session, then the harness scope — which disposes everything registered
801
+ * there in reverse order: by-value `harness` halves die with the harness that ran them (each
802
+ * drained, then its dispose hook — default the instance's close()), then the host's own
803
+ * registrations, then the defaults.
804
+ */
656
805
  close(): Promise<void>;
806
+ private runClose;
807
+ /** Every path that opens a session starts here: a closing harness opens nothing new. */
808
+ private assertAcceptingSessions;
657
809
  /**
658
810
  * Replace by-value extensions with new versions, as one coordinated act — the by-value twin of
659
811
  * `harness.extensions.reload()` (docs/architecture.md §5.5). Every session holding
660
812
  * one of them rendezvous at its run boundary (in-flight runs finish untouched — nothing is
661
- * aborted); in that global quiet moment the old halves detach, each `create` half re-runs and
813
+ * aborted); in that global quiet moment the old halves detach, each `harness` half re-runs and
662
814
  * its service is swapped, and the new halves attach; then everyone resumes. Sessions born from
663
815
  * the quiet moment on are born with the new definitions. On rendezvous timeout NOTHING changes:
664
816
  * the barrier lifts, `BarrierTimeout` names the stuck sessions, and the caller retries later
@@ -670,7 +822,7 @@ export declare class Harness<TContext = unknown> {
670
822
  *
671
823
  * `services` swaps HOST-registered services (`createHarness({ services })`) in the same quiet
672
824
  * moment, for when the shape that changed is the host's rather than an extension's. Those obey
673
- * their `replaceable` flag; an extension's own `create` service swaps unconditionally, since
825
+ * their `replaceable` flag; an extension's own `harness` service swaps unconditionally, since
674
826
  * its owner is being replaced along with it.
675
827
  *
676
828
  * Implementation-only swaps need none of this — `harness.services.replace` (layer 1) lands on
@@ -684,13 +836,13 @@ export declare class Harness<TContext = unknown> {
684
836
  readonly drainTimeoutMs?: number;
685
837
  }): Promise<void>;
686
838
  /**
687
- * The service half of one replaced extension: publish what its `create` staged. `replace`
839
+ * The service half of one replaced extension: publish what its `harness` staged. `replace`
688
840
  * rather than unregister + register, so consumers this barrier never gated — other extensions
689
841
  * holding a `uses` handle — see no gap; `force` because the owner is being replaced along with
690
842
  * it (see `ServiceRegistry.replace`).
691
843
  */
692
844
  private publishValueService;
693
- /** Dispose a `create` result that was staged but never published (default rule: its `close()`). */
845
+ /** Dispose a `harness` result that was staged but never published (default rule: its `close()`). */
694
846
  private disposeStaged;
695
847
  /**
696
848
  * The barrier itself (docs/architecture.md §5.5), shared by `replaceExtension` and
@@ -701,7 +853,7 @@ export declare class Harness<TContext = unknown> {
701
853
  * Whatever happens, every gate is released.
702
854
  */
703
855
  private withBarrier;
704
- /** Resolve the capability set for one session — calling the factory fresh when one was given. */
856
+ /** Resolve the capability set for one session — the `session` hook, called fresh every time. */
705
857
  private resolveCapabilities;
706
858
  /**
707
859
  * The harness-tier reach handed to a session's extensions: other sessions, and the model
@@ -713,6 +865,13 @@ export declare class Harness<TContext = unknown> {
713
865
  */
714
866
  private extensionHost;
715
867
  private openFromStore;
868
+ /**
869
+ * Undo a failed `openFromStore`, newest artifact first. Every step runs even if an earlier one
870
+ * throws (a cleanup failure is logged, never allowed to mask the open's own error).
871
+ */
872
+ private rollbackOpen;
873
+ /** Every definition this harness mounts, by value then from files. */
874
+ private allDefinitions;
716
875
  private contextFor;
717
876
  }
718
877
  /**
@@ -726,7 +885,7 @@ export declare class Harness<TContext = unknown> {
726
885
  *
727
886
  * A worker that dies mid-turn journals no `turn.ended`; to every reader the turn is still
728
887
  * running. Opening the session repairs that (the same recovery runs inside `resumeSession`),
729
- * but opening boots capabilities and a machine just to write two records. This does only the
888
+ * but opening boots capabilities and an environment just to write two records. This does only the
730
889
  * repair: fold the log, and for every frame still inside a turn append `turn.ended` with
731
890
  * `reason: "failed"` (and `agent.ended` for a live sub-agent). Returns the events those records
732
891
  * project to, so a host that has live subscribers can hand them on — a record appended here