@clinebot/core 0.0.10 → 0.0.11

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.
@@ -7,5 +7,5 @@ export declare function extractWorkspaceMetadataFromSystemPrompt(systemPrompt: s
7
7
  export declare function hasRuntimeHooks(hooks: AgentConfig["hooks"]): boolean;
8
8
  export declare function mergeAgentExtensions(explicitExtensions: AgentConfig["extensions"] | undefined, loadedExtensions: AgentConfig["extensions"] | undefined): AgentConfig["extensions"];
9
9
  export declare function serializeAgentEvent(event: AgentEvent): string;
10
- export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult): StoredMessageWithMetadata[];
10
+ export declare function withLatestAssistantTurnMetadata(messages: LlmsProviders.Message[], result: AgentResult, previousMessages?: LlmsProviders.MessageWithMetadata[]): StoredMessageWithMetadata[];
11
11
  export declare function toSessionRecord(row: SessionRowShape): SessionRecord;
@@ -17,6 +17,7 @@ export type ActiveSession = {
17
17
  started: boolean;
18
18
  aborting: boolean;
19
19
  interactive: boolean;
20
+ persistedMessages?: LlmsProviders.MessageWithMetadata[];
20
21
  activeTeamRunIds: Set<string>;
21
22
  pendingTeamRunUpdates: TeamRunUpdate[];
22
23
  teamRunWaiters: Array<() => void>;
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@clinebot/core",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "main": "./dist/index.node.js",
5
5
  "dependencies": {
6
- "@clinebot/agents": "0.0.10",
7
- "@clinebot/llms": "0.0.10",
8
- "@clinebot/shared": "0.0.10",
6
+ "@clinebot/agents": "0.0.11",
7
+ "@clinebot/llms": "0.0.11",
8
+ "@clinebot/shared": "0.0.11",
9
9
  "@opentelemetry/api": "^1.9.0",
10
10
  "@opentelemetry/api-logs": "^0.56.0",
11
11
  "@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
@@ -309,6 +309,137 @@ describe("DefaultSessionManager", () => {
309
309
  });
310
310
  });
311
311
 
312
+ it("preserves per-turn metadata on prior assistant messages across turns", async () => {
313
+ const sessionId = "sess-meta-multi";
314
+ const manifest = createManifest(sessionId);
315
+ const persistSessionMessages = vi.fn();
316
+ const runtimeBuilder = {
317
+ build: vi.fn().mockReturnValue({
318
+ tools: [],
319
+ shutdown: vi.fn(),
320
+ }),
321
+ };
322
+ const firstTurnMessages = [
323
+ {
324
+ role: "user" as const,
325
+ content: [{ type: "text" as const, text: "hello" }],
326
+ },
327
+ {
328
+ role: "assistant" as const,
329
+ content: [{ type: "text" as const, text: "world" }],
330
+ },
331
+ ];
332
+ const secondTurnMessages = [
333
+ ...firstTurnMessages,
334
+ {
335
+ role: "user" as const,
336
+ content: [{ type: "text" as const, text: "again" }],
337
+ },
338
+ {
339
+ role: "assistant" as const,
340
+ content: [{ type: "text" as const, text: "still here" }],
341
+ },
342
+ ];
343
+ const run = vi.fn().mockResolvedValue(
344
+ createResult({
345
+ usage: {
346
+ inputTokens: 33,
347
+ outputTokens: 12,
348
+ cacheReadTokens: 4,
349
+ cacheWriteTokens: 1,
350
+ totalCost: 0.42,
351
+ },
352
+ model: {
353
+ id: "claude-sonnet-4-6",
354
+ provider: "anthropic",
355
+ },
356
+ endedAt: new Date("2026-01-01T00:00:02.000Z"),
357
+ messages: firstTurnMessages,
358
+ }),
359
+ );
360
+ const continueFn = vi.fn().mockResolvedValue(
361
+ createResult({
362
+ usage: {
363
+ inputTokens: 10,
364
+ outputTokens: 5,
365
+ cacheReadTokens: 2,
366
+ cacheWriteTokens: 0,
367
+ totalCost: 0.12,
368
+ },
369
+ model: {
370
+ id: "claude-sonnet-4-6",
371
+ provider: "anthropic",
372
+ },
373
+ endedAt: new Date("2026-01-01T00:00:03.000Z"),
374
+ messages: secondTurnMessages,
375
+ }),
376
+ );
377
+ const agent = {
378
+ run,
379
+ continue: continueFn,
380
+ abort: vi.fn(),
381
+ shutdown: vi.fn().mockResolvedValue(undefined),
382
+ restore: vi.fn(),
383
+ getMessages: vi.fn().mockReturnValue([]),
384
+ messages: [],
385
+ };
386
+ const sessionService = {
387
+ ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
388
+ createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
389
+ manifestPath: "/tmp/manifest-meta-multi.json",
390
+ transcriptPath: "/tmp/transcript-meta-multi.log",
391
+ hookPath: "/tmp/hook-meta-multi.log",
392
+ messagesPath: "/tmp/messages-meta-multi.json",
393
+ manifest,
394
+ }),
395
+ persistSessionMessages,
396
+ updateSessionStatus: vi.fn().mockResolvedValue({ updated: true }),
397
+ writeSessionManifest: vi.fn(),
398
+ listSessions: vi.fn().mockResolvedValue([]),
399
+ deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
400
+ };
401
+ const manager = new DefaultSessionManager({
402
+ distinctId,
403
+ sessionService: sessionService as never,
404
+ runtimeBuilder,
405
+ createAgent: () => agent as never,
406
+ });
407
+
408
+ await manager.start({
409
+ config: createConfig({
410
+ sessionId,
411
+ providerId: "anthropic",
412
+ modelId: "claude-sonnet-4-6",
413
+ }),
414
+ interactive: true,
415
+ });
416
+
417
+ await manager.send({ sessionId, prompt: "hello" });
418
+ await manager.send({ sessionId, prompt: "again" });
419
+
420
+ const persisted = persistSessionMessages.mock.calls[1]?.[1];
421
+ expect(persisted?.[1]).toMatchObject({
422
+ role: "assistant",
423
+ metrics: {
424
+ inputTokens: 33,
425
+ outputTokens: 12,
426
+ cacheReadTokens: 4,
427
+ cacheWriteTokens: 1,
428
+ cost: 0.42,
429
+ },
430
+ });
431
+ expect(persisted?.[3]).toMatchObject({
432
+ role: "assistant",
433
+ metrics: {
434
+ inputTokens: 10,
435
+ outputTokens: 5,
436
+ cacheReadTokens: 2,
437
+ cacheWriteTokens: 0,
438
+ cost: 0.12,
439
+ },
440
+ });
441
+ });
442
+
312
443
  it("persists rendered messages when a turn fails", async () => {
313
444
  const sessionId = "sess-failed-turn";
314
445
  const manifest = createManifest(sessionId);
@@ -303,6 +303,7 @@ export class DefaultSessionManager implements SessionManager {
303
303
  started: false,
304
304
  aborting: false,
305
305
  interactive: input.interactive === true,
306
+ persistedMessages: input.initialMessages,
306
307
  activeTeamRunIds: new Set<string>(),
307
308
  pendingTeamRunUpdates: [],
308
309
  teamRunWaiters: [],
@@ -548,7 +549,8 @@ export class DefaultSessionManager implements SessionManager {
548
549
  ): Promise<AgentResult> {
549
550
  const shouldContinue =
550
551
  session.started || session.agent.getMessages().length > 0;
551
- const baselineMessages = session.agent.getMessages();
552
+ const baselineMessages =
553
+ session.persistedMessages ?? session.agent.getMessages();
552
554
  const usageBaseline =
553
555
  this.usageBySession.get(session.sessionId) ??
554
556
  createInitialAccumulatedUsage();
@@ -582,7 +584,9 @@ export class DefaultSessionManager implements SessionManager {
582
584
  const persistedMessages = withLatestAssistantTurnMetadata(
583
585
  result.messages,
584
586
  result,
587
+ baselineMessages,
585
588
  );
589
+ session.persistedMessages = persistedMessages;
586
590
  this.usageBySession.set(
587
591
  session.sessionId,
588
592
  accumulateUsageTotals(usageBaseline, result.usage),
@@ -62,10 +62,20 @@ export function serializeAgentEvent(event: AgentEvent): string {
62
62
  export function withLatestAssistantTurnMetadata(
63
63
  messages: LlmsProviders.Message[],
64
64
  result: AgentResult,
65
+ previousMessages: LlmsProviders.MessageWithMetadata[] = [],
65
66
  ): StoredMessageWithMetadata[] {
66
- const next = messages.map((message) => ({
67
- ...message,
68
- })) as StoredMessageWithMetadata[];
67
+ const next = messages.map((message, index) => {
68
+ const previous = previousMessages[index];
69
+ const sameMessage =
70
+ previous?.role === message.role &&
71
+ JSON.stringify(previous.content) === JSON.stringify(message.content);
72
+ return sameMessage
73
+ ? ({
74
+ ...previous,
75
+ ...message,
76
+ } as StoredMessageWithMetadata)
77
+ : ({ ...message } as StoredMessageWithMetadata);
78
+ });
69
79
  const assistantIndex = [...next]
70
80
  .reverse()
71
81
  .findIndex((message) => message.role === "assistant");
@@ -18,6 +18,7 @@ export type ActiveSession = {
18
18
  started: boolean;
19
19
  aborting: boolean;
20
20
  interactive: boolean;
21
+ persistedMessages?: LlmsProviders.MessageWithMetadata[];
21
22
  activeTeamRunIds: Set<string>;
22
23
  pendingTeamRunUpdates: TeamRunUpdate[];
23
24
  teamRunWaiters: Array<() => void>;