@yefengr/remote-pi 0.7.12 → 0.7.14

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.
@@ -0,0 +1,42 @@
1
+ import type { PeerRecord, PeerWriteReceipt } from "../pairing/storage.js";
2
+ import type { QRSession } from "../pairing/qr.js";
3
+ import type { ClientFrame, ServerFrame } from "../protocol/v2/index.js";
4
+ import type { RelayClient } from "../transport/relay_client.js";
5
+ import { V2PeerChannel, type HostRouteIdentity } from "../transport/peer_channel.js";
6
+ type PairRequest = Extract<ClientFrame, {
7
+ type: "pair_request";
8
+ }>;
9
+ type PairOk = Extract<ServerFrame, {
10
+ type: "pair_ok";
11
+ }>;
12
+ export interface PairingBinding {
13
+ readonly channel: V2PeerChannel;
14
+ }
15
+ export interface PairingCoordinatorDependencies {
16
+ readonly qrSession: QRSession;
17
+ readonly routeIdentity: () => HostRouteIdentity;
18
+ readonly isRelayCurrent: (relay: RelayClient) => boolean;
19
+ readonly attachOwner: (relay: RelayClient, ownerId: string) => PairingBinding | null;
20
+ readonly activeBinding: (ownerId: string) => PairingBinding | undefined;
21
+ readonly addPeer: (record: PeerRecord) => Promise<PeerWriteReceipt>;
22
+ readonly rollbackPeer: (receipt: PeerWriteReceipt) => Promise<unknown>;
23
+ readonly updateEndpoint: (relay: RelayClient) => Promise<boolean>;
24
+ readonly refreshCurrentEndpoint: () => Promise<boolean>;
25
+ readonly buildPairOk: (frame: PairRequest) => PairOk;
26
+ }
27
+ /** Coordinates one-process pairing retries across storage and Relay lifecycles. */
28
+ export declare class PairingCoordinator {
29
+ private readonly deps;
30
+ private readonly attempts;
31
+ constructor(deps: PairingCoordinatorDependencies);
32
+ handle(relay: RelayClient, ownerId: string, frame: PairRequest): Promise<void>;
33
+ abandonInactive(): void;
34
+ private run;
35
+ private rollback;
36
+ private lifecycleIsCurrent;
37
+ private canCommit;
38
+ private reject;
39
+ private sendError;
40
+ private attemptKey;
41
+ }
42
+ export {};
@@ -0,0 +1,136 @@
1
+ import { V2PeerChannel } from "../transport/peer_channel.js";
2
+ /** Coordinates one-process pairing retries across storage and Relay lifecycles. */
3
+ export class PairingCoordinator {
4
+ deps;
5
+ attempts = new Map();
6
+ constructor(deps) {
7
+ this.deps = deps;
8
+ }
9
+ async handle(relay, ownerId, frame) {
10
+ if (!this.deps.isRelayCurrent(relay))
11
+ return;
12
+ const key = this.attemptKey(ownerId, frame.id);
13
+ const pending = this.attempts.get(key);
14
+ if (pending) {
15
+ await pending.completion;
16
+ return this.handle(relay, ownerId, frame);
17
+ }
18
+ const reservation = this.deps.qrSession.reserveToken(frame.token, ownerId, frame.id);
19
+ if (reservation.status === "expired" || reservation.status === "consumed" || reservation.status === "unknown") {
20
+ const code = reservation.status === "expired" ? "token_expired" : reservation.status === "consumed" ? "token_consumed" : "token_unknown";
21
+ this.reject(relay, ownerId, frame, code, reservation.status === "consumed" ? "Pairing token is already in use" : "Pairing token is invalid or expired");
22
+ return;
23
+ }
24
+ if (reservation.status === "committed") {
25
+ const activeBinding = this.deps.activeBinding(ownerId);
26
+ if (activeBinding) {
27
+ activeBinding.channel.sendV2(reservation.completion);
28
+ return;
29
+ }
30
+ const replayBinding = this.deps.attachOwner(relay, ownerId);
31
+ replayBinding?.channel.sendV2(reservation.completion);
32
+ return;
33
+ }
34
+ if (reservation.status !== "reserved")
35
+ return;
36
+ const binding = this.deps.attachOwner(relay, ownerId);
37
+ if (!binding) {
38
+ this.deps.qrSession.releaseToken(reservation.reservation);
39
+ return;
40
+ }
41
+ let resolveCompletion;
42
+ const completion = new Promise((resolve) => { resolveCompletion = resolve; });
43
+ const attempt = {
44
+ key,
45
+ ownerId,
46
+ relay,
47
+ binding,
48
+ reservation: reservation.reservation,
49
+ completion,
50
+ resolveCompletion,
51
+ };
52
+ this.attempts.set(key, attempt);
53
+ void this.run(attempt, frame);
54
+ }
55
+ abandonInactive() {
56
+ for (const attempt of [...this.attempts.values()]) {
57
+ if (this.lifecycleIsCurrent(attempt))
58
+ continue;
59
+ this.attempts.delete(attempt.key);
60
+ this.deps.qrSession.releaseToken(attempt.reservation);
61
+ attempt.resolveCompletion(null);
62
+ }
63
+ }
64
+ async run(attempt, frame) {
65
+ let completion = null;
66
+ let receipt;
67
+ try {
68
+ receipt = await this.deps.addPeer({ name: frame.device_name, remote_epk: attempt.ownerId, paired_at: new Date().toISOString() });
69
+ if (!this.canCommit(attempt)) {
70
+ await this.rollback(receipt);
71
+ return;
72
+ }
73
+ // peers.json is durable authority, but do not confirm pairing while the
74
+ // current Relay has explicitly rejected the ACL frame. A later retry of
75
+ // the same request can safely repeat the idempotent write.
76
+ const aclSent = await this.deps.updateEndpoint(attempt.relay);
77
+ if (!aclSent || !this.canCommit(attempt)) {
78
+ await this.rollback(receipt);
79
+ return;
80
+ }
81
+ const pairOk = this.deps.buildPairOk(frame);
82
+ if (!this.deps.qrSession.commitToken(attempt.reservation, pairOk)) {
83
+ await this.rollback(receipt);
84
+ return;
85
+ }
86
+ completion = pairOk;
87
+ attempt.binding.channel.sendV2(pairOk);
88
+ }
89
+ catch {
90
+ if (receipt)
91
+ await this.rollback(receipt);
92
+ if (this.lifecycleIsCurrent(attempt)) {
93
+ this.sendError(attempt.binding.channel, frame, "internal_error", "Failed to persist pairing");
94
+ }
95
+ }
96
+ finally {
97
+ if (!completion)
98
+ this.deps.qrSession.releaseToken(attempt.reservation);
99
+ if (this.attempts.get(attempt.key) === attempt)
100
+ this.attempts.delete(attempt.key);
101
+ attempt.resolveCompletion(completion);
102
+ }
103
+ }
104
+ async rollback(receipt) {
105
+ try {
106
+ await this.deps.rollbackPeer(receipt);
107
+ }
108
+ catch {
109
+ // A failed compensation leaves peers.json authoritative.
110
+ }
111
+ // A new Relay may have announced the transient write while this old
112
+ // attempt was pending. Always refresh whichever Relay is current now.
113
+ try {
114
+ await this.deps.refreshCurrentEndpoint();
115
+ }
116
+ catch { /* reconnect owns recovery */ }
117
+ }
118
+ lifecycleIsCurrent(attempt) {
119
+ return this.deps.isRelayCurrent(attempt.relay) && this.deps.activeBinding(attempt.ownerId) === attempt.binding;
120
+ }
121
+ canCommit(attempt) {
122
+ return this.lifecycleIsCurrent(attempt) && this.deps.qrSession.isReservationCurrent(attempt.reservation);
123
+ }
124
+ reject(relay, ownerId, frame, code, message) {
125
+ const channel = new V2PeerChannel(relay, ownerId, this.deps.routeIdentity(), () => undefined);
126
+ this.sendError(channel, frame, code, message);
127
+ channel.detach();
128
+ }
129
+ sendError(channel, frame, code, message) {
130
+ channel.sendV2({ protocol_version: 2, type: "pair_error", in_reply_to: frame.id, code, message });
131
+ }
132
+ attemptKey(ownerId, requestId) {
133
+ return `${ownerId}\u0000${requestId}`;
134
+ }
135
+ }
136
+ //# sourceMappingURL=pairing_coordinator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pairing_coordinator.js","sourceRoot":"","sources":["../../src/runtime/pairing_coordinator.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import type { SessionManager } from "@earendil-works/pi-coding-agent";
2
- import { type JsonValue, type TimelineEvent } from "../protocol/v2/index.js";
1
+ import type { MessageUpdateEvent, SessionManager, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent } from "@earendil-works/pi-coding-agent";
2
+ import { type JsonValue, type TimelineEvent, type TimelinePartial } from "../protocol/v2/index.js";
3
3
  export declare const TIMELINE_MARKER: "remote-pi:timeline-v2";
4
4
  type MessageRole = "user" | "assistant" | "toolResult";
5
5
  export type Correlation = {
@@ -22,6 +22,7 @@ export type TimelineRuntimeOptions = {
22
22
  getHistoryGeneration?: () => string;
23
23
  onStarted?: (started: TimelineStarted) => void;
24
24
  onPublished?: (event: TimelineEvent, correlation: Correlation) => void;
25
+ onPartial?: (partial: TimelinePartial, correlation: Correlation) => void;
25
26
  };
26
27
  export declare class TimelineRuntime {
27
28
  private readonly correlations;
@@ -29,13 +30,16 @@ export declare class TimelineRuntime {
29
30
  private readonly pending;
30
31
  private readonly pendingByRole;
31
32
  private readonly published;
33
+ private readonly toolAssociations;
32
34
  private sessionManager;
33
35
  private epoch;
36
+ private stateRevision;
34
37
  private activeGroupId;
35
38
  private active;
36
39
  private readonly getHistoryGenerationValue?;
37
40
  private readonly onStarted?;
38
41
  private readonly onPublished?;
42
+ private readonly onPartial?;
39
43
  constructor(options?: TimelineRuntimeOptions);
40
44
  attach(sessionManager: SessionManager): void;
41
45
  resetSession(sessionManager: SessionManager): void;
@@ -52,9 +56,18 @@ export declare class TimelineRuntime {
52
56
  onAgentStart(): void;
53
57
  onAgentEnd(): void;
54
58
  onMessageStart(message: unknown, sessionManager: SessionManager): TimelineStarted | null;
59
+ onMessageUpdate(event: MessageUpdateEvent, sessionManager: SessionManager): void;
55
60
  onMessageEnd(message: unknown, sessionManager: SessionManager): void;
61
+ onToolExecutionStart(event: ToolExecutionStartEvent, sessionManager: SessionManager): void;
62
+ onToolExecutionUpdate(event: ToolExecutionUpdateEvent, sessionManager: SessionManager): void;
63
+ onToolExecutionEnd(event: ToolExecutionEndEvent, sessionManager: SessionManager): void;
64
+ private publishToolPartial;
56
65
  recover(sessionManager: SessionManager): TimelineEvent[];
66
+ private findPending;
67
+ private messageIdentity;
57
68
  private publish;
69
+ private publishPartial;
70
+ private partialDeltaChunks;
58
71
  private correlationFor;
59
72
  private buildMarker;
60
73
  private parseMarker;
@@ -71,6 +84,5 @@ export declare class TimelineRuntime {
71
84
  private textFromContent;
72
85
  private nonEmpty;
73
86
  private base64Length;
74
- private jsonValue;
75
87
  }
76
88
  export {};
@@ -1,7 +1,9 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { randomUUID } from "node:crypto";
3
- import { MarkerSchemaV2, TimelineEventSchema, } from "../protocol/v2/index.js";
3
+ import { MarkerSchemaV2, TimelineEventSchema } from "../protocol/v2/index.js";
4
+ import { jsonValue, recoverToolCalls, ToolLifecycleTracker, toolPartial, toolTimelineEvent } from "./tool_lifecycle.js";
4
5
  export const TIMELINE_MARKER = "remote-pi:timeline-v2";
6
+ const MAX_PARTIAL_DELTA_CHARS = 64 * 1024;
5
7
  export class TimelineRuntime {
6
8
  correlations = new AsyncLocalStorage();
7
9
  messageCorrelations = new WeakMap();
@@ -12,17 +14,21 @@ export class TimelineRuntime {
12
14
  toolResult: [],
13
15
  };
14
16
  published = [];
17
+ toolAssociations = new ToolLifecycleTracker();
15
18
  sessionManager = null;
16
19
  epoch = 0;
20
+ stateRevision = 0;
17
21
  activeGroupId = null;
18
22
  active = false;
19
23
  getHistoryGenerationValue;
20
24
  onStarted;
21
25
  onPublished;
26
+ onPartial;
22
27
  constructor(options = {}) {
23
28
  this.getHistoryGenerationValue = options.getHistoryGeneration;
24
29
  this.onStarted = options.onStarted;
25
30
  this.onPublished = options.onPublished;
31
+ this.onPartial = options.onPartial;
26
32
  }
27
33
  attach(sessionManager) {
28
34
  if (this.sessionManager === sessionManager)
@@ -35,6 +41,7 @@ export class TimelineRuntime {
35
41
  this.resetState();
36
42
  }
37
43
  resetState() {
44
+ this.stateRevision += 1;
38
45
  this.epoch = 0;
39
46
  this.activeGroupId = null;
40
47
  this.active = false;
@@ -42,6 +49,7 @@ export class TimelineRuntime {
42
49
  this.pendingByRole.user = [];
43
50
  this.pendingByRole.assistant = [];
44
51
  this.pendingByRole.toolResult = [];
52
+ this.toolAssociations.clear();
45
53
  }
46
54
  get sessionId() {
47
55
  return this.sessionManager?.getSessionId() ?? null;
@@ -85,6 +93,10 @@ export class TimelineRuntime {
85
93
  onAgentEnd() {
86
94
  this.active = false;
87
95
  this.activeGroupId = null;
96
+ this.pendingByRole.user = [];
97
+ this.pendingByRole.assistant = [];
98
+ this.pendingByRole.toolResult = [];
99
+ this.toolAssociations.clear();
88
100
  }
89
101
  onMessageStart(message, sessionManager) {
90
102
  this.attach(sessionManager);
@@ -102,7 +114,7 @@ export class TimelineRuntime {
102
114
  sessionManager.appendCustomEntry(TIMELINE_MARKER, marker);
103
115
  const objectMessage = message;
104
116
  this.messageCorrelations.set(objectMessage, correlation);
105
- const pending = { message: objectMessage, role: record.role, marker, correlation };
117
+ const pending = { message: objectMessage, role: record.role, marker, correlation, identity: this.messageIdentity(record) };
106
118
  this.pending.set(objectMessage, pending);
107
119
  this.pendingByRole[record.role].push(pending);
108
120
  const started = {
@@ -115,18 +127,55 @@ export class TimelineRuntime {
115
127
  this.onStarted?.(started);
116
128
  return started;
117
129
  }
130
+ onMessageUpdate(event, sessionManager) {
131
+ if (this.sessionManager !== sessionManager || this.messageRole(event.message) !== "assistant")
132
+ return;
133
+ const pending = this.findPending(event.message, "assistant");
134
+ if (!pending)
135
+ return;
136
+ const update = event.assistantMessageEvent;
137
+ if (update.type === "text_start") {
138
+ this.publishPartial(pending, "assistant", update.contentIndex, "running");
139
+ }
140
+ else if (update.type === "thinking_start") {
141
+ this.publishPartial(pending, "thinking", update.contentIndex, "running");
142
+ }
143
+ else if (update.type === "text_delta" && update.delta) {
144
+ this.publishPartial(pending, "assistant", update.contentIndex, "delta", update.delta);
145
+ }
146
+ else if (update.type === "thinking_delta" && update.delta) {
147
+ this.publishPartial(pending, "thinking", update.contentIndex, "delta", update.delta);
148
+ }
149
+ else if (update.type === "toolcall_end") {
150
+ this.toolAssociations.indexAssistantContent([update.toolCall], {
151
+ groupId: pending.marker.group_id,
152
+ correlation: { ...pending.correlation },
153
+ });
154
+ }
155
+ }
118
156
  onMessageEnd(message, sessionManager) {
119
157
  this.attach(sessionManager);
120
158
  if (typeof message !== "object" || message === null)
121
159
  return;
122
160
  const objectMessage = message;
123
- const direct = this.pending.get(objectMessage);
124
161
  const role = this.messageRole(message);
125
- const pending = direct ?? (role && role !== "user" ? this.pendingByRole[role][0] : undefined);
162
+ const pending = role ? this.findPending(message, role) : undefined;
126
163
  if (!pending)
127
164
  return;
165
+ if (pending.role === "assistant") {
166
+ this.toolAssociations.indexAssistantContent(this.asMessageRecord(message)?.content, {
167
+ groupId: pending.marker.group_id,
168
+ correlation: { ...pending.correlation },
169
+ });
170
+ }
171
+ this.pending.delete(pending.message);
172
+ this.pending.delete(objectMessage);
128
173
  this.pendingByRole[pending.role] = this.pendingByRole[pending.role].filter((candidate) => candidate !== pending);
174
+ const revision = this.stateRevision;
175
+ const generation = this.historyGeneration;
129
176
  setImmediate(() => {
177
+ if (this.stateRevision !== revision || this.sessionManager !== sessionManager || this.historyGeneration !== generation)
178
+ return;
130
179
  const branch = sessionManager.getBranch();
131
180
  const markerIndex = branch.findIndex((entry) => entry.id === pending.marker.event_id || (entry.type === "custom" && entry.customType === TIMELINE_MARKER && entry.data &&
132
181
  typeof entry.data === "object" && entry.data.event_id === pending.marker.event_id));
@@ -135,15 +184,50 @@ export class TimelineRuntime {
135
184
  const target = this.scanTargetAfterMarker(branch, markerIndex, pending.role);
136
185
  if (!target)
137
186
  return;
138
- const event = this.toTimelineEvent(target, pending.marker, pending.correlation, sessionManager);
187
+ const call = pending.role === "toolResult" ? recoverToolCalls(branch).get(target.id) : undefined;
188
+ const event = this.toTimelineEvent(target, pending.marker, pending.correlation, sessionManager, call);
139
189
  if (!event)
140
190
  return;
141
191
  this.publish(event, pending.correlation);
192
+ if (event.kind === "tool")
193
+ this.toolAssociations.complete(event.tool_call_id);
142
194
  });
143
195
  }
196
+ onToolExecutionStart(event, sessionManager) {
197
+ if (this.sessionManager !== sessionManager || !this.active)
198
+ return;
199
+ const fallback = this.activeGroupId ? {
200
+ groupId: this.activeGroupId,
201
+ correlation: this.currentCorrelation() ?? { origin: "unknown", delivery: "unknown" },
202
+ } : undefined;
203
+ this.toolAssociations.start(event.toolCallId, event.toolName, event.args, fallback);
204
+ this.publishToolPartial(event.toolCallId);
205
+ }
206
+ onToolExecutionUpdate(event, sessionManager) {
207
+ if (this.sessionManager !== sessionManager || !this.active)
208
+ return;
209
+ this.publishToolPartial(event.toolCallId, event.partialResult);
210
+ }
211
+ onToolExecutionEnd(event, sessionManager) {
212
+ if (this.sessionManager !== sessionManager || !this.active)
213
+ return;
214
+ this.publishToolPartial(event.toolCallId, event.result);
215
+ }
216
+ publishToolPartial(toolCallId, result) {
217
+ const association = this.toolAssociations.get(toolCallId);
218
+ const sessionId = this.sessionId;
219
+ const historyGeneration = this.historyGeneration;
220
+ if (!association || !sessionId || !historyGeneration)
221
+ return;
222
+ const partial = toolPartial(toolCallId, association, { sessionId, historyGeneration }, result);
223
+ if (partial)
224
+ this.onPartial?.(partial, { ...association.correlation });
225
+ }
144
226
  recover(sessionManager) {
145
227
  this.attach(sessionManager);
146
228
  const branch = sessionManager.getBranch();
229
+ const legacyGroup = `legacy:${sessionManager.getSessionId()}`;
230
+ const recoveryTools = recoverToolCalls(branch);
147
231
  const matched = new Set();
148
232
  const recovered = [];
149
233
  for (let index = 0; index < branch.length; index += 1) {
@@ -156,12 +240,11 @@ export class TimelineRuntime {
156
240
  if (!target || target.type !== "message")
157
241
  continue;
158
242
  matched.add(target.id);
159
- const event = this.toTimelineEvent(target, marker, this.correlationFromMarker(marker), sessionManager);
243
+ const event = this.toTimelineEvent(target, marker, this.correlationFromMarker(marker), sessionManager, recoveryTools.get(target.id));
160
244
  if (event)
161
245
  recovered.push(event);
162
246
  }
163
247
  }
164
- const legacyGroup = `legacy:${sessionManager.getSessionId()}`;
165
248
  for (const entry of branch) {
166
249
  if (entry.type !== "message" || matched.has(entry.id))
167
250
  continue;
@@ -171,7 +254,7 @@ export class TimelineRuntime {
171
254
  const marker = role === "user"
172
255
  ? { version: 2, event_id: `legacy:${entry.id}`, group_id: legacyGroup, kind: "user", origin: "unknown", delivery: "unknown" }
173
256
  : { version: 2, event_id: `legacy:${entry.id}`, group_id: legacyGroup, kind: role === "assistant" ? "assistant" : "tool" };
174
- const event = this.toTimelineEvent(entry, marker, this.correlationFromMarker(marker), sessionManager);
257
+ const event = this.toTimelineEvent(entry, marker, this.correlationFromMarker(marker), sessionManager, recoveryTools.get(entry.id));
175
258
  if (event)
176
259
  recovered.push(event);
177
260
  }
@@ -182,12 +265,69 @@ export class TimelineRuntime {
182
265
  }
183
266
  return recovered;
184
267
  }
268
+ findPending(message, role) {
269
+ if (typeof message !== "object" || message === null)
270
+ return undefined;
271
+ const direct = this.pending.get(message);
272
+ if (direct?.role === role && this.pendingByRole[role].includes(direct))
273
+ return direct;
274
+ const record = this.asMessageRecord(message);
275
+ const identity = record ? this.messageIdentity(record) : null;
276
+ if (!identity)
277
+ return undefined;
278
+ return this.pendingByRole[role].find((candidate) => candidate.identity === identity);
279
+ }
280
+ messageIdentity(message) {
281
+ if (typeof message.timestamp !== "number" || !Number.isFinite(message.timestamp))
282
+ return null;
283
+ return [message.role, message.timestamp, message.api ?? "", message.provider ?? "", message.model ?? ""].join(":");
284
+ }
185
285
  publish(event, correlation) {
186
286
  if (this.published.some((existing) => existing.event_id === event.event_id))
187
287
  return;
188
288
  this.published.push(event);
189
289
  this.onPublished?.(event, { ...correlation });
190
290
  }
291
+ publishPartial(pending, kind, contentIndex, status, delta) {
292
+ const sessionId = this.sessionId;
293
+ const historyGeneration = this.historyGeneration;
294
+ const groupId = pending.marker.group_id;
295
+ if (!sessionId || !historyGeneration || !groupId)
296
+ return;
297
+ const publish = (chunk) => {
298
+ const partial = {
299
+ protocol_version: 2,
300
+ type: "timeline_partial",
301
+ session_id: sessionId,
302
+ history_generation: historyGeneration,
303
+ group_id: groupId,
304
+ partial_id: `${pending.marker.event_id}:${kind}:${contentIndex}`,
305
+ kind,
306
+ status,
307
+ ...(chunk === undefined ? {} : { delta: chunk }),
308
+ };
309
+ this.onPartial?.(partial, { ...pending.correlation });
310
+ };
311
+ if (delta === undefined) {
312
+ publish();
313
+ return;
314
+ }
315
+ for (const chunk of this.partialDeltaChunks(delta))
316
+ publish(chunk);
317
+ }
318
+ partialDeltaChunks(delta) {
319
+ const chunks = [];
320
+ for (let offset = 0; offset < delta.length;) {
321
+ let end = Math.min(delta.length, offset + MAX_PARTIAL_DELTA_CHARS);
322
+ const finalCodeUnit = delta.charCodeAt(end - 1);
323
+ const nextCodeUnit = delta.charCodeAt(end);
324
+ if (end < delta.length && finalCodeUnit >= 0xD800 && finalCodeUnit <= 0xDBFF && nextCodeUnit >= 0xDC00 && nextCodeUnit <= 0xDFFF)
325
+ end -= 1;
326
+ chunks.push(delta.slice(offset, end));
327
+ offset = end;
328
+ }
329
+ return chunks;
330
+ }
191
331
  correlationFor(message) {
192
332
  const existing = this.currentCorrelation();
193
333
  if (existing)
@@ -239,7 +379,7 @@ export class TimelineRuntime {
239
379
  }
240
380
  return undefined;
241
381
  }
242
- toTimelineEvent(entry, marker, correlation, sessionManager) {
382
+ toTimelineEvent(entry, marker, correlation, sessionManager, association) {
243
383
  const message = this.asMessageRecord(entry.message);
244
384
  if (!message)
245
385
  return null;
@@ -248,6 +388,8 @@ export class TimelineRuntime {
248
388
  const historyGeneration = this.getHistoryGenerationValue?.() ?? sessionId;
249
389
  const base = { event_id: marker.event_id, session_id: sessionId, history_generation: historyGeneration, timestamp };
250
390
  const groupId = marker.group_id;
391
+ if (!groupId)
392
+ return null;
251
393
  if (message.role === "user") {
252
394
  const senderRef = marker.kind === "user" && marker.sender_ref ? { sender_ref: marker.sender_ref } : {};
253
395
  return TimelineEventSchema.parse({
@@ -270,19 +412,7 @@ export class TimelineRuntime {
270
412
  status: message.stopReason === "aborted" ? "interrupted" : "complete",
271
413
  });
272
414
  }
273
- const result = this.jsonValue(message.content);
274
- if (message.isError) {
275
- return TimelineEventSchema.parse({
276
- ...base, group_id: groupId, kind: "tool", tool_call_id: message.toolCallId ?? marker.event_id,
277
- tool: message.toolName ?? "unknown", args: this.jsonValue(message.args ?? {}), truncated: false,
278
- status: "error", error: this.nonEmpty(message.errorMessage ?? this.textFromContent(message.content) ?? "tool failed"),
279
- });
280
- }
281
- return TimelineEventSchema.parse({
282
- ...base, group_id: groupId, kind: "tool", tool_call_id: message.toolCallId ?? marker.event_id,
283
- tool: message.toolName ?? "unknown", args: this.jsonValue(message.args ?? {}), truncated: false,
284
- status: "complete", result,
285
- });
415
+ return toolTimelineEvent(base, groupId, message, association);
286
416
  }
287
417
  toSystemEvent(entry, sessionManager) {
288
418
  const base = {
@@ -302,7 +432,7 @@ export class TimelineRuntime {
302
432
  first_kept_entry_id: entry.firstKeptEntryId,
303
433
  tokens_before: entry.tokensBefore,
304
434
  from_hook: entry.fromHook ?? false,
305
- ...(entry.details === undefined ? {} : { details: this.jsonValue(entry.details) }),
435
+ ...(entry.details === undefined ? {} : { details: jsonValue(entry.details) }),
306
436
  },
307
437
  };
308
438
  }
@@ -314,7 +444,7 @@ export class TimelineRuntime {
314
444
  summary: entry.summary,
315
445
  from_id: entry.fromId,
316
446
  from_hook: entry.fromHook ?? false,
317
- ...(entry.details === undefined ? {} : { details: this.jsonValue(entry.details) }),
447
+ ...(entry.details === undefined ? {} : { details: jsonValue(entry.details) }),
318
448
  },
319
449
  };
320
450
  }
@@ -324,7 +454,7 @@ export class TimelineRuntime {
324
454
  kind: "custom",
325
455
  payload: {
326
456
  custom_type: entry.customType,
327
- ...(entry.data === undefined ? {} : { data: this.jsonValue(entry.data) }),
457
+ ...(entry.data === undefined ? {} : { data: jsonValue(entry.data) }),
328
458
  },
329
459
  };
330
460
  }
@@ -380,9 +510,10 @@ export class TimelineRuntime {
380
510
  if (!part || typeof part !== "object")
381
511
  return [];
382
512
  const item = part;
383
- if ((item.type === "text" || item.type === "thinking") && typeof item.text === "string") {
384
- return [{ type: item.type, text: item.text }];
385
- }
513
+ if (item.type === "text" && typeof item.text === "string")
514
+ return [{ type: "text", text: item.text }];
515
+ if (item.type === "thinking" && typeof item.thinking === "string")
516
+ return [{ type: "thinking", text: item.thinking }];
386
517
  return [];
387
518
  });
388
519
  }
@@ -409,20 +540,5 @@ export class TimelineRuntime {
409
540
  return 0;
410
541
  }
411
542
  }
412
- jsonValue(value) {
413
- if (value === null || typeof value === "boolean" || typeof value === "string")
414
- return value;
415
- if (typeof value === "number")
416
- return Number.isFinite(value) ? value : null;
417
- if (Array.isArray(value))
418
- return value.map((item) => this.jsonValue(item));
419
- if (typeof value === "object") {
420
- const result = {};
421
- for (const [key, item] of Object.entries(value))
422
- result[key] = this.jsonValue(item);
423
- return result;
424
- }
425
- return null;
426
- }
427
543
  }
428
544
  //# sourceMappingURL=runtime.js.map