@omercnet/paseo-omp 0.4.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +8 -8
  2. package/client/external-url.ts +2 -11
  3. package/client/omp-config-styles.ts +284 -0
  4. package/client/omp-config-surface.tsx +16 -286
  5. package/client/omp-routing-editor.tsx +1 -3
  6. package/client/provider-launch-settings.tsx +118 -0
  7. package/index.client.tsx +21 -2
  8. package/index.server.ts +10 -0
  9. package/package.json +14 -9
  10. package/paseo-plugin.json +2 -1
  11. package/scripts/prepare-dependencies.mjs +44 -11
  12. package/server/omp-models.ts +2 -6
  13. package/server/package-version.ts +1 -1
  14. package/server/protocol-violation-diagnostics.ts +1 -1
  15. package/server/provider/catalog.ts +2 -1
  16. package/server/provider/config-normalization.ts +19 -3
  17. package/server/provider/connection.ts +30 -0
  18. package/server/provider/host-tools.ts +2 -2
  19. package/server/provider/mcp-transport.ts +9 -2
  20. package/server/provider/omp-rpc-environment.ts +368 -0
  21. package/server/provider/omp-rpc-process.ts +119 -0
  22. package/server/provider/omp-rpc-protocol.ts +1949 -0
  23. package/server/provider/omp-rpc-transport.ts +1373 -0
  24. package/server/provider/omp-rpc-values.ts +31 -0
  25. package/server/provider/omp-rpc.ts +267 -3311
  26. package/server/provider/profile-providers.ts +2 -1
  27. package/server/provider/prompt-payload.ts +253 -0
  28. package/server/provider/registration.ts +10 -2
  29. package/server/provider/session-compaction.ts +187 -0
  30. package/server/provider/session-descriptors.ts +52 -18
  31. package/server/provider/session-events.ts +42 -0
  32. package/server/provider/session-permissions.ts +987 -0
  33. package/server/provider/session-terminal.ts +307 -0
  34. package/server/provider/session-usage.ts +234 -0
  35. package/server/provider/session.ts +145 -1853
  36. package/server/provider/settings.ts +8 -5
  37. package/server/provider/subsessions.ts +30 -9
  38. package/server/provider/timeline-projector.ts +140 -29
  39. package/shared/external-url.ts +0 -9
  40. package/shared/provider-launch-settings.ts +18 -0
@@ -0,0 +1,307 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { ProviderError } from "@getpaseo/plugin/server/provider";
3
+ import type { OmpMessage, OmpRpcEvent } from "./omp-rpc-protocol";
4
+
5
+ export type PendingUser = {
6
+ clientMessageId: string;
7
+ text: string;
8
+ accepted: boolean;
9
+ fallbackOnFinish: boolean;
10
+ bufferedEchoes: OmpMessage[];
11
+ };
12
+ type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
13
+
14
+ type TerminalCorrelation = {
15
+ policy: "initial-turn" | "ordered-legacy" | "native-command";
16
+ evidence: Map<TerminalCorrelationEvidence, number>;
17
+ };
18
+
19
+ export type TerminalCandidate = {
20
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>;
21
+ arrivalSequence: number;
22
+ confidence:
23
+ | "keyed"
24
+ | "initial-turn"
25
+ | "ordered-legacy"
26
+ | "native-command"
27
+ | "interrupted"
28
+ | "ambiguous";
29
+ };
30
+
31
+ export type ActiveTurn = {
32
+ turnId: string;
33
+ clientMessageId: string;
34
+ generation: number;
35
+ promptResultEmitted: boolean;
36
+ started: boolean;
37
+ terminal: boolean;
38
+ interrupted: boolean;
39
+ starting: boolean;
40
+ nativeActivity: boolean;
41
+ userEchoObserved: boolean;
42
+ localOnlyDisabled: boolean;
43
+ localOnlyEligible: boolean;
44
+ awaitingPermissionEvidence: boolean;
45
+ activitySequence: number;
46
+ acknowledged: boolean;
47
+ terminalCorrelation: TerminalCorrelation;
48
+ replayingBufferedEvents: boolean;
49
+ agentInvoked?: boolean;
50
+ nativeRequestId?: string;
51
+ promptAcceptedEventIndex?: number;
52
+ localOnlyTimer?: unknown;
53
+ usagePollTimer?: unknown;
54
+ usagePoll?: Promise<void>;
55
+ usageSampleFloor: number;
56
+ manualCompaction: boolean;
57
+ manualCompactionPending: boolean;
58
+ manualCompactionDeadlineTimer?: unknown;
59
+ agentEndPending: boolean;
60
+ agentEndRetryTimer?: unknown;
61
+ agentEndDeadlineTimer?: unknown;
62
+ agentEndCheck?: Promise<void>;
63
+ ambiguousTerminalTimer?: unknown;
64
+ terminalizing: boolean;
65
+ terminalization?: Promise<void>;
66
+ terminalOutcome?: TurnOutcome;
67
+ operationalTerminalStage?: "failed" | "unresolved";
68
+ terminalWake?: VoidDeferred;
69
+ steerReady: VoidDeferred;
70
+ steersInFlight: number;
71
+ deferredAgentEnd?: TerminalCandidate;
72
+ activeToolCallIds: Set<string>;
73
+ bufferedEvents: OmpRpcEvent[];
74
+ pendingUsers: PendingUser[];
75
+ userEchoes: OmpMessage[];
76
+ userCorrelationActive: boolean;
77
+ userLookups: Set<Promise<void>>;
78
+ completedMessageCount: number;
79
+ streamedMessageEntryIds: string[];
80
+ streamedMessageIdentityComplete: boolean;
81
+ lastCompletedAssistantOutcome?: AgentEndOutcome;
82
+ lastCompletedAssistantEntryId?: string;
83
+ };
84
+
85
+ export type VoidDeferred = {
86
+ promise: Promise<void>;
87
+ resolve(value?: void | PromiseLike<void>): void;
88
+ reject(reason?: unknown): void;
89
+ };
90
+
91
+ export type TurnOutcome = {
92
+ state: "completed" | "failed" | "canceled";
93
+ error?: ProviderError;
94
+ usageSampled: boolean;
95
+ };
96
+
97
+ export function nativeEntryId(message: OmpMessage): string | undefined {
98
+ return message.entryId;
99
+ }
100
+
101
+ export type AgentEndOutcome = "completed" | "failed" | "canceled";
102
+ type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
103
+
104
+ export function assistantTerminalOutcome(
105
+ message: Extract<OmpMessage, { role: "assistant" }>,
106
+ ): AgentEndOutcome {
107
+ const stopReason = message.stopReason?.toLowerCase();
108
+ if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
109
+ return "canceled";
110
+ }
111
+ return stopReason === "error" || message.errorMessage ? "failed" : "completed";
112
+ }
113
+
114
+ function lastAssistantStatus(
115
+ messages: readonly OmpMessage[],
116
+ startIndex = 0,
117
+ ): AssistantTerminalStatus {
118
+ for (let index = messages.length - 1; index >= startIndex; index -= 1) {
119
+ const message = messages[index];
120
+ if (message?.role !== "assistant") continue;
121
+ return assistantTerminalOutcome(message);
122
+ }
123
+ return "unavailable";
124
+ }
125
+
126
+ export function terminalOutcome(
127
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
128
+ turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
129
+ ): AgentEndOutcome | undefined {
130
+ const messages = event.messages;
131
+ if (
132
+ messages !== undefined &&
133
+ (event.messageCount === undefined || messages.length >= event.messageCount)
134
+ ) {
135
+ const status = lastAssistantStatus(messages);
136
+ return status === "unavailable" ? "completed" : status;
137
+ }
138
+ if (event.messageCount === 0) return "completed";
139
+ if (
140
+ turn.lastCompletedAssistantOutcome !== undefined &&
141
+ (event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
142
+ ) {
143
+ return turn.lastCompletedAssistantOutcome;
144
+ }
145
+ return undefined;
146
+ }
147
+
148
+ export function historyTerminalOutcome(
149
+ messages: readonly OmpMessage[],
150
+ declaredCount: number,
151
+ turn: Pick<
152
+ ActiveTurn,
153
+ | "streamedMessageEntryIds"
154
+ | "streamedMessageIdentityComplete"
155
+ | "lastCompletedAssistantOutcome"
156
+ | "lastCompletedAssistantEntryId"
157
+ >,
158
+ retainedMessages: readonly OmpMessage[],
159
+ ): AgentEndOutcome | undefined {
160
+ if (
161
+ messages.length < declaredCount ||
162
+ !turn.streamedMessageIdentityComplete ||
163
+ turn.streamedMessageEntryIds.length === 0
164
+ ) {
165
+ return undefined;
166
+ }
167
+ const startIndex = messages.length - declaredCount;
168
+ let historyIndex = startIndex;
169
+ for (const entryId of turn.streamedMessageEntryIds) {
170
+ while (historyIndex < messages.length) {
171
+ const historyMessage = messages[historyIndex];
172
+ if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
173
+ historyIndex += 1;
174
+ }
175
+ if (historyIndex >= messages.length) return undefined;
176
+ const correlated = messages[historyIndex];
177
+ if (
178
+ entryId === turn.lastCompletedAssistantEntryId &&
179
+ (correlated?.role !== "assistant" ||
180
+ assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
181
+ )
182
+ return undefined;
183
+ historyIndex += 1;
184
+ }
185
+ historyIndex = startIndex;
186
+ for (const retained of retainedMessages) {
187
+ const entryId = nativeEntryId(retained);
188
+ if (!entryId) return undefined;
189
+ while (historyIndex < messages.length) {
190
+ const historyMessage = messages[historyIndex];
191
+ if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
192
+ historyIndex += 1;
193
+ }
194
+ if (historyIndex >= messages.length) return undefined;
195
+ const correlated = messages[historyIndex];
196
+ if (!correlated || correlated.role !== retained.role) return undefined;
197
+ if (
198
+ retained.role === "assistant" &&
199
+ correlated.role === "assistant" &&
200
+ assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
201
+ )
202
+ return undefined;
203
+ historyIndex += 1;
204
+ }
205
+ const status = lastAssistantStatus(messages, startIndex);
206
+ return status === "unavailable" ? undefined : status;
207
+ }
208
+
209
+ export function unknownTerminalOutcomeError(
210
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
211
+ turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
212
+ ): string {
213
+ const retainedMessages = event.messages?.length ?? 0;
214
+ const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
215
+ const lastStatus =
216
+ retainedStatus === "unavailable"
217
+ ? (turn.lastCompletedAssistantOutcome ?? "unavailable")
218
+ : retainedStatus;
219
+ return (
220
+ "OMP agent_end omitted terminal messages; outcome is unknown " +
221
+ `(declaredCount=${event.messageCount ?? "unavailable"}, ` +
222
+ `observedCount=${turn.completedMessageCount}, ` +
223
+ `retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
224
+ );
225
+ }
226
+
227
+ export function createActiveTurn(
228
+ clientMessageId: string,
229
+ text: string,
230
+ generation: number,
231
+ terminalPolicy: TerminalCorrelation["policy"],
232
+ manualCompaction = false,
233
+ ): ActiveTurn {
234
+ return {
235
+ turnId: randomUUID(),
236
+ clientMessageId,
237
+ agentInvoked: undefined,
238
+ generation,
239
+ promptResultEmitted: false,
240
+ started: false,
241
+ terminal: false,
242
+ awaitingPermissionEvidence: false,
243
+ interrupted: false,
244
+ starting: true,
245
+ nativeActivity: false,
246
+ userEchoObserved: false,
247
+ localOnlyDisabled: false,
248
+ localOnlyEligible: false,
249
+ usageSampleFloor: 0,
250
+ agentEndPending: false,
251
+ terminalizing: false,
252
+ manualCompactionPending: manualCompaction,
253
+ manualCompaction,
254
+ activitySequence: 0,
255
+ acknowledged: false,
256
+ terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
257
+ replayingBufferedEvents: false,
258
+ steersInFlight: 0,
259
+ activeToolCallIds: new Set(),
260
+ steerReady: Promise.withResolvers<void>(),
261
+ userCorrelationActive: false,
262
+ userLookups: new Set(),
263
+ userEchoes: [],
264
+ completedMessageCount: 0,
265
+ bufferedEvents: [],
266
+ streamedMessageEntryIds: [],
267
+ streamedMessageIdentityComplete: true,
268
+ pendingUsers: [
269
+ {
270
+ clientMessageId,
271
+ text,
272
+ accepted: true,
273
+ fallbackOnFinish: true,
274
+ bufferedEchoes: [],
275
+ },
276
+ ],
277
+ };
278
+ }
279
+ export function classifyTerminalCandidate(
280
+ turn: ActiveTurn,
281
+ event: Extract<OmpRpcEvent, { type: "agent_end" }>,
282
+ arrivalSequence = turn.activitySequence,
283
+ ): TerminalCandidate {
284
+ if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
285
+ if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
286
+ if (turn.terminalCorrelation.policy === "initial-turn") {
287
+ return { event, confidence: "initial-turn", arrivalSequence };
288
+ }
289
+ const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
290
+ if (
291
+ turn.terminalCorrelation.policy === "native-command" &&
292
+ assistantSequence !== undefined &&
293
+ assistantSequence <= arrivalSequence
294
+ ) {
295
+ return { event, confidence: "native-command", arrivalSequence };
296
+ }
297
+ const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
298
+ if (
299
+ userSequence !== undefined &&
300
+ assistantSequence !== undefined &&
301
+ userSequence < assistantSequence &&
302
+ assistantSequence <= arrivalSequence
303
+ ) {
304
+ return { event, confidence: "ordered-legacy", arrivalSequence };
305
+ }
306
+ return { event, confidence: "ambiguous", arrivalSequence };
307
+ }
@@ -0,0 +1,234 @@
1
+ import type { ProviderEvent, ProviderUsage } from "@getpaseo/plugin/server/provider";
2
+ import type { OmpRuntimeSession } from "./omp-rpc";
3
+ import type { OmpSessionState, OmpSessionStats } from "./omp-rpc-protocol";
4
+ import type { ActiveTurn } from "./session-terminal";
5
+ import type { OmpTimelineScheduler } from "./timeline-projector";
6
+
7
+ const USAGE_POLL_MS = 1_000;
8
+
9
+ interface UsageContext {
10
+ closed: boolean;
11
+ runtimeDead: boolean;
12
+ generation: number;
13
+ runtime: OmpRuntimeSession;
14
+ activeTurn: ActiveTurn | null;
15
+ }
16
+
17
+ type Emit = (event: ProviderEvent) => void;
18
+ type ReadContext = () => UsageContext;
19
+ type IsPollable = (turn: ActiveTurn) => boolean;
20
+
21
+ interface UsageSample {
22
+ turn: ActiveTurn;
23
+ generation: number;
24
+ runtime: OmpRuntimeSession;
25
+ epoch: number;
26
+ sequence: number;
27
+ promise: Promise<OmpSessionState | undefined>;
28
+ }
29
+
30
+ function usageFrom(
31
+ state: OmpSessionState | undefined,
32
+ stats: OmpSessionStats | undefined,
33
+ ): ProviderUsage | undefined {
34
+ if (!state?.contextUsage && !stats) return undefined;
35
+ const stateContext = state?.contextUsage;
36
+ const statsContext = stats?.contextUsage;
37
+ const modelCapacity = state?.model?.contextWindow;
38
+ const inputTokens = stats?.tokens?.input;
39
+ const cachedInputTokens = stats?.tokens?.cacheRead;
40
+ const outputTokens = stats?.tokens?.output;
41
+ const totalCostUsd = stats?.cost;
42
+ const contextTokens =
43
+ typeof stateContext?.tokens === "number"
44
+ ? stateContext.tokens
45
+ : typeof statsContext?.tokens === "number"
46
+ ? statsContext.tokens
47
+ : undefined;
48
+ const contextWindow =
49
+ typeof stateContext?.contextWindow === "number" && stateContext.contextWindow > 0
50
+ ? stateContext.contextWindow
51
+ : typeof statsContext?.contextWindow === "number" && statsContext.contextWindow > 0
52
+ ? statsContext.contextWindow
53
+ : typeof modelCapacity === "number" && modelCapacity > 0
54
+ ? modelCapacity
55
+ : undefined;
56
+ const usage: ProviderUsage = {
57
+ ...(typeof inputTokens === "number" ? { inputTokens } : {}),
58
+ ...(typeof cachedInputTokens === "number" ? { cachedInputTokens } : {}),
59
+ ...(typeof outputTokens === "number" ? { outputTokens } : {}),
60
+ ...(typeof totalCostUsd === "number" ? { totalCostUsd } : {}),
61
+ ...(contextTokens !== undefined ? { contextWindowUsedTokens: contextTokens } : {}),
62
+ ...(contextWindow !== undefined ? { contextWindowMaxTokens: contextWindow } : {}),
63
+ };
64
+ return Object.keys(usage).length > 0 ? usage : undefined;
65
+ }
66
+
67
+ export class OmpSessionUsage {
68
+ private epoch = 0;
69
+ private sequence = 0;
70
+ private sample: UsageSample | null = null;
71
+ private lastUsage: ProviderUsage | null = null;
72
+
73
+ constructor(
74
+ private readonly sessionId: string,
75
+ private readonly emit: Emit,
76
+ private readonly scheduler: OmpTimelineScheduler,
77
+ private readonly readContext: ReadContext,
78
+ private readonly isPollable: IsPollable,
79
+ ) {}
80
+
81
+ get latest(): ProviderUsage | null {
82
+ return this.lastUsage;
83
+ }
84
+
85
+ private ownsSample(turn: ActiveTurn, generation: number, runtime: OmpRuntimeSession): boolean {
86
+ const context = this.readContext();
87
+ return (
88
+ !context.closed &&
89
+ !context.runtimeDead &&
90
+ !turn.terminal &&
91
+ generation === context.generation &&
92
+ runtime === context.runtime &&
93
+ context.activeTurn === turn
94
+ );
95
+ }
96
+
97
+ publishSnapshot(
98
+ turn: ActiveTurn,
99
+ minimumEpoch = this.epoch,
100
+ minimumSequence = turn.usageSampleFloor,
101
+ ): Promise<OmpSessionState | undefined> {
102
+ const { generation, runtime } = this.readContext();
103
+ if (!this.ownsSample(turn, generation, runtime)) return Promise.resolve(undefined);
104
+ const current = this.sample;
105
+ if (current) {
106
+ if (
107
+ current.turn === turn &&
108
+ current.generation === generation &&
109
+ current.runtime === runtime &&
110
+ current.epoch >= minimumEpoch &&
111
+ current.sequence >= minimumSequence
112
+ ) {
113
+ return current.promise;
114
+ }
115
+ return current.promise.then(() => {
116
+ if (!this.ownsSample(turn, generation, runtime)) return undefined;
117
+ return this.publishSnapshot(turn, minimumEpoch, minimumSequence);
118
+ });
119
+ }
120
+ const epoch = this.epoch;
121
+ const sequence = ++this.sequence;
122
+ const promise = Promise.allSettled([runtime.getState(), runtime.getSessionStats()]).then(
123
+ ([stateResult, statsResult]) => {
124
+ if (
125
+ !this.ownsSample(turn, generation, runtime) ||
126
+ epoch !== this.epoch ||
127
+ epoch < minimumEpoch ||
128
+ sequence < turn.usageSampleFloor ||
129
+ sequence < minimumSequence
130
+ ) {
131
+ return undefined;
132
+ }
133
+ const state = stateResult.status === "fulfilled" ? stateResult.value : undefined;
134
+ const stats = statsResult.status === "fulfilled" ? statsResult.value : undefined;
135
+ const usage = usageFrom(state, stats);
136
+ if (usage) {
137
+ this.lastUsage = usage;
138
+ this.emit({
139
+ type: "session.usage",
140
+ sessionId: this.sessionId,
141
+ turnId: turn.turnId,
142
+ usage,
143
+ });
144
+ }
145
+ return state;
146
+ },
147
+ );
148
+ this.sample = { turn, generation, runtime, epoch, sequence, promise };
149
+ void promise.finally(() => {
150
+ if (this.sample?.promise === promise) this.sample = null;
151
+ });
152
+ return promise;
153
+ }
154
+
155
+ async boundedSnapshot(turn: ActiveTurn, timeoutMs: number): Promise<OmpSessionState | undefined> {
156
+ const timeout = Promise.withResolvers<undefined>();
157
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
158
+ try {
159
+ return await Promise.race([this.publishSnapshot(turn), timeout.promise]);
160
+ } finally {
161
+ this.scheduler.clear(timer);
162
+ }
163
+ }
164
+
165
+ async boundedTerminalState(
166
+ turn: ActiveTurn,
167
+ timeoutMs: number,
168
+ ): Promise<OmpSessionState | undefined> {
169
+ const { generation, runtime } = this.readContext();
170
+ const timeout = Promise.withResolvers<undefined>();
171
+ const timer = this.scheduler.set(() => timeout.resolve(undefined), timeoutMs);
172
+ try {
173
+ const state = await Promise.race([runtime.getState(), timeout.promise]);
174
+ return this.ownsSample(turn, generation, runtime) ? state : undefined;
175
+ } catch {
176
+ return undefined;
177
+ } finally {
178
+ this.scheduler.clear(timer);
179
+ }
180
+ }
181
+
182
+ schedulePoll(turn: ActiveTurn, delayMs = USAGE_POLL_MS): void {
183
+ if (!this.isPollable(turn) || turn.usagePollTimer !== undefined) return;
184
+ turn.usagePollTimer = this.scheduler.set(() => {
185
+ turn.usagePollTimer = undefined;
186
+ this.poll(turn);
187
+ }, delayMs);
188
+ }
189
+
190
+ poll(turn: ActiveTurn): void {
191
+ if (!this.isPollable(turn) || turn.usagePoll) return;
192
+ const poll = this.publishSnapshot(turn).then(() => undefined);
193
+ turn.usagePoll = poll;
194
+ void poll.finally(() => {
195
+ if (turn.usagePoll === poll) turn.usagePoll = undefined;
196
+ this.schedulePoll(turn);
197
+ });
198
+ }
199
+
200
+ stopPoll(turn: ActiveTurn): void {
201
+ if (turn.usagePollTimer === undefined) return;
202
+ this.scheduler.clear(turn.usagePollTimer);
203
+ turn.usagePollTimer = undefined;
204
+ }
205
+
206
+ invalidateBeforeNextSample(turn: ActiveTurn): void {
207
+ turn.usageSampleFloor = this.sequence + 1;
208
+ if (this.sample?.turn === turn && this.sample.sequence < turn.usageSampleFloor) {
209
+ this.sample = null;
210
+ }
211
+ }
212
+
213
+ completeCompaction(): void {
214
+ this.epoch += 1;
215
+ const staleSample = this.sample;
216
+ if (staleSample && staleSample.epoch < this.epoch) {
217
+ this.sample = null;
218
+ staleSample.turn.usagePoll = undefined;
219
+ }
220
+ this.lastUsage = null;
221
+ }
222
+ clearLatest(): void {
223
+ this.lastUsage = null;
224
+ }
225
+
226
+ clearSample(turn: ActiveTurn): void {
227
+ if (this.sample?.turn === turn) this.sample = null;
228
+ }
229
+
230
+ reset(): void {
231
+ this.sample = null;
232
+ this.lastUsage = null;
233
+ }
234
+ }