specrails-core 6.4.0 → 6.6.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/dist/agent-runtime/capabilities.d.ts +2 -2
- package/dist/agent-runtime/capabilities.js +22 -5
- package/dist/agent-runtime/cli-executor.js +8 -3
- package/dist/agent-runtime/cli.d.ts +4 -4
- package/dist/agent-runtime/cli.js +50 -3
- package/dist/agent-runtime/compact/environment.d.ts +50 -4
- package/dist/agent-runtime/compact/environment.js +183 -6
- package/dist/agent-runtime/compact/step.d.ts +1 -1
- package/dist/agent-runtime/efficiency-summary.d.ts +1 -1
- package/dist/agent-runtime/efficiency-types.d.ts +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +15 -15
- package/dist/agent-runtime/engine/contracts.d.ts +2 -1
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +8 -8
- package/dist/agent-runtime/engine/fork.d.ts +1 -1
- package/dist/agent-runtime/engine/graph-description.d.ts +16 -16
- package/dist/agent-runtime/engine/implementation-compiler.d.ts +68 -68
- package/dist/agent-runtime/engine/piece-dependencies.js +4 -4
- package/dist/agent-runtime/engine/pieces/implementation-step.d.ts +1 -1
- package/dist/agent-runtime/engine/pieces/role-turn.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/role-turn.js +64 -22
- package/dist/agent-runtime/engine/pieces/shared.d.ts +2 -2
- package/dist/agent-runtime/engine/pieces/verify.js +46 -13
- package/dist/agent-runtime/engine/run-status.d.ts +165 -165
- package/dist/agent-runtime/engine/runs.d.ts +168 -168
- package/dist/agent-runtime/engine/state.d.ts +16 -16
- package/dist/agent-runtime/evaluation.d.ts +3 -3
- package/dist/agent-runtime/executor-types.d.ts +10 -10
- package/dist/agent-runtime/graph/convergence.d.ts +2 -2
- package/dist/agent-runtime/graph/convergence.js +11 -0
- package/dist/agent-runtime/graph/nodes.js +35 -4
- package/dist/agent-runtime/graph/review-policy.d.ts +1 -1
- package/dist/agent-runtime/graph/state.d.ts +6 -2
- package/dist/agent-runtime/guardrails.d.ts +1 -1
- package/dist/agent-runtime/guardrails.js +4 -1
- package/dist/agent-runtime/index.d.ts +3 -2
- package/dist/agent-runtime/index.js +1 -0
- package/dist/agent-runtime/openai-executor.d.ts +9 -9
- package/dist/agent-runtime/openspec.d.ts +4 -4
- package/dist/agent-runtime/prompts.d.ts +6 -0
- package/dist/agent-runtime/prompts.js +32 -8
- package/dist/agent-runtime/recovery.d.ts +12 -12
- package/dist/agent-runtime/session/application/active-session.d.ts +176 -0
- package/dist/agent-runtime/session/application/active-session.js +701 -0
- package/dist/agent-runtime/session/application/delegation.d.ts +73 -0
- package/dist/agent-runtime/session/application/delegation.js +171 -0
- package/dist/agent-runtime/session/application/output-buffer.d.ts +43 -0
- package/dist/agent-runtime/session/application/output-buffer.js +65 -0
- package/dist/agent-runtime/session/application/session-service.d.ts +121 -0
- package/dist/agent-runtime/session/application/session-service.js +256 -0
- package/dist/agent-runtime/session/application/timers.d.ts +15 -0
- package/dist/agent-runtime/session/application/timers.js +35 -0
- package/dist/agent-runtime/session/domain/descriptor.d.ts +6 -0
- package/dist/agent-runtime/session/domain/descriptor.js +32 -0
- package/dist/agent-runtime/session/domain/errors.d.ts +9 -0
- package/dist/agent-runtime/session/domain/errors.js +16 -0
- package/dist/agent-runtime/session/domain/events.d.ts +147 -0
- package/dist/agent-runtime/session/domain/events.js +24 -0
- package/dist/agent-runtime/session/domain/fingerprint.d.ts +2 -0
- package/dist/agent-runtime/session/domain/fingerprint.js +10 -0
- package/dist/agent-runtime/session/domain/interruption.d.ts +13 -0
- package/dist/agent-runtime/session/domain/interruption.js +39 -0
- package/dist/agent-runtime/session/domain/policy.d.ts +42 -0
- package/dist/agent-runtime/session/domain/policy.js +104 -0
- package/dist/agent-runtime/session/domain/snapshot.d.ts +100 -0
- package/dist/agent-runtime/session/domain/snapshot.js +234 -0
- package/dist/agent-runtime/session/domain/state-machines.d.ts +21 -0
- package/dist/agent-runtime/session/domain/state-machines.js +41 -0
- package/dist/agent-runtime/session/domain/types.d.ts +130 -0
- package/dist/agent-runtime/session/domain/types.js +15 -0
- package/dist/agent-runtime/session/domain/usage.d.ts +54 -0
- package/dist/agent-runtime/session/domain/usage.js +86 -0
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +67 -0
- package/dist/agent-runtime/session/drivers/claude/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/claude/driver.js +134 -0
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +40 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +247 -0
- package/dist/agent-runtime/session/drivers/codex/argv.d.ts +12 -0
- package/dist/agent-runtime/session/drivers/codex/argv.js +76 -0
- package/dist/agent-runtime/session/drivers/codex/driver.d.ts +19 -0
- package/dist/agent-runtime/session/drivers/codex/driver.js +193 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.d.ts +28 -0
- package/dist/agent-runtime/session/drivers/codex/rpc.js +81 -0
- package/dist/agent-runtime/session/drivers/codex/translator.d.ts +53 -0
- package/dist/agent-runtime/session/drivers/codex/translator.js +267 -0
- package/dist/agent-runtime/session/drivers/common/process.d.ts +37 -0
- package/dist/agent-runtime/session/drivers/common/process.js +116 -0
- package/dist/agent-runtime/session/drivers/executor/driver.d.ts +22 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +163 -0
- package/dist/agent-runtime/session/drivers/registry.d.ts +32 -0
- package/dist/agent-runtime/session/drivers/registry.js +38 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +18 -0
- package/dist/agent-runtime/session/host/protocol.js +88 -0
- package/dist/agent-runtime/session/host/server.d.ts +57 -0
- package/dist/agent-runtime/session/host/server.js +229 -0
- package/dist/agent-runtime/session/index.d.ts +45 -0
- package/dist/agent-runtime/session/index.js +74 -0
- package/dist/agent-runtime/session/journal/schema.d.ts +7 -0
- package/dist/agent-runtime/session/journal/schema.js +44 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.d.ts +70 -0
- package/dist/agent-runtime/session/journal/sqlite-journal.js +279 -0
- package/dist/agent-runtime/session/ports.d.ts +192 -0
- package/dist/agent-runtime/session/ports.js +1 -0
- package/dist/agent-runtime/session/testing/driver-conformance.d.ts +21 -0
- package/dist/agent-runtime/session/testing/driver-conformance.js +97 -0
- package/dist/agent-runtime/session/testing/fake-clock.d.ts +20 -0
- package/dist/agent-runtime/session/testing/fake-clock.js +34 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.d.ts +41 -0
- package/dist/agent-runtime/session/testing/fixture-replayer.js +172 -0
- package/dist/agent-runtime/session/testing/index.d.ts +9 -0
- package/dist/agent-runtime/session/testing/index.js +9 -0
- package/dist/agent-runtime/session/testing/memory-journal.d.ts +27 -0
- package/dist/agent-runtime/session/testing/memory-journal.js +70 -0
- package/dist/agent-runtime/session/testing/scripted-driver.d.ts +30 -0
- package/dist/agent-runtime/session/testing/scripted-driver.js +77 -0
- package/dist/agent-runtime/verification-proposals.d.ts +9 -0
- package/dist/agent-runtime/verification-proposals.js +69 -0
- package/dist/agent-runtime/verification-repair.d.ts +31 -2
- package/dist/agent-runtime/verification-repair.js +58 -3
- package/dist/installer/commands/init.d.ts +1 -1
- package/dist/installer/phases/install-config.js +10 -3
- package/dist/installer/util/paths.d.ts +25 -1
- package/dist/installer/util/registry.js +3 -3
- package/dist/pipeline/pipeline-state.d.ts +42 -11
- package/dist/pipeline/pipeline-state.js +47 -8
- package/dist/shared/specrails-home.d.ts +21 -0
- package/dist/shared/specrails-home.js +36 -0
- package/integration-contract.json +42 -1
- package/package.json +23 -11
- package/schemas/agent-runtime.schema.json +1 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { SessionError } from '../domain/errors.js';
|
|
2
|
+
import { resolvePolicy } from '../domain/policy.js';
|
|
3
|
+
import { liveSubagents } from '../domain/snapshot.js';
|
|
4
|
+
import { ActiveSession } from './active-session.js';
|
|
5
|
+
import { DelegationCoordinator } from './delegation.js';
|
|
6
|
+
export const DEFAULT_MAX_RESIDENT = 6;
|
|
7
|
+
/**
|
|
8
|
+
* Application facade for one scope: the use cases behind every protocol method.
|
|
9
|
+
* Operations on one session are serialized; different sessions run concurrently.
|
|
10
|
+
*/
|
|
11
|
+
export class SessionService {
|
|
12
|
+
options;
|
|
13
|
+
active = new Map();
|
|
14
|
+
locks = new Map();
|
|
15
|
+
listeners = new Set();
|
|
16
|
+
shuttingDown = false;
|
|
17
|
+
delegation;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
this.delegation = new DelegationCoordinator({
|
|
21
|
+
journal: options.journal,
|
|
22
|
+
ids: options.ids,
|
|
23
|
+
parent: (sessionId) => this.session(sessionId),
|
|
24
|
+
open: (params) => this.open(params),
|
|
25
|
+
send: (sessionId, input) => this.send(sessionId, input),
|
|
26
|
+
interrupt: (sessionId) => this.interrupt(sessionId),
|
|
27
|
+
close: (sessionId, reason) => this.close(sessionId, reason),
|
|
28
|
+
snapshot: (sessionId) => this.snapshot(sessionId),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
drivers() {
|
|
32
|
+
return this.options.drivers.descriptors();
|
|
33
|
+
}
|
|
34
|
+
subscribe(listener) {
|
|
35
|
+
this.listeners.add(listener);
|
|
36
|
+
return () => { this.listeners.delete(listener); };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mark work a previous host left running as interrupted. Called once at
|
|
40
|
+
* startup before serving requests; never starts a provider process.
|
|
41
|
+
*/
|
|
42
|
+
recover(reason = 'restart') {
|
|
43
|
+
const recovered = [];
|
|
44
|
+
for (const summary of this.options.journal.list('open')) {
|
|
45
|
+
const snapshot = this.options.journal.snapshot(summary.sessionId);
|
|
46
|
+
if (!snapshot || !needsRecovery(snapshot))
|
|
47
|
+
continue;
|
|
48
|
+
const factory = this.options.drivers.get(snapshot.driver);
|
|
49
|
+
if (!factory)
|
|
50
|
+
continue;
|
|
51
|
+
new ActiveSession(summary.sessionId, snapshot, this.configFrom(snapshot), this.deps(factory)).recoverAfterHostLoss(reason);
|
|
52
|
+
recovered.push(summary.sessionId);
|
|
53
|
+
}
|
|
54
|
+
// A delegated child cannot outlive the host that ran it: close the orphans.
|
|
55
|
+
for (const summary of this.options.journal.list('open')) {
|
|
56
|
+
if (this.options.journal.getSession(summary.sessionId)?.metadata?.delegated !== true)
|
|
57
|
+
continue;
|
|
58
|
+
const envelopes = this.options.journal.append(summary.sessionId, [{ type: 'session.closed', reason: 'delegation_interrupted', at: this.options.clock.iso() }]);
|
|
59
|
+
this.publish(envelopes);
|
|
60
|
+
}
|
|
61
|
+
return recovered;
|
|
62
|
+
}
|
|
63
|
+
async open(params) {
|
|
64
|
+
this.assertRunning();
|
|
65
|
+
if (params.resume)
|
|
66
|
+
return this.resume(params.resume.sessionId);
|
|
67
|
+
const factory = this.options.drivers.get(params.driver);
|
|
68
|
+
if (!factory)
|
|
69
|
+
throw new SessionError('driver_unavailable', `Driver "${params.driver}" is not registered`, { driver: params.driver });
|
|
70
|
+
const policy = resolvePolicy(params.policy, factory.descriptor);
|
|
71
|
+
const sessionId = params.sessionId ?? this.options.ids.session();
|
|
72
|
+
if (this.options.journal.getSession(sessionId))
|
|
73
|
+
throw new SessionError('invalid_params', `Session ${sessionId} already exists; use resume`, { path: 'sessionId' });
|
|
74
|
+
this.options.journal.createSession({ sessionId, driver: params.driver, cwd: params.cwd, createdAt: this.options.clock.iso(), metadata: { ...(params.metadata ?? {}) } });
|
|
75
|
+
const envelopes = this.options.journal.append(sessionId, [{
|
|
76
|
+
type: 'session.opened',
|
|
77
|
+
driver: params.driver,
|
|
78
|
+
model: params.model,
|
|
79
|
+
...(params.effort ? { effort: params.effort } : {}),
|
|
80
|
+
...(params.systemPrompt ? { systemPrompt: params.systemPrompt } : {}),
|
|
81
|
+
policy,
|
|
82
|
+
resumed: params.providerSessionRef !== undefined,
|
|
83
|
+
providerSessionRef: params.providerSessionRef ?? null,
|
|
84
|
+
at: this.options.clock.iso(),
|
|
85
|
+
}]);
|
|
86
|
+
this.publish(envelopes);
|
|
87
|
+
const session = this.activate(sessionId);
|
|
88
|
+
return { sessionId, snapshot: session.state };
|
|
89
|
+
}
|
|
90
|
+
async send(sessionId, input) {
|
|
91
|
+
return this.exclusive(sessionId, () => this.session(sessionId).send(input));
|
|
92
|
+
}
|
|
93
|
+
async interrupt(sessionId) {
|
|
94
|
+
return this.exclusive(sessionId, async () => ({ turnId: await this.session(sessionId).interrupt() }));
|
|
95
|
+
}
|
|
96
|
+
async stopSubagents(sessionId, subagentIds) {
|
|
97
|
+
return this.exclusive(sessionId, async () => {
|
|
98
|
+
const session = this.session(sessionId);
|
|
99
|
+
const native = subagentIds?.filter((id) => !this.delegation.isDelegated(id));
|
|
100
|
+
const delegated = await this.delegation.stop(sessionId, subagentIds);
|
|
101
|
+
const stopped = native?.length === 0 ? [] : await session.stopSubagents(native);
|
|
102
|
+
return { stopped: [...delegated, ...stopped] };
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Launch a sub-agent as a child session (delegated runtime). */
|
|
106
|
+
async delegate(sessionId, params) {
|
|
107
|
+
return this.exclusive(sessionId, () => this.delegation.delegate(sessionId, params));
|
|
108
|
+
}
|
|
109
|
+
/** Wait for delegated sub-agents; never holds the session lock while waiting. */
|
|
110
|
+
async waitSubagents(sessionId, subagentIds, timeoutMs) {
|
|
111
|
+
this.session(sessionId);
|
|
112
|
+
return this.delegation.wait(sessionId, subagentIds, timeoutMs);
|
|
113
|
+
}
|
|
114
|
+
async update(sessionId, changes) {
|
|
115
|
+
return this.exclusive(sessionId, async () => {
|
|
116
|
+
const session = this.session(sessionId);
|
|
117
|
+
const factory = this.requireFactory(session.state.driver);
|
|
118
|
+
const { policy, ...rest } = changes;
|
|
119
|
+
const resolved = policy ? { ...rest, policy: resolvePolicy(policy, factory.descriptor) } : rest;
|
|
120
|
+
return { outcome: await session.update(resolved) };
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async close(sessionId, reason) {
|
|
124
|
+
await this.exclusive(sessionId, async () => {
|
|
125
|
+
const snapshot = this.options.journal.snapshot(sessionId);
|
|
126
|
+
if (!snapshot)
|
|
127
|
+
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
128
|
+
if (snapshot.status === 'closed')
|
|
129
|
+
return;
|
|
130
|
+
await this.delegation.closeParent(sessionId);
|
|
131
|
+
await this.session(sessionId).close(reason);
|
|
132
|
+
this.active.delete(sessionId);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
snapshot(sessionId) {
|
|
136
|
+
const active = this.active.get(sessionId);
|
|
137
|
+
if (active)
|
|
138
|
+
return active.state;
|
|
139
|
+
const snapshot = this.options.journal.snapshot(sessionId);
|
|
140
|
+
if (!snapshot)
|
|
141
|
+
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
142
|
+
return snapshot;
|
|
143
|
+
}
|
|
144
|
+
events(sessionId, afterSeq, limit = 500) {
|
|
145
|
+
if (!this.options.journal.getSession(sessionId))
|
|
146
|
+
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
147
|
+
const page = this.options.journal.read(sessionId, afterSeq, Math.min(Math.max(limit, 1), 5_000));
|
|
148
|
+
return { events: page.events, nextSeq: page.events.at(-1)?.seq ?? afterSeq, hasMore: page.hasMore };
|
|
149
|
+
}
|
|
150
|
+
list(state = 'open') {
|
|
151
|
+
return this.options.journal.list(state);
|
|
152
|
+
}
|
|
153
|
+
stats() {
|
|
154
|
+
return { sessions: this.active.size, residentProcesses: [...this.active.values()].filter((session) => session.processAlive).length };
|
|
155
|
+
}
|
|
156
|
+
/** Retire every provider process; running work is recorded as interrupted. */
|
|
157
|
+
async shutdown() {
|
|
158
|
+
this.shuttingDown = true;
|
|
159
|
+
await Promise.allSettled([...this.active.values()].map((session) => session.retire('shutdown', { turnStatus: 'interrupted', subagentPhase: 'interrupted', reason: 'shutdown' })));
|
|
160
|
+
}
|
|
161
|
+
// ── internals ─────────────────────────────────────────────────────────────
|
|
162
|
+
async resume(sessionId) {
|
|
163
|
+
const snapshot = this.options.journal.snapshot(sessionId);
|
|
164
|
+
if (!snapshot)
|
|
165
|
+
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
166
|
+
if (snapshot.status === 'closed')
|
|
167
|
+
throw new SessionError('session_closed', `Session ${sessionId} is closed`);
|
|
168
|
+
const session = this.activate(sessionId);
|
|
169
|
+
return { sessionId, snapshot: session.state };
|
|
170
|
+
}
|
|
171
|
+
activate(sessionId) {
|
|
172
|
+
const existing = this.active.get(sessionId);
|
|
173
|
+
if (existing)
|
|
174
|
+
return existing;
|
|
175
|
+
const snapshot = this.options.journal.snapshot(sessionId);
|
|
176
|
+
if (!snapshot)
|
|
177
|
+
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
178
|
+
const session = new ActiveSession(sessionId, snapshot, this.configFrom(snapshot), this.deps(this.requireFactory(snapshot.driver)));
|
|
179
|
+
this.active.set(sessionId, session);
|
|
180
|
+
return session;
|
|
181
|
+
}
|
|
182
|
+
session(sessionId) {
|
|
183
|
+
this.assertRunning();
|
|
184
|
+
const snapshot = this.active.get(sessionId)?.state ?? this.options.journal.snapshot(sessionId);
|
|
185
|
+
if (!snapshot)
|
|
186
|
+
throw new SessionError('session_not_found', `Unknown session ${sessionId}`);
|
|
187
|
+
if (snapshot.status === 'closed')
|
|
188
|
+
throw new SessionError('session_closed', `Session ${sessionId} is closed`);
|
|
189
|
+
return this.activate(sessionId);
|
|
190
|
+
}
|
|
191
|
+
configFrom(snapshot) {
|
|
192
|
+
const record = this.options.journal.getSession(snapshot.sessionId);
|
|
193
|
+
if (!snapshot.policy || !record)
|
|
194
|
+
throw new SessionError('internal', `Session ${snapshot.sessionId} has no opening record`);
|
|
195
|
+
return { cwd: record.cwd, model: snapshot.model, effort: snapshot.effort, systemPrompt: snapshot.systemPrompt, policy: snapshot.policy };
|
|
196
|
+
}
|
|
197
|
+
deps(factory) {
|
|
198
|
+
return {
|
|
199
|
+
journal: this.options.journal,
|
|
200
|
+
factory,
|
|
201
|
+
clock: this.options.clock,
|
|
202
|
+
ids: this.options.ids,
|
|
203
|
+
rateCard: this.options.rateCard ?? (() => null),
|
|
204
|
+
publish: (envelopes) => this.publish(envelopes),
|
|
205
|
+
touched: () => this.enforceResidentCap(),
|
|
206
|
+
...(this.options.outputLimits ? { outputLimits: this.options.outputLimits } : {}),
|
|
207
|
+
...(this.options.interruptGraceMs !== undefined ? { interruptGraceMs: this.options.interruptGraceMs } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
requireFactory(driverId) {
|
|
211
|
+
const factory = this.options.drivers.get(driverId);
|
|
212
|
+
if (!factory)
|
|
213
|
+
throw new SessionError('driver_unavailable', `Driver "${driverId}" is not registered`, { driver: driverId });
|
|
214
|
+
return factory;
|
|
215
|
+
}
|
|
216
|
+
/** Keep-alive cap: retire least-recently-used idle processes; never busy ones. */
|
|
217
|
+
enforceResidentCap() {
|
|
218
|
+
const cap = this.options.maxResident ?? DEFAULT_MAX_RESIDENT;
|
|
219
|
+
const alive = [...this.active.values()].filter((session) => session.processAlive);
|
|
220
|
+
if (alive.length <= cap)
|
|
221
|
+
return;
|
|
222
|
+
const evictable = alive.filter((session) => !session.isBusy).sort((a, b) => a.lastUsedAt - b.lastUsedAt);
|
|
223
|
+
for (const session of evictable.slice(0, alive.length - cap)) {
|
|
224
|
+
void this.exclusive(session.sessionId, () => session.retire('evicted', { turnStatus: 'interrupted', subagentPhase: 'interrupted', reason: 'evicted' }));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
publish(envelopes) {
|
|
228
|
+
if (envelopes.length === 0)
|
|
229
|
+
return;
|
|
230
|
+
for (const listener of this.listeners)
|
|
231
|
+
listener(envelopes);
|
|
232
|
+
// Mirror delegated children into their parents (after hosts saw the child events).
|
|
233
|
+
this.delegation.observe(envelopes);
|
|
234
|
+
}
|
|
235
|
+
/** Serialize operations per session. */
|
|
236
|
+
exclusive(sessionId, fn) {
|
|
237
|
+
const previous = this.locks.get(sessionId) ?? Promise.resolve();
|
|
238
|
+
const next = previous.then(fn, fn);
|
|
239
|
+
const settled = next.catch(() => undefined);
|
|
240
|
+
this.locks.set(sessionId, settled);
|
|
241
|
+
void settled.then(() => { if (this.locks.get(sessionId) === settled)
|
|
242
|
+
this.locks.delete(sessionId); });
|
|
243
|
+
return next;
|
|
244
|
+
}
|
|
245
|
+
assertRunning() {
|
|
246
|
+
if (this.shuttingDown)
|
|
247
|
+
throw new SessionError('busy', 'The session host is shutting down');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function needsRecovery(snapshot) {
|
|
251
|
+
return snapshot.openTurn !== null
|
|
252
|
+
|| snapshot.process.alive
|
|
253
|
+
|| snapshot.phase !== 'idle'
|
|
254
|
+
|| liveSubagents(snapshot).length > 0
|
|
255
|
+
|| Object.values(snapshot.inputs).some((input) => input.state === 'accepted' || input.state === 'queued' || input.state === 'started');
|
|
256
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Clock } from '../ports.js';
|
|
2
|
+
/** The timers one session can hold; each kind has at most one armed timer. */
|
|
3
|
+
export type TimerKind = 'idle' | 'stall' | 'backgroundMax' | 'turnInactivity' | 'settle' | 'interruptGrace' | 'flush';
|
|
4
|
+
/** Named, cancellable timers for one session. Re-arming a kind replaces it. */
|
|
5
|
+
export declare class SessionTimers {
|
|
6
|
+
private readonly clock;
|
|
7
|
+
private readonly armed;
|
|
8
|
+
constructor(clock: Clock);
|
|
9
|
+
arm(kind: TimerKind, ms: number, callback: () => void): void;
|
|
10
|
+
/** Arm only when not already armed (deadlines that must not slide). */
|
|
11
|
+
ensure(kind: TimerKind, ms: number, callback: () => void): void;
|
|
12
|
+
isArmed(kind: TimerKind): boolean;
|
|
13
|
+
cancel(...kinds: TimerKind[]): void;
|
|
14
|
+
cancelAll(): void;
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Named, cancellable timers for one session. Re-arming a kind replaces it. */
|
|
2
|
+
export class SessionTimers {
|
|
3
|
+
clock;
|
|
4
|
+
armed = new Map();
|
|
5
|
+
constructor(clock) {
|
|
6
|
+
this.clock = clock;
|
|
7
|
+
}
|
|
8
|
+
arm(kind, ms, callback) {
|
|
9
|
+
this.cancel(kind);
|
|
10
|
+
const handle = this.clock.after(ms, () => {
|
|
11
|
+
if (this.armed.get(kind) !== handle)
|
|
12
|
+
return;
|
|
13
|
+
this.armed.delete(kind);
|
|
14
|
+
callback();
|
|
15
|
+
});
|
|
16
|
+
this.armed.set(kind, handle);
|
|
17
|
+
}
|
|
18
|
+
/** Arm only when not already armed (deadlines that must not slide). */
|
|
19
|
+
ensure(kind, ms, callback) {
|
|
20
|
+
if (!this.armed.has(kind))
|
|
21
|
+
this.arm(kind, ms, callback);
|
|
22
|
+
}
|
|
23
|
+
isArmed(kind) {
|
|
24
|
+
return this.armed.has(kind);
|
|
25
|
+
}
|
|
26
|
+
cancel(...kinds) {
|
|
27
|
+
for (const kind of kinds) {
|
|
28
|
+
this.armed.get(kind)?.cancel();
|
|
29
|
+
this.armed.delete(kind);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
cancelAll() {
|
|
33
|
+
this.cancel(...this.armed.keys());
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DriverDescriptor } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Validate a driver descriptor, including capability coherence: a driver
|
|
4
|
+
* cannot claim behaviour that depends on a capability it does not have.
|
|
5
|
+
*/
|
|
6
|
+
export declare function validateDescriptor(descriptor: DriverDescriptor): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SessionError } from './errors.js';
|
|
2
|
+
const DRIVER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
3
|
+
const COST = new Set(['session-cumulative', 'per-turn', 'none']);
|
|
4
|
+
const TOKENS = new Set(['per-turn', 'cumulative', 'none']);
|
|
5
|
+
/**
|
|
6
|
+
* Validate a driver descriptor, including capability coherence: a driver
|
|
7
|
+
* cannot claim behaviour that depends on a capability it does not have.
|
|
8
|
+
*/
|
|
9
|
+
export function validateDescriptor(descriptor) {
|
|
10
|
+
const fail = (message) => { throw new SessionError('internal', `Invalid driver descriptor "${descriptor?.id}": ${message}`); };
|
|
11
|
+
if (!descriptor || typeof descriptor.id !== 'string' || !DRIVER_ID.test(descriptor.id))
|
|
12
|
+
fail('id must match [a-z0-9][a-z0-9._-]{0,63}');
|
|
13
|
+
if (typeof descriptor.displayName !== 'string' || !descriptor.displayName.trim())
|
|
14
|
+
fail('displayName is required');
|
|
15
|
+
const caps = descriptor.capabilities;
|
|
16
|
+
if (!caps)
|
|
17
|
+
fail('capabilities are required');
|
|
18
|
+
for (const key of ['resident', 'nativeInputQueue', 'subagentDisable', 'autonomousContinuation', 'steer', 'toolFiltering']) {
|
|
19
|
+
if (typeof caps[key] !== 'boolean')
|
|
20
|
+
fail(`capabilities.${key} must be a boolean`);
|
|
21
|
+
}
|
|
22
|
+
if (caps.subagents !== 'supported' && caps.subagents !== 'unsupported')
|
|
23
|
+
fail('capabilities.subagents must be supported|unsupported');
|
|
24
|
+
if (caps.subagents === 'unsupported' && (caps.subagentDisable || caps.autonomousContinuation))
|
|
25
|
+
fail('sub-agent capabilities require subagents=supported');
|
|
26
|
+
if (!caps.resident && caps.nativeInputQueue)
|
|
27
|
+
fail('a native input queue requires a resident process');
|
|
28
|
+
if (!caps.resident && caps.autonomousContinuation)
|
|
29
|
+
fail('autonomous continuation requires a resident process');
|
|
30
|
+
if (!COST.has(caps.usage?.costUsd) || !TOKENS.has(caps.usage?.tokens))
|
|
31
|
+
fail('capabilities.usage is invalid');
|
|
32
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Stable error codes shared by the domain, application and protocol layers. */
|
|
2
|
+
export type SessionErrorCode = 'protocol_mismatch' | 'not_initialized' | 'invalid_params' | 'payload_too_large' | 'session_not_found' | 'session_closed' | 'driver_unavailable' | 'policy_unenforceable' | 'input_conflict' | 'illegal_transition' | 'journal_locked' | 'store_incompatible' | 'busy' | 'limit_reached' | 'internal';
|
|
3
|
+
export declare class SessionError extends Error {
|
|
4
|
+
readonly code: SessionErrorCode;
|
|
5
|
+
readonly detail?: Record<string, unknown> | undefined;
|
|
6
|
+
readonly retryable: boolean;
|
|
7
|
+
constructor(code: SessionErrorCode, message: string, detail?: Record<string, unknown> | undefined);
|
|
8
|
+
}
|
|
9
|
+
export declare function isSessionError(error: unknown): error is SessionError;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const RETRYABLE = new Set(['journal_locked', 'busy', 'internal']);
|
|
2
|
+
export class SessionError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
detail;
|
|
5
|
+
retryable;
|
|
6
|
+
constructor(code, message, detail) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.detail = detail;
|
|
10
|
+
this.name = 'SessionError';
|
|
11
|
+
this.retryable = RETRYABLE.has(code);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function isSessionError(error) {
|
|
15
|
+
return error instanceof SessionError;
|
|
16
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { Attachment, InputDelivery, OutputChannel, SessionPhase, SessionPolicy, SubagentKind, SubagentPhase, TurnOrigin, Usage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The journaled, provider-neutral event vocabulary (session protocol v1).
|
|
4
|
+
* Events are the source of truth: snapshots and projections are folds over them.
|
|
5
|
+
* Adding a type is non-breaking; changing a payload's meaning needs a new
|
|
6
|
+
* protocol version (docs/agent-sessions/protocol.md).
|
|
7
|
+
*/
|
|
8
|
+
export interface ToolActivity {
|
|
9
|
+
toolUseId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
phase: 'started' | 'completed';
|
|
12
|
+
input?: unknown;
|
|
13
|
+
output?: string;
|
|
14
|
+
isError?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type SessionEventBody = {
|
|
17
|
+
type: 'session.opened';
|
|
18
|
+
driver: string;
|
|
19
|
+
model: string;
|
|
20
|
+
effort?: string;
|
|
21
|
+
systemPrompt?: string;
|
|
22
|
+
policy: SessionPolicy;
|
|
23
|
+
resumed: boolean;
|
|
24
|
+
providerSessionRef: string | null;
|
|
25
|
+
} | {
|
|
26
|
+
type: 'session.phase';
|
|
27
|
+
phase: SessionPhase;
|
|
28
|
+
} | {
|
|
29
|
+
type: 'session.process';
|
|
30
|
+
state: 'started' | 'retired' | 'exited';
|
|
31
|
+
generation: number;
|
|
32
|
+
reason?: string;
|
|
33
|
+
exitCode?: number | null;
|
|
34
|
+
} | {
|
|
35
|
+
type: 'session.provider-ref';
|
|
36
|
+
providerSessionRef: string;
|
|
37
|
+
} | {
|
|
38
|
+
type: 'session.updated';
|
|
39
|
+
changes: Record<string, unknown>;
|
|
40
|
+
outcome: 'applied' | 'deferred';
|
|
41
|
+
} | {
|
|
42
|
+
type: 'session.closed';
|
|
43
|
+
reason: string;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'input.accepted';
|
|
46
|
+
inputId: string;
|
|
47
|
+
delivery: InputDelivery;
|
|
48
|
+
text: string;
|
|
49
|
+
attachments?: Attachment[];
|
|
50
|
+
} | {
|
|
51
|
+
type: 'input.state';
|
|
52
|
+
inputId: string;
|
|
53
|
+
state: 'queued' | 'started' | 'completed' | 'rejected' | 'interrupted';
|
|
54
|
+
turnId?: string;
|
|
55
|
+
reason?: string;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'turn.started';
|
|
58
|
+
turnId: string;
|
|
59
|
+
origin: TurnOrigin;
|
|
60
|
+
inputIds: string[];
|
|
61
|
+
trigger?: {
|
|
62
|
+
subagentIds: string[];
|
|
63
|
+
};
|
|
64
|
+
} | {
|
|
65
|
+
type: 'turn.output';
|
|
66
|
+
turnId: string;
|
|
67
|
+
channel: OutputChannel;
|
|
68
|
+
delta: string;
|
|
69
|
+
} | ({
|
|
70
|
+
type: 'turn.tool';
|
|
71
|
+
turnId: string;
|
|
72
|
+
} & ToolActivity) | {
|
|
73
|
+
type: 'turn.completed';
|
|
74
|
+
turnId: string;
|
|
75
|
+
status: 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
76
|
+
text: string;
|
|
77
|
+
error?: string;
|
|
78
|
+
usage: Usage;
|
|
79
|
+
} | {
|
|
80
|
+
type: 'subagent.started';
|
|
81
|
+
subagentId: string;
|
|
82
|
+
parentId: string | null;
|
|
83
|
+
kind: SubagentKind;
|
|
84
|
+
agentType?: string;
|
|
85
|
+
description: string;
|
|
86
|
+
prompt?: string;
|
|
87
|
+
delegated?: {
|
|
88
|
+
driver: string;
|
|
89
|
+
model: string;
|
|
90
|
+
};
|
|
91
|
+
} | {
|
|
92
|
+
type: 'subagent.phase';
|
|
93
|
+
subagentId: string;
|
|
94
|
+
phase: SubagentPhase;
|
|
95
|
+
reason?: string;
|
|
96
|
+
} | {
|
|
97
|
+
type: 'subagent.output';
|
|
98
|
+
subagentId: string;
|
|
99
|
+
channel: 'text' | 'tool';
|
|
100
|
+
delta?: string;
|
|
101
|
+
tool?: ToolActivity;
|
|
102
|
+
} | {
|
|
103
|
+
type: 'subagent.usage';
|
|
104
|
+
subagentId: string;
|
|
105
|
+
usage: Usage;
|
|
106
|
+
toolUses?: number;
|
|
107
|
+
durationMs?: number;
|
|
108
|
+
billing?: 'included' | 'separate';
|
|
109
|
+
} | {
|
|
110
|
+
type: 'subagent.result';
|
|
111
|
+
subagentId: string;
|
|
112
|
+
summary: string;
|
|
113
|
+
} | {
|
|
114
|
+
type: 'subagents.settled';
|
|
115
|
+
settled: boolean;
|
|
116
|
+
live: number;
|
|
117
|
+
} | {
|
|
118
|
+
type: 'output.truncated';
|
|
119
|
+
scope: {
|
|
120
|
+
turnId: string;
|
|
121
|
+
} | {
|
|
122
|
+
subagentId: string;
|
|
123
|
+
};
|
|
124
|
+
droppedEvents: number;
|
|
125
|
+
droppedBytes: number;
|
|
126
|
+
} | {
|
|
127
|
+
type: 'notice.interruption';
|
|
128
|
+
subagentIds: string[];
|
|
129
|
+
inputIds: string[];
|
|
130
|
+
} | {
|
|
131
|
+
type: 'provider.diagnostic';
|
|
132
|
+
level: 'info' | 'warning';
|
|
133
|
+
code: string;
|
|
134
|
+
message: string;
|
|
135
|
+
};
|
|
136
|
+
export type SessionEventType = SessionEventBody['type'];
|
|
137
|
+
/** A committed event: body plus journal coordinates. */
|
|
138
|
+
export type SessionEvent = SessionEventBody & {
|
|
139
|
+
at: string;
|
|
140
|
+
};
|
|
141
|
+
export interface SessionEventEnvelope {
|
|
142
|
+
sessionId: string;
|
|
143
|
+
seq: number;
|
|
144
|
+
event: SessionEvent;
|
|
145
|
+
}
|
|
146
|
+
/** Closed list of event types (mirrored into integration-contract.json). */
|
|
147
|
+
export declare const SESSION_EVENT_TYPES: readonly ["session.opened", "session.phase", "session.process", "session.provider-ref", "session.updated", "session.closed", "input.accepted", "input.state", "turn.started", "turn.output", "turn.tool", "turn.completed", "subagent.started", "subagent.phase", "subagent.output", "subagent.usage", "subagent.result", "subagents.settled", "output.truncated", "notice.interruption", "provider.diagnostic"];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Closed list of event types (mirrored into integration-contract.json). */
|
|
2
|
+
export const SESSION_EVENT_TYPES = Object.freeze([
|
|
3
|
+
'session.opened',
|
|
4
|
+
'session.phase',
|
|
5
|
+
'session.process',
|
|
6
|
+
'session.provider-ref',
|
|
7
|
+
'session.updated',
|
|
8
|
+
'session.closed',
|
|
9
|
+
'input.accepted',
|
|
10
|
+
'input.state',
|
|
11
|
+
'turn.started',
|
|
12
|
+
'turn.output',
|
|
13
|
+
'turn.tool',
|
|
14
|
+
'turn.completed',
|
|
15
|
+
'subagent.started',
|
|
16
|
+
'subagent.phase',
|
|
17
|
+
'subagent.output',
|
|
18
|
+
'subagent.usage',
|
|
19
|
+
'subagent.result',
|
|
20
|
+
'subagents.settled',
|
|
21
|
+
'output.truncated',
|
|
22
|
+
'notice.interruption',
|
|
23
|
+
'provider.diagnostic',
|
|
24
|
+
]);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 64-bit FNV-1a over UTF-16 code units, hex. Identity check only, not security. */
|
|
2
|
+
export function fingerprint(text) {
|
|
3
|
+
let hash = 0xcbf29ce484222325n;
|
|
4
|
+
const prime = 0x100000001b3n;
|
|
5
|
+
for (let index = 0; index < text.length; index++) {
|
|
6
|
+
hash ^= BigInt(text.charCodeAt(index));
|
|
7
|
+
hash = (hash * prime) & 0xffffffffffffffffn;
|
|
8
|
+
}
|
|
9
|
+
return hash.toString(16).padStart(16, '0');
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionSnapshot } from './snapshot.js';
|
|
2
|
+
export interface InterruptionNotice {
|
|
3
|
+
text: string;
|
|
4
|
+
subagentIds: string[];
|
|
5
|
+
inputIds: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Build the one-time notice prefixed to the next user input after work was
|
|
9
|
+
* interrupted. It names what happened and tells the agent not to relaunch it on
|
|
10
|
+
* its own — providers otherwise "resume" orphaned work silently (spike finding).
|
|
11
|
+
* Returns null when there is nothing to report.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildInterruptionNotice(state: SessionSnapshot): InterruptionNotice | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const REASONS = Object.freeze({
|
|
2
|
+
user_stop: 'stopped by the user',
|
|
3
|
+
host_request: 'stopped by the host',
|
|
4
|
+
restart: 'interrupted by an application restart',
|
|
5
|
+
shutdown: 'interrupted when the application closed',
|
|
6
|
+
host_lost: 'interrupted when the session host was lost',
|
|
7
|
+
crashed: 'interrupted when the provider process exited unexpectedly',
|
|
8
|
+
stalled: 'interrupted after no progress',
|
|
9
|
+
limit: 'stopped after reaching its time limit',
|
|
10
|
+
project_removed: 'interrupted because its project was removed',
|
|
11
|
+
});
|
|
12
|
+
function describeReason(phase, reason) {
|
|
13
|
+
return (reason && REASONS[reason]) ?? (phase === 'interrupted' ? 'interrupted' : 'stopped');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Build the one-time notice prefixed to the next user input after work was
|
|
17
|
+
* interrupted. It names what happened and tells the agent not to relaunch it on
|
|
18
|
+
* its own — providers otherwise "resume" orphaned work silently (spike finding).
|
|
19
|
+
* Returns null when there is nothing to report.
|
|
20
|
+
*/
|
|
21
|
+
export function buildInterruptionNotice(state) {
|
|
22
|
+
const { subagentIds, inputIds } = state.pendingInterruptions;
|
|
23
|
+
if (subagentIds.length === 0 && inputIds.length === 0)
|
|
24
|
+
return null;
|
|
25
|
+
const lines = ['[Specrails session notice]'];
|
|
26
|
+
if (subagentIds.length > 0) {
|
|
27
|
+
lines.push('These sub-agents did not finish:');
|
|
28
|
+
for (const id of subagentIds) {
|
|
29
|
+
const node = state.subagents[id];
|
|
30
|
+
if (node)
|
|
31
|
+
lines.push(`- "${node.description}" (${node.agentType ?? node.kind}) — ${describeReason(node.phase, node.reason)}.`);
|
|
32
|
+
}
|
|
33
|
+
lines.push('Do not relaunch or resume them unless the user explicitly asks. If their results matter for the next step, say so and ask.');
|
|
34
|
+
}
|
|
35
|
+
if (inputIds.length > 0) {
|
|
36
|
+
lines.push(`${inputIds.length === 1 ? 'One earlier user message' : `${inputIds.length} earlier user messages`} may not have reached you before the interruption; the user can resend if needed.`);
|
|
37
|
+
}
|
|
38
|
+
return { text: lines.join('\n'), subagentIds: [...subagentIds], inputIds: [...inputIds] };
|
|
39
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DriverDescriptor, McpServerSpec, SessionLimits, SessionPolicy, SubagentRuntime } from './types.js';
|
|
2
|
+
/** Defaults chosen from the provider spike and Desktop's previous mission limits. */
|
|
3
|
+
export declare const DEFAULT_LIMITS: SessionLimits;
|
|
4
|
+
/** What a host may send in `session.open` / `session.update`. Only `subagents` is mandatory. */
|
|
5
|
+
export interface SessionPolicyInput {
|
|
6
|
+
subagents: SessionPolicy['subagents'];
|
|
7
|
+
onSubagentsSettled?: SessionPolicy['onSubagentsSettled'];
|
|
8
|
+
tools?: SessionPolicy['tools'];
|
|
9
|
+
permissions?: SessionPolicy['permissions'];
|
|
10
|
+
mcp?: {
|
|
11
|
+
servers?: McpServerSpec[];
|
|
12
|
+
inheritUserScope?: boolean;
|
|
13
|
+
};
|
|
14
|
+
limits?: Partial<SessionLimits>;
|
|
15
|
+
subagentRuntime?: {
|
|
16
|
+
mode: 'native';
|
|
17
|
+
model?: string;
|
|
18
|
+
effort?: string;
|
|
19
|
+
} | {
|
|
20
|
+
mode: 'delegated';
|
|
21
|
+
driver: string;
|
|
22
|
+
model: string;
|
|
23
|
+
effort?: string;
|
|
24
|
+
maxConcurrent?: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare const DEFAULT_MAX_DELEGATED = 4;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a host policy against a driver's declared capabilities. The result is
|
|
30
|
+
* fully specified; anything the driver cannot enforce is refused, never ignored.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolvePolicy(input: SessionPolicyInput, driver: DriverDescriptor): SessionPolicy;
|
|
33
|
+
/** True when the provider's own sub-agents may run (enabled and not delegated to Core). */
|
|
34
|
+
export declare function nativeSubagentsAllowed(policy: SessionPolicy): boolean;
|
|
35
|
+
/** True when Core launches this session's sub-agents itself. */
|
|
36
|
+
export declare function delegatedSubagents(policy: SessionPolicy): policy is SessionPolicy & {
|
|
37
|
+
subagentRuntime: Extract<SubagentRuntime, {
|
|
38
|
+
mode: 'delegated';
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
/** True when the effective native sub-agent behaviour of the session is "enabled". */
|
|
42
|
+
export declare function subagentsActive(policy: SessionPolicy, driver: DriverDescriptor): boolean;
|