tinker-agent 2.8.0 → 2.10.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/CHANGELOG.md +79 -1
- package/README.md +81 -11
- package/package.json +5 -3
- package/src/agent/runtime-context-capabilities.ts +19 -0
- package/src/agent/runtime-context-events.ts +127 -0
- package/src/agent/runtime-context-maintenance.ts +780 -0
- package/src/agent/runtime-hosted-session.ts +443 -0
- package/src/agent/runtime-interactions.ts +291 -0
- package/src/agent/runtime-prompt-scheduler.ts +182 -0
- package/src/agent/runtime-session-contracts.ts +317 -0
- package/src/agent/runtime-session.ts +250 -2130
- package/src/agent/runtime-skills.ts +544 -0
- package/src/cli/command-line.ts +26 -2
- package/src/cli/connect-runner.tsx +26 -0
- package/src/cli/main.ts +26 -0
- package/src/cli/output.ts +1 -1
- package/src/cli/public-cli-contract.ts +18 -0
- package/src/cli/public-config-contract.ts +1 -1
- package/src/cli/runner-dependencies.ts +6 -5
- package/src/cli/serve-runner.ts +45 -0
- package/src/cli/serve-runtime.ts +100 -0
- package/src/context/context-automation-policy.ts +12 -118
- package/src/context/context-swap-renderer.ts +14 -0
- package/src/events/types.ts +12 -0
- package/src/memory/memory-get-tool.ts +1 -1
- package/src/observation/observation-builder.ts +128 -48
- package/src/remote/client.ts +350 -0
- package/src/remote/config.ts +95 -0
- package/src/remote/http-server.ts +240 -0
- package/src/remote/protocol.ts +228 -0
- package/src/remote/service-store.ts +175 -0
- package/src/remote/service.ts +219 -0
- package/src/remote/sync-hub.ts +95 -0
- package/src/session/remote-history-reader.ts +143 -0
- package/src/session/resume-projection.ts +47 -21
- package/src/session/session-history-access.ts +238 -0
- package/src/session/session-store-context-readers.ts +183 -0
- package/src/session/session-store-ledger-writer.ts +315 -0
- package/src/session/session-store-record-writer.ts +318 -0
- package/src/session/session-store-recovery.ts +225 -0
- package/src/session/session-store-revisions.ts +1004 -0
- package/src/session/session-store-sql.ts +40 -0
- package/src/session/session-store-validation.ts +657 -0
- package/src/session/session-store.ts +756 -3186
- package/src/tools/bash-task.ts +46 -18
- package/src/tools/bash.ts +44 -2
- package/src/tools/glob.ts +107 -19
- package/src/tools/grep-output.ts +130 -0
- package/src/tools/grep-pagination.ts +73 -0
- package/src/tools/grep-path.ts +11 -0
- package/src/tools/grep-snippets.ts +111 -0
- package/src/tools/grep.ts +139 -154
- package/src/tools/read.ts +0 -9
- package/src/tools/recall.ts +106 -50
- package/src/tools/registry.ts +4 -6
- package/src/tools/ripgrep.ts +19 -26
- package/src/tools/shell-process.ts +30 -4
- package/src/tools/task-output-range.ts +146 -0
- package/src/tools/task-output-tool.ts +35 -5
- package/src/tools/task-output.ts +35 -0
- package/src/tools/task-stop.ts +2 -1
- package/src/tools/task-tool-args.ts +34 -0
- package/src/tools/terminal-screen.ts +11 -2
- package/src/tools/types.ts +39 -2
- package/src/tui/event-store.ts +23 -5
- package/src/tui/remote-app.tsx +210 -0
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { RuntimeSession } from "./runtime-session";
|
|
3
|
+
import type {
|
|
4
|
+
AssistantTextDeltaSink,
|
|
5
|
+
AssistantTextDeltaUpdate,
|
|
6
|
+
} from "./assistant-text-delta";
|
|
7
|
+
import type { AgentEvent } from "../events/types";
|
|
8
|
+
import type { EventSink } from "../events/event-sink";
|
|
9
|
+
import {
|
|
10
|
+
RemoteHistoryReader,
|
|
11
|
+
type RemoteHistoryPage,
|
|
12
|
+
} from "../session/remote-history-reader";
|
|
13
|
+
import { parseSessionId } from "../ids/runtime-id";
|
|
14
|
+
import {
|
|
15
|
+
RemoteError,
|
|
16
|
+
type RemoteActivity,
|
|
17
|
+
type RemoteOperationInput,
|
|
18
|
+
type OperationReceipt,
|
|
19
|
+
type RemoteView,
|
|
20
|
+
} from "../remote/protocol";
|
|
21
|
+
import { type ManagedSessionRecord, RemoteServiceStore } from "../remote/service-store";
|
|
22
|
+
import { RemoteSyncHub } from "../remote/sync-hub";
|
|
23
|
+
|
|
24
|
+
export type HostedRuntimeFactory = (input: {
|
|
25
|
+
record: ManagedSessionRecord;
|
|
26
|
+
sink: EventSink & AssistantTextDeltaSink;
|
|
27
|
+
}) => Promise<{
|
|
28
|
+
runtime: RuntimeSession;
|
|
29
|
+
databasePath: string;
|
|
30
|
+
modelName: string;
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
export class HostedSession implements EventSink, AssistantTextDeltaSink {
|
|
34
|
+
readonly name = "remote-view";
|
|
35
|
+
readonly hub: RemoteSyncHub;
|
|
36
|
+
private runtime?: RuntimeSession;
|
|
37
|
+
private reader?: RemoteHistoryReader;
|
|
38
|
+
private opening?: Promise<void>;
|
|
39
|
+
private readonly queue: OperationReceipt[] = [];
|
|
40
|
+
private active?: {
|
|
41
|
+
receipt: OperationReceipt;
|
|
42
|
+
controller: AbortController;
|
|
43
|
+
completion?: Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
private activity: Omit<RemoteActivity, "session" | "operations"> = {
|
|
46
|
+
status: "idle",
|
|
47
|
+
tools: [],
|
|
48
|
+
};
|
|
49
|
+
private lastOrdinal = 0;
|
|
50
|
+
private streamTimer?: ReturnType<typeof setTimeout>;
|
|
51
|
+
private stopping = false;
|
|
52
|
+
private unsubscribers: (() => void)[] = [];
|
|
53
|
+
private controlTail: Promise<void> = Promise.resolve();
|
|
54
|
+
|
|
55
|
+
constructor(
|
|
56
|
+
private record: ManagedSessionRecord,
|
|
57
|
+
private readonly store: RemoteServiceStore,
|
|
58
|
+
epoch: string,
|
|
59
|
+
private readonly factory: HostedRuntimeFactory,
|
|
60
|
+
) {
|
|
61
|
+
this.hub = new RemoteSyncHub(epoch, () => this.view());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
open(): Promise<void> {
|
|
65
|
+
return (this.opening ??= this.initialize());
|
|
66
|
+
}
|
|
67
|
+
private async initialize(): Promise<void> {
|
|
68
|
+
try {
|
|
69
|
+
const opened = await this.factory({ record: this.record, sink: this });
|
|
70
|
+
this.runtime = opened.runtime;
|
|
71
|
+
this.reader = new RemoteHistoryReader(
|
|
72
|
+
opened.databasePath,
|
|
73
|
+
parseSessionId(this.record.id),
|
|
74
|
+
this.record.workspacePath,
|
|
75
|
+
);
|
|
76
|
+
this.record = {
|
|
77
|
+
...this.record,
|
|
78
|
+
initialized: true,
|
|
79
|
+
modelName: opened.modelName,
|
|
80
|
+
status: "idle",
|
|
81
|
+
};
|
|
82
|
+
this.store.saveSession(this.record);
|
|
83
|
+
const latest = this.reader.latestTurn();
|
|
84
|
+
if (latest && latest.status !== "open") {
|
|
85
|
+
this.activity.status = latest.status as RemoteActivity["status"];
|
|
86
|
+
this.activity.error = latest.error;
|
|
87
|
+
}
|
|
88
|
+
// Canonical terminal status wins over a crash between turn commit and receipt update.
|
|
89
|
+
for (const receipt of this.store.operations(this.record.id)) {
|
|
90
|
+
const status = receipt.turnId
|
|
91
|
+
? this.reader.turnStatus(receipt.turnId)
|
|
92
|
+
: undefined;
|
|
93
|
+
if (receipt.kind === "prompt" && status && status !== "open") {
|
|
94
|
+
this.store.update({
|
|
95
|
+
...receipt,
|
|
96
|
+
status: status as OperationReceipt["status"],
|
|
97
|
+
error: status === "interrupted" ? receipt.error : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const last = this.store
|
|
102
|
+
.operations(this.record.id)
|
|
103
|
+
.filter((op) => op.kind === "prompt")
|
|
104
|
+
.at(-1);
|
|
105
|
+
if (last?.status === "interrupted") {
|
|
106
|
+
this.activity.status = "interrupted";
|
|
107
|
+
this.activity.error = last.error;
|
|
108
|
+
}
|
|
109
|
+
this.unsubscribers = [
|
|
110
|
+
this.runtime.subscribeAskUser(() => this.updateInteraction()),
|
|
111
|
+
this.runtime.subscribeBashGuard(() => this.updateInteraction()),
|
|
112
|
+
];
|
|
113
|
+
this.publish();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
this.activity.status = "failed";
|
|
116
|
+
this.activity.error = errorMessage(error);
|
|
117
|
+
if (this.runtime)
|
|
118
|
+
await this.runtime
|
|
119
|
+
.dispose({ type: "runner_failed", error: errorMessage(error) })
|
|
120
|
+
.catch(() => undefined);
|
|
121
|
+
this.publish();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
view(): RemoteView {
|
|
127
|
+
return { ...this.readActivity(), history: this.history() };
|
|
128
|
+
}
|
|
129
|
+
history(before?: number, limit?: number): RemoteHistoryPage {
|
|
130
|
+
return this.reader?.page(before, limit) ?? { messages: [], hasMore: false };
|
|
131
|
+
}
|
|
132
|
+
private readActivity(): RemoteActivity {
|
|
133
|
+
const { id, workspaceId, title, modelName, owner, updatedAt } = this.record;
|
|
134
|
+
const session = { id, workspaceId, title, modelName, owner, updatedAt };
|
|
135
|
+
return structuredClone({
|
|
136
|
+
...this.activity,
|
|
137
|
+
session: { ...session, status: this.activity.status },
|
|
138
|
+
operations: this.store.operations(this.record.id),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
validate(input: RemoteOperationInput): void {
|
|
143
|
+
if (this.stopping)
|
|
144
|
+
throw new RemoteError(503, "SERVICE_STOPPING", "The local service is stopping.");
|
|
145
|
+
if (input.kind === "prompt" && this.queue.length >= 8)
|
|
146
|
+
throw new RemoteError(
|
|
147
|
+
409,
|
|
148
|
+
"QUEUE_FULL",
|
|
149
|
+
"This session already has eight queued requests.",
|
|
150
|
+
);
|
|
151
|
+
if (input.kind === "answer" || input.kind === "confirm") {
|
|
152
|
+
const pending = this.activity.interaction;
|
|
153
|
+
if (
|
|
154
|
+
!pending ||
|
|
155
|
+
pending.id !== input.interactionId ||
|
|
156
|
+
pending.kind !== (input.kind === "answer" ? "question" : "confirmation")
|
|
157
|
+
)
|
|
158
|
+
throw new RemoteError(
|
|
159
|
+
409,
|
|
160
|
+
"STALE_INTERACTION",
|
|
161
|
+
"This question or confirmation is no longer pending.",
|
|
162
|
+
);
|
|
163
|
+
if (
|
|
164
|
+
input.kind === "answer" &&
|
|
165
|
+
input.selectedIndex !== null &&
|
|
166
|
+
pending.kind === "question" &&
|
|
167
|
+
!pending.options[input.selectedIndex]
|
|
168
|
+
)
|
|
169
|
+
throw new RemoteError(400, "INVALID_ANSWER", "Answer index is out of range.");
|
|
170
|
+
}
|
|
171
|
+
if (input.kind === "stop") {
|
|
172
|
+
const target = this.store.get(input.targetRequestId);
|
|
173
|
+
if (target.sessionId !== this.record.id || target.kind !== "prompt")
|
|
174
|
+
throw new RemoteError(
|
|
175
|
+
409,
|
|
176
|
+
"INVALID_STOP_TARGET",
|
|
177
|
+
"Stop must target a prompt in this session.",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
enqueue(receipt: OperationReceipt): void {
|
|
183
|
+
this.queue.push(receipt);
|
|
184
|
+
if (!this.active) this.activity.status = "accepted";
|
|
185
|
+
if (this.record.title === "New session" && receipt.prompt) {
|
|
186
|
+
this.record = {
|
|
187
|
+
...this.record,
|
|
188
|
+
title: receipt.prompt.replace(/\s+/g, " ").slice(0, 100),
|
|
189
|
+
};
|
|
190
|
+
this.store.saveSession(this.record);
|
|
191
|
+
}
|
|
192
|
+
this.publish();
|
|
193
|
+
this.pump();
|
|
194
|
+
}
|
|
195
|
+
private pump(): void {
|
|
196
|
+
if (this.active || this.stopping) return;
|
|
197
|
+
const receipt = this.queue.shift();
|
|
198
|
+
if (!receipt) return;
|
|
199
|
+
const active = {
|
|
200
|
+
receipt,
|
|
201
|
+
controller: new AbortController(),
|
|
202
|
+
completion: undefined as Promise<void> | undefined,
|
|
203
|
+
};
|
|
204
|
+
this.active = active;
|
|
205
|
+
active.completion = this.execute(active);
|
|
206
|
+
}
|
|
207
|
+
private async execute(active: NonNullable<HostedSession["active"]>): Promise<void> {
|
|
208
|
+
try {
|
|
209
|
+
await this.open();
|
|
210
|
+
if (active.controller.signal.aborted) {
|
|
211
|
+
this.updateReceipt(active.receipt, { status: "cancelled" });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.activity = {
|
|
215
|
+
status: "running",
|
|
216
|
+
activeRequestId: active.receipt.requestId,
|
|
217
|
+
tools: [],
|
|
218
|
+
};
|
|
219
|
+
this.updateReceipt(active.receipt, { status: "running" });
|
|
220
|
+
this.publish();
|
|
221
|
+
const accepted = await this.runtime!.admitTurn({
|
|
222
|
+
userMessage: { role: "user", content: active.receipt.prompt! },
|
|
223
|
+
signal: active.controller.signal,
|
|
224
|
+
});
|
|
225
|
+
active.receipt = this.updateReceipt(active.receipt, {
|
|
226
|
+
status: "running",
|
|
227
|
+
turnId: accepted.turnId,
|
|
228
|
+
});
|
|
229
|
+
this.activity.activeTurnId = accepted.turnId;
|
|
230
|
+
this.publish();
|
|
231
|
+
const result = await accepted.completion;
|
|
232
|
+
this.updateReceipt(active.receipt, {
|
|
233
|
+
status: result.status,
|
|
234
|
+
...(result.status === "failed" ? { error: result.error } : {}),
|
|
235
|
+
});
|
|
236
|
+
this.activity.status = result.status;
|
|
237
|
+
if (result.status === "failed") this.activity.error = result.error;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const status = active.controller.signal.aborted ? "cancelled" : "failed";
|
|
240
|
+
this.updateReceipt(active.receipt, {
|
|
241
|
+
status,
|
|
242
|
+
error: errorMessage(error),
|
|
243
|
+
});
|
|
244
|
+
this.activity.status = status;
|
|
245
|
+
this.activity.error = errorMessage(error);
|
|
246
|
+
} finally {
|
|
247
|
+
this.activity.streaming = undefined;
|
|
248
|
+
this.activity.interaction = undefined;
|
|
249
|
+
this.activity.activeRequestId = undefined;
|
|
250
|
+
this.activity.activeTurnId = undefined;
|
|
251
|
+
for (const tool of this.activity.tools) {
|
|
252
|
+
if (tool.status === "running")
|
|
253
|
+
tool.status = this.activity.status === "cancelled" ? "cancelled" : "failed";
|
|
254
|
+
}
|
|
255
|
+
this.active = undefined;
|
|
256
|
+
this.publish(true);
|
|
257
|
+
this.pump();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
control(input: RemoteOperationInput, receipt: OperationReceipt): void {
|
|
262
|
+
// Separate from prompt execution: answering a wait cannot wait for that turn.
|
|
263
|
+
this.controlTail = this.controlTail.then(async () => {
|
|
264
|
+
try {
|
|
265
|
+
await this.open();
|
|
266
|
+
this.validate(input);
|
|
267
|
+
if (input.kind === "stop") {
|
|
268
|
+
if (this.active?.receipt.requestId === input.targetRequestId)
|
|
269
|
+
this.active.controller.abort();
|
|
270
|
+
const index = this.queue.findIndex(
|
|
271
|
+
(op) => op.requestId === input.targetRequestId,
|
|
272
|
+
);
|
|
273
|
+
if (index !== -1)
|
|
274
|
+
this.updateReceipt(this.queue.splice(index, 1)[0], {
|
|
275
|
+
status: "cancelled",
|
|
276
|
+
});
|
|
277
|
+
} else if (input.kind === "answer") {
|
|
278
|
+
await this.runtime!.resolveAskUser(
|
|
279
|
+
input.selectedIndex === null
|
|
280
|
+
? { outcome: "dismissed" }
|
|
281
|
+
: { outcome: "selected", selectedIndex: input.selectedIndex },
|
|
282
|
+
);
|
|
283
|
+
} else if (input.kind === "confirm") {
|
|
284
|
+
await this.runtime!.resolveBashConfirmation(input.decision);
|
|
285
|
+
}
|
|
286
|
+
this.updateReceipt(receipt, { status: "completed" });
|
|
287
|
+
} catch (error) {
|
|
288
|
+
this.updateReceipt(receipt, {
|
|
289
|
+
status: "failed",
|
|
290
|
+
error: errorMessage(error),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
this.publish();
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private updateReceipt(
|
|
298
|
+
receipt: OperationReceipt,
|
|
299
|
+
patch: Partial<OperationReceipt>,
|
|
300
|
+
): OperationReceipt {
|
|
301
|
+
const next = this.store.update({
|
|
302
|
+
...this.store.get(receipt.requestId),
|
|
303
|
+
...patch,
|
|
304
|
+
});
|
|
305
|
+
this.record = { ...this.record, updatedAt: next.updatedAt };
|
|
306
|
+
this.store.saveSession(this.record);
|
|
307
|
+
if (this.active?.receipt.requestId === receipt.requestId)
|
|
308
|
+
this.active.receipt = next;
|
|
309
|
+
return next;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private updateInteraction(): void {
|
|
313
|
+
const question = this.runtime?.askUser().pending;
|
|
314
|
+
const confirmation = this.runtime?.bashGuard().pending;
|
|
315
|
+
if (question || confirmation) {
|
|
316
|
+
if (!this.activity.interaction) {
|
|
317
|
+
this.activity.interaction = question
|
|
318
|
+
? { id: randomUUID(), kind: "question", ...question }
|
|
319
|
+
: { id: randomUUID(), kind: "confirmation", ...confirmation! };
|
|
320
|
+
}
|
|
321
|
+
this.activity.status = "waiting_input";
|
|
322
|
+
} else {
|
|
323
|
+
this.activity.interaction = undefined;
|
|
324
|
+
if (this.active) this.activity.status = "running";
|
|
325
|
+
}
|
|
326
|
+
if (this.active)
|
|
327
|
+
this.updateReceipt(this.active.receipt, {
|
|
328
|
+
status: this.activity.status as OperationReceipt["status"],
|
|
329
|
+
});
|
|
330
|
+
this.publish();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async append(event: AgentEvent): Promise<void> {
|
|
334
|
+
if (!this.reader) return;
|
|
335
|
+
switch (event.type) {
|
|
336
|
+
case "turn.started":
|
|
337
|
+
this.activity.activeTurnId = event.turnId;
|
|
338
|
+
if (this.active)
|
|
339
|
+
this.updateReceipt(this.active.receipt, { turnId: event.turnId });
|
|
340
|
+
break;
|
|
341
|
+
case "model.request.started":
|
|
342
|
+
this.activity.streaming = undefined;
|
|
343
|
+
break;
|
|
344
|
+
case "model.request.finished":
|
|
345
|
+
this.activity.streaming = undefined;
|
|
346
|
+
break;
|
|
347
|
+
case "tool.started":
|
|
348
|
+
this.activity.tools.push({
|
|
349
|
+
id: event.data.call.toolCallId,
|
|
350
|
+
name: event.data.call.name,
|
|
351
|
+
arguments: JSON.stringify(event.data.call.args),
|
|
352
|
+
status: "running",
|
|
353
|
+
});
|
|
354
|
+
this.activity.tools = this.activity.tools.slice(-100);
|
|
355
|
+
break;
|
|
356
|
+
case "tool.finished": {
|
|
357
|
+
const tool = this.activity.tools.find((tool) => tool.id === event.toolCallId);
|
|
358
|
+
if (tool) tool.status = event.data.ok ? "completed" : "failed";
|
|
359
|
+
break;
|
|
360
|
+
}
|
|
361
|
+
case "tool.observation": {
|
|
362
|
+
const tool = this.activity.tools.find((tool) => tool.id === event.toolCallId);
|
|
363
|
+
if (tool) tool.detail = event.data.observation.displayText;
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
case "turn.finished":
|
|
367
|
+
case "turn.failed":
|
|
368
|
+
case "turn.cancelled":
|
|
369
|
+
case "agent.iteration.finished":
|
|
370
|
+
case "turn.steering.applied":
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
this.publish();
|
|
376
|
+
}
|
|
377
|
+
updateAssistantTextDelta(update: AssistantTextDeltaUpdate): void {
|
|
378
|
+
const previous = this.activity.streaming;
|
|
379
|
+
this.activity.streaming = {
|
|
380
|
+
iterationId: update.iterationId,
|
|
381
|
+
attempt: update.attemptNumber,
|
|
382
|
+
text:
|
|
383
|
+
previous?.iterationId === update.iterationId &&
|
|
384
|
+
previous.attempt === update.attemptNumber
|
|
385
|
+
? previous.text + update.content
|
|
386
|
+
: update.content,
|
|
387
|
+
};
|
|
388
|
+
if (!this.streamTimer)
|
|
389
|
+
this.streamTimer = setTimeout(() => {
|
|
390
|
+
this.streamTimer = undefined;
|
|
391
|
+
this.publish();
|
|
392
|
+
}, 50);
|
|
393
|
+
}
|
|
394
|
+
private publish(refreshTurn = false): void {
|
|
395
|
+
if (this.streamTimer) {
|
|
396
|
+
clearTimeout(this.streamTimer);
|
|
397
|
+
this.streamTimer = undefined;
|
|
398
|
+
}
|
|
399
|
+
const messages = this.reader?.after(this.lastOrdinal) ?? [];
|
|
400
|
+
if (messages.length) this.lastOrdinal = messages.at(-1)!.ordinal;
|
|
401
|
+
if (refreshTurn) {
|
|
402
|
+
const latest = this.reader?.latestTurn();
|
|
403
|
+
messages.push(
|
|
404
|
+
...(this.reader?.page().messages.filter((m) => m.turnId === latest?.id) ?? []),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
this.hub.publish({ activity: this.readActivity(), messages });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async close(): Promise<void> {
|
|
411
|
+
this.stopping = true;
|
|
412
|
+
for (const receipt of this.queue.splice(0))
|
|
413
|
+
this.updateReceipt(receipt, {
|
|
414
|
+
status: "interrupted",
|
|
415
|
+
error: "The service was stopped before this queued request began.",
|
|
416
|
+
});
|
|
417
|
+
if (this.opening) await this.opening.catch(() => undefined);
|
|
418
|
+
if (this.runtime)
|
|
419
|
+
await this.runtime.dispose({
|
|
420
|
+
type: "runner_failed",
|
|
421
|
+
error: "Remote service shutdown.",
|
|
422
|
+
});
|
|
423
|
+
await this.active?.completion;
|
|
424
|
+
await this.controlTail;
|
|
425
|
+
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
426
|
+
if (this.streamTimer) clearTimeout(this.streamTimer);
|
|
427
|
+
this.hub.close();
|
|
428
|
+
this.reader?.close();
|
|
429
|
+
}
|
|
430
|
+
get pendingCount(): number {
|
|
431
|
+
return this.queue.length + (this.active ? 1 : 0);
|
|
432
|
+
}
|
|
433
|
+
receiptChanged(): void {
|
|
434
|
+
this.publish();
|
|
435
|
+
}
|
|
436
|
+
get initialized(): boolean {
|
|
437
|
+
return this.runtime !== undefined;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function errorMessage(error: unknown): string {
|
|
442
|
+
return error instanceof Error ? error.message : String(error);
|
|
443
|
+
}
|