@salesforce/sfdx-agent-harness-claude 0.45.0 → 0.46.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 +5 -0
- package/dist/claude-harness.d.ts +34 -10
- package/dist/claude-harness.js +177 -40
- package/dist/claude-harness.js.map +1 -1
- package/dist/test/tsconfig.tsbuildinfo +1 -1
- package/dist/thread-context-store.d.ts +40 -16
- package/dist/thread-context-store.js +96 -20
- package/dist/thread-context-store.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
All notable changes to `@salesforce/sfdx-agent-harness-claude` are documented in this file.
|
|
4
4
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## [0.46.0] - 2026-08-17
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
- **agent-sdk,harness-mastra,harness-claude,harness-openai**: session-context contract + conformance gate @W-23632685@ ([#747](https://github.com/forcedotcom/agentic-dx/pull/747))
|
|
10
|
+
|
|
6
11
|
## [0.45.0] - 2026-08-14
|
|
7
12
|
|
|
8
13
|
_No changes — released alongside dependent packages._
|
package/dist/claude-harness.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Clock, type LogRecord, type UniqueIDGenerator, type Unsubscribe } from '@salesforce/agentic-common';
|
|
2
|
-
import { type AgentHarness, type ChatStreamResult, type HarnessAgentConfig, type McpServerInfo, type Message, type MessagePart, type ModelConnectivityInfo, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
|
|
2
|
+
import { type AgentHarness, type ChatStreamResult, type HarnessAgentConfig, type McpServerInfo, type Message, type MessagePart, type ModelConnectivityInfo, type SessionContext, type StreamOptions, type TelemetryEventCallback, type ToolResultInfo, type WireCommunicationEventCallback } from '@salesforce/sfdx-agent-sdk';
|
|
3
3
|
import { type AgentHooks, type WithAgentConfig } from '@salesforce/sfdx-agent-sdk/harness';
|
|
4
4
|
import type { ClaudeAgentConfig } from './claude-config.js';
|
|
5
5
|
import type { ClaudeQueryEnvBuilder } from './claude-query-env-builder.js';
|
|
@@ -222,16 +222,36 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
|
|
|
222
222
|
*/
|
|
223
223
|
cloneThread(agentId: string, sourceThreadId: string): Promise<string>;
|
|
224
224
|
/**
|
|
225
|
-
*
|
|
226
|
-
* `
|
|
225
|
+
* Allocate and register a fresh `ThreadState` for `threadId`, mirroring
|
|
226
|
+
* `createThread`'s initial shape (legacy 1:1 `claudeSessionId === threadId`,
|
|
227
|
+
* empty context arrays, `sessionContext` absent). Used by `setSessionContext`
|
|
228
|
+
* to seed a thread the consumer references before `createThread` / the first
|
|
229
|
+
* `stream()` — the session-context contract allows seeding context on an
|
|
230
|
+
* as-yet-unregistered thread.
|
|
231
|
+
*/
|
|
232
|
+
private allocateThreadState;
|
|
233
|
+
/**
|
|
234
|
+
* Project a `ThreadState` into the on-disk `ThreadContextRecord`. Every
|
|
235
|
+
* sidecar write goes through this one builder so all persisted channels —
|
|
236
|
+
* addContext arrays, the `claudeSessionId` mapping, AND `sessionContext` —
|
|
237
|
+
* travel together and no write site can silently drop one. `sessionContext`
|
|
238
|
+
* is included only when set, so a thread that never set it writes no field
|
|
239
|
+
* (and reads back `{}`).
|
|
240
|
+
*/
|
|
241
|
+
private threadContextRecordFrom;
|
|
242
|
+
/**
|
|
243
|
+
* Write the sidecar for a thread so its `contextHistory`, `pendingContext`,
|
|
244
|
+
* `claudeSessionId`, AND `sessionContext` survive across restart.
|
|
227
245
|
*
|
|
228
|
-
* The early-return optimization for "nothing to persist" only fires
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* `claudeSessionId` check matters post-#511
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
246
|
+
* The early-return optimization for "nothing to persist" only fires when
|
|
247
|
+
* EVERY channel matches the legacy default: no addContext history, an
|
|
248
|
+
* un-rotated `claudeSessionId` (`=== threadId`), and no session context set.
|
|
249
|
+
* The `claudeSessionId` check matters post-#511 (a thread whose sessionId
|
|
250
|
+
* was rotated by `clearMessages()` MUST write a sidecar even with no
|
|
251
|
+
* addContext data); the `sessionContext` check matters symmetrically — a
|
|
252
|
+
* thread carrying only session context MUST persist it. `setSessionContext`
|
|
253
|
+
* itself writes unconditionally rather than through this helper, so this
|
|
254
|
+
* guard is for the incidental persist sites (stream-consume, clone).
|
|
235
255
|
*/
|
|
236
256
|
private persistThreadContextIfPresent;
|
|
237
257
|
/**
|
|
@@ -384,7 +404,11 @@ export declare class ClaudeHarness implements ClaudeAgentHarness {
|
|
|
384
404
|
getMessages(agentId: string, threadId: string): Promise<Message[]>;
|
|
385
405
|
clearMessages(agentId: string, threadId: string): Promise<void>;
|
|
386
406
|
private buildContextPrompt;
|
|
407
|
+
addMessages(agentId: string, threadId: string, messages: Message[]): Promise<void>;
|
|
408
|
+
/** @deprecated Renamed to {@link addMessages}; this delegates verbatim. */
|
|
387
409
|
addContext(agentId: string, threadId: string, messages: Message[]): Promise<void>;
|
|
410
|
+
setSessionContext(agentId: string, threadId: string, content: SessionContext): Promise<void>;
|
|
411
|
+
getSessionContext(agentId: string, threadId: string): Promise<SessionContext>;
|
|
388
412
|
/**
|
|
389
413
|
* Synchronously flips `serverState.currentStatus` (no-op when the next
|
|
390
414
|
* status equals the current) and returns a closure that, when invoked,
|
package/dist/claude-harness.js
CHANGED
|
@@ -48,6 +48,24 @@ function extractSystemPromptBody(systemPrompt) {
|
|
|
48
48
|
}
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Deep-copy a `SessionContext` at the harness's public boundary so the consumer
|
|
53
|
+
* and the in-memory `ThreadState.sessionContext` never share a reference — a
|
|
54
|
+
* caller mutating either the object it passed to `setSessionContext` or the one
|
|
55
|
+
* `getSessionContext` returns must not reach into harness-internal state (which
|
|
56
|
+
* would silently diverge from what is persisted to disk until the next write).
|
|
57
|
+
* A JSON round-trip is the right copy here: `SessionContext` is a `JsonValue`
|
|
58
|
+
* object by contract (no functions / `Date` / class instances), so the round-trip
|
|
59
|
+
* is lossless AND it normalizes the object to the exact shape a post-restart read
|
|
60
|
+
* off disk would produce — so `getSessionContext` returns an identical value
|
|
61
|
+
* whether or not a restart intervened. Caveat: `JSON.stringify` drops keys whose
|
|
62
|
+
* value is `undefined`; type-safe here because `JsonValue` excludes `undefined`,
|
|
63
|
+
* but an `any`-typed caller that smuggles one in would see that key silently
|
|
64
|
+
* dropped rather than preserved as `undefined`.
|
|
65
|
+
*/
|
|
66
|
+
function cloneSessionContext(content) {
|
|
67
|
+
return JSON.parse(JSON.stringify(content));
|
|
68
|
+
}
|
|
51
69
|
export class ClaudeHarness {
|
|
52
70
|
envBuilder;
|
|
53
71
|
optionsBuilder;
|
|
@@ -319,6 +337,11 @@ export class ClaudeHarness {
|
|
|
319
337
|
claudeSessionId,
|
|
320
338
|
pendingContext: ctx.pendingContext,
|
|
321
339
|
contextHistory: ctx.contextHistory,
|
|
340
|
+
// Rehydrate the session-context channel so a set survives
|
|
341
|
+
// restart. Absent on the record (never set, or a pre-session-
|
|
342
|
+
// context sidecar) leaves `ThreadState.sessionContext`
|
|
343
|
+
// undefined → `getSessionContext` reads back `{}`.
|
|
344
|
+
...(ctx.sessionContext !== undefined ? { sessionContext: ctx.sessionContext } : {}),
|
|
322
345
|
});
|
|
323
346
|
consumedSessionIds.add(claudeSessionId);
|
|
324
347
|
}
|
|
@@ -415,6 +438,13 @@ export class ClaudeHarness {
|
|
|
415
438
|
await closeClientWithTimeout(s.client);
|
|
416
439
|
await s.ready.catch(() => undefined);
|
|
417
440
|
}));
|
|
441
|
+
// Session context now shares the `ThreadContextStore` sidecar with the
|
|
442
|
+
// addContext channel (the spike's "extend the existing sidecar" for
|
|
443
|
+
// Claude), so it inherits that channel's lifecycle: sidecars are cleaned
|
|
444
|
+
// per-thread by `destroyThread`, NOT wiped en masse here. `destroyAgent`
|
|
445
|
+
// already leaves addContext sidecars on disk so a recreate over the same
|
|
446
|
+
// `projectRoot` rehydrates the threads (#455); session context rides the
|
|
447
|
+
// same durability. There is deliberately no agent-level sidecar wipe.
|
|
418
448
|
return this.agents.delete(agentId);
|
|
419
449
|
}
|
|
420
450
|
async getAgentIds() {
|
|
@@ -642,6 +672,17 @@ export class ClaudeHarness {
|
|
|
642
672
|
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
643
673
|
}
|
|
644
674
|
const id = threadId ?? this.idGenerator.getUniqueId();
|
|
675
|
+
// A thread id may already carry state if the consumer seeded session
|
|
676
|
+
// context before creating the thread — `setSessionContext` lazily
|
|
677
|
+
// allocates a `ThreadState` for an unregistered id (the contract allows
|
|
678
|
+
// seeding pre-`createThread`). Carry that `sessionContext` forward so a
|
|
679
|
+
// later `createThread` on the same id does not silently drop the last
|
|
680
|
+
// object the consumer set — `getSessionContext` MUST still return it
|
|
681
|
+
// (and its persisted sidecar copy stays consistent with memory). The
|
|
682
|
+
// message channels DO reset here: `createThread` semantically starts a
|
|
683
|
+
// fresh conversation, and session context is the one out-of-history
|
|
684
|
+
// piece that outlives that reset.
|
|
685
|
+
const existingSessionContext = agent.threads.get(id)?.sessionContext;
|
|
645
686
|
// claudeSessionId starts equal to threadId — only diverges after a
|
|
646
687
|
// `clearMessages()` cycle (see issue #511 / ThreadState docstring).
|
|
647
688
|
agent.threads.set(id, {
|
|
@@ -649,6 +690,7 @@ export class ClaudeHarness {
|
|
|
649
690
|
claudeSessionId: id,
|
|
650
691
|
pendingContext: [],
|
|
651
692
|
contextHistory: [],
|
|
693
|
+
...(existingSessionContext !== undefined ? { sessionContext: existingSessionContext } : {}),
|
|
652
694
|
});
|
|
653
695
|
return id;
|
|
654
696
|
}
|
|
@@ -669,10 +711,11 @@ export class ClaudeHarness {
|
|
|
669
711
|
sessionStore: this.sessionStore,
|
|
670
712
|
});
|
|
671
713
|
}
|
|
672
|
-
// Delete the
|
|
673
|
-
//
|
|
674
|
-
// streamed, and leaving the sidecar would resurrect
|
|
675
|
-
// next createAgent rehydrate pass.
|
|
714
|
+
// Delete the sidecar regardless of `hasStreamed` — a thread can have
|
|
715
|
+
// addContext-injected turns OR setSessionContext state without ever
|
|
716
|
+
// being streamed, and leaving the sidecar would resurrect either on the
|
|
717
|
+
// next createAgent rehydrate pass (or if the thread id is recreated).
|
|
718
|
+
// Both channels share this one sidecar, so a single delete covers them.
|
|
676
719
|
await this.threadContextStore.delete(agent.projectRoot, threadId);
|
|
677
720
|
agent.threads.delete(threadId);
|
|
678
721
|
}
|
|
@@ -707,6 +750,12 @@ export class ClaudeHarness {
|
|
|
707
750
|
if (!sourceState) {
|
|
708
751
|
throw new Error(`Thread "${sourceThreadId}" not found`);
|
|
709
752
|
}
|
|
753
|
+
// Both branches below build the clone's `ThreadState` inline rather than
|
|
754
|
+
// through `threadContextRecordFrom`, and deliberately do NOT copy
|
|
755
|
+
// `sourceState.sessionContext`: session-context carry-forward is deferred
|
|
756
|
+
// to this harness's render PR (W-23632691). When it lands, copy
|
|
757
|
+
// `sessionContext` here too. Until then a clone starts with empty session
|
|
758
|
+
// context by design, not by omission.
|
|
710
759
|
if (!sourceState.hasStreamed) {
|
|
711
760
|
const newId = this.idGenerator.getUniqueId();
|
|
712
761
|
agent.threads.set(newId, {
|
|
@@ -739,27 +788,60 @@ export class ClaudeHarness {
|
|
|
739
788
|
return result.sessionId;
|
|
740
789
|
}
|
|
741
790
|
/**
|
|
742
|
-
*
|
|
743
|
-
* `
|
|
791
|
+
* Allocate and register a fresh `ThreadState` for `threadId`, mirroring
|
|
792
|
+
* `createThread`'s initial shape (legacy 1:1 `claudeSessionId === threadId`,
|
|
793
|
+
* empty context arrays, `sessionContext` absent). Used by `setSessionContext`
|
|
794
|
+
* to seed a thread the consumer references before `createThread` / the first
|
|
795
|
+
* `stream()` — the session-context contract allows seeding context on an
|
|
796
|
+
* as-yet-unregistered thread.
|
|
797
|
+
*/
|
|
798
|
+
allocateThreadState(agent, threadId) {
|
|
799
|
+
const state = {
|
|
800
|
+
hasStreamed: false,
|
|
801
|
+
claudeSessionId: threadId,
|
|
802
|
+
pendingContext: [],
|
|
803
|
+
contextHistory: [],
|
|
804
|
+
};
|
|
805
|
+
agent.threads.set(threadId, state);
|
|
806
|
+
return state;
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Project a `ThreadState` into the on-disk `ThreadContextRecord`. Every
|
|
810
|
+
* sidecar write goes through this one builder so all persisted channels —
|
|
811
|
+
* addContext arrays, the `claudeSessionId` mapping, AND `sessionContext` —
|
|
812
|
+
* travel together and no write site can silently drop one. `sessionContext`
|
|
813
|
+
* is included only when set, so a thread that never set it writes no field
|
|
814
|
+
* (and reads back `{}`).
|
|
815
|
+
*/
|
|
816
|
+
threadContextRecordFrom(source) {
|
|
817
|
+
return {
|
|
818
|
+
contextHistory: source.contextHistory,
|
|
819
|
+
pendingContext: source.pendingContext,
|
|
820
|
+
claudeSessionId: source.claudeSessionId,
|
|
821
|
+
...(source.sessionContext !== undefined ? { sessionContext: source.sessionContext } : {}),
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Write the sidecar for a thread so its `contextHistory`, `pendingContext`,
|
|
826
|
+
* `claudeSessionId`, AND `sessionContext` survive across restart.
|
|
744
827
|
*
|
|
745
|
-
* The early-return optimization for "nothing to persist" only fires
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
* `claudeSessionId` check matters post-#511
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
828
|
+
* The early-return optimization for "nothing to persist" only fires when
|
|
829
|
+
* EVERY channel matches the legacy default: no addContext history, an
|
|
830
|
+
* un-rotated `claudeSessionId` (`=== threadId`), and no session context set.
|
|
831
|
+
* The `claudeSessionId` check matters post-#511 (a thread whose sessionId
|
|
832
|
+
* was rotated by `clearMessages()` MUST write a sidecar even with no
|
|
833
|
+
* addContext data); the `sessionContext` check matters symmetrically — a
|
|
834
|
+
* thread carrying only session context MUST persist it. `setSessionContext`
|
|
835
|
+
* itself writes unconditionally rather than through this helper, so this
|
|
836
|
+
* guard is for the incidental persist sites (stream-consume, clone).
|
|
752
837
|
*/
|
|
753
838
|
async persistThreadContextIfPresent(projectRoot, threadId, source) {
|
|
754
839
|
const hasContext = source.contextHistory.length > 0 || source.pendingContext.length > 0;
|
|
755
840
|
const hasDivergedSessionId = source.claudeSessionId !== threadId;
|
|
756
|
-
|
|
841
|
+
const hasSessionContext = source.sessionContext !== undefined;
|
|
842
|
+
if (!hasContext && !hasDivergedSessionId && !hasSessionContext)
|
|
757
843
|
return;
|
|
758
|
-
await this.threadContextStore.write(projectRoot, threadId,
|
|
759
|
-
contextHistory: source.contextHistory,
|
|
760
|
-
pendingContext: source.pendingContext,
|
|
761
|
-
claudeSessionId: source.claudeSessionId,
|
|
762
|
-
});
|
|
844
|
+
await this.threadContextStore.write(projectRoot, threadId, this.threadContextRecordFrom(source));
|
|
763
845
|
}
|
|
764
846
|
/**
|
|
765
847
|
* Compacts a thread into a new thread seeded with an LLM-generated summary
|
|
@@ -815,7 +897,7 @@ export class ClaudeHarness {
|
|
|
815
897
|
const newThreadId = await this.createThread(agentId);
|
|
816
898
|
const userTs = this.clock.now();
|
|
817
899
|
const assistantTs = this.clock.nextAfter(userTs);
|
|
818
|
-
await this.
|
|
900
|
+
await this.addMessages(agentId, newThreadId, [
|
|
819
901
|
{
|
|
820
902
|
id: this.idGenerator.getUniqueId(),
|
|
821
903
|
role: 'user',
|
|
@@ -1261,12 +1343,11 @@ export class ClaudeHarness {
|
|
|
1261
1343
|
// systemPrompt. contextHistory persists unchanged and verbatim —
|
|
1262
1344
|
// structured MessagePart[] content (incl. image/file base64) is
|
|
1263
1345
|
// preserved so the getMessages() round-trip stays lossless across a
|
|
1264
|
-
// restart, matching Mastra's persist-verbatim behavior.
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
});
|
|
1346
|
+
// restart, matching Mastra's persist-verbatim behavior. Routed
|
|
1347
|
+
// through `threadContextRecordFrom` (reading the just-cleared
|
|
1348
|
+
// in-memory `pendingContext`) so any `sessionContext` on the thread
|
|
1349
|
+
// survives the clear.
|
|
1350
|
+
await this.threadContextStore.write(projectRoot, threadId, this.threadContextRecordFrom(threadState));
|
|
1270
1351
|
}
|
|
1271
1352
|
// Wire-communication monitoring on Claude (subscriber-gated): the Claude Agent SDK runs
|
|
1272
1353
|
// the model client in a subprocess with no programmatic HTTP-interception seam, so we
|
|
@@ -1445,7 +1526,14 @@ export class ClaudeHarness {
|
|
|
1445
1526
|
// the contract's ascending-by-createdAt post-condition under stable
|
|
1446
1527
|
// sort, so step the fallback per-position via `clock.nextAfter`.
|
|
1447
1528
|
let lastFallback = this.clock.now();
|
|
1448
|
-
const persisted = sessionMessages.
|
|
1529
|
+
const persisted = sessionMessages.flatMap((msg) => {
|
|
1530
|
+
// The SDK's `MessageRole` is `'user' | 'assistant' | 'tool'` — `'system'`
|
|
1531
|
+
// is not a consumer-visible transcript role (system state rides
|
|
1532
|
+
// `instructions` / session context). The Anthropic transcript never
|
|
1533
|
+
// stores a bare `system` turn on this path, but drop it defensively
|
|
1534
|
+
// rather than surface an off-contract role across the SDK boundary.
|
|
1535
|
+
if (msg.type !== 'user' && msg.type !== 'assistant')
|
|
1536
|
+
return [];
|
|
1449
1537
|
const recorded = timestamps.get(msg.uuid);
|
|
1450
1538
|
let createdAt;
|
|
1451
1539
|
if (recorded) {
|
|
@@ -1455,12 +1543,14 @@ export class ClaudeHarness {
|
|
|
1455
1543
|
lastFallback = this.clock.nextAfter(lastFallback);
|
|
1456
1544
|
createdAt = lastFallback;
|
|
1457
1545
|
}
|
|
1458
|
-
return
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1546
|
+
return [
|
|
1547
|
+
{
|
|
1548
|
+
id: msg.uuid,
|
|
1549
|
+
role: msg.type,
|
|
1550
|
+
content: mapSessionMessageContent(msg.message),
|
|
1551
|
+
createdAt,
|
|
1552
|
+
},
|
|
1553
|
+
];
|
|
1464
1554
|
});
|
|
1465
1555
|
// Merge the addContext-injected mirror with the persisted transcript
|
|
1466
1556
|
// and sort chronologically (#464). The Claude SDK has no public API
|
|
@@ -1542,7 +1632,7 @@ export class ClaudeHarness {
|
|
|
1542
1632
|
const prefix = baseInstructions ? `${baseInstructions}\n\n` : '';
|
|
1543
1633
|
return `${prefix}<prior-context>\n${contextBlock}\n</prior-context>`;
|
|
1544
1634
|
}
|
|
1545
|
-
async
|
|
1635
|
+
async addMessages(agentId, threadId, messages) {
|
|
1546
1636
|
const agent = this.agents.get(agentId);
|
|
1547
1637
|
if (!agent) {
|
|
1548
1638
|
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
@@ -1578,12 +1668,59 @@ export class ClaudeHarness {
|
|
|
1578
1668
|
// restart — matching Mastra's persist-verbatim (LibSQL) behavior. The
|
|
1579
1669
|
// earlier placeholder-flattening (#491) was a premature optimization that
|
|
1580
1670
|
// silently destroyed structured tool parts on rehydrate and diverged from
|
|
1581
|
-
// Mastra; removed in favor of cross-harness correctness.
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1671
|
+
// Mastra; removed in favor of cross-harness correctness. Routed through
|
|
1672
|
+
// `threadContextRecordFrom` so any `sessionContext` on the thread is
|
|
1673
|
+
// carried in the same write and never clobbered by this addContext path.
|
|
1674
|
+
await this.threadContextStore.write(agent.projectRoot, threadId, this.threadContextRecordFrom(threadState));
|
|
1675
|
+
}
|
|
1676
|
+
/** @deprecated Renamed to {@link addMessages}; this delegates verbatim. */
|
|
1677
|
+
async addContext(agentId, threadId, messages) {
|
|
1678
|
+
await this.addMessages(agentId, threadId, messages);
|
|
1679
|
+
}
|
|
1680
|
+
async setSessionContext(agentId, threadId, content) {
|
|
1681
|
+
// AGENT_NOT_FOUND guard runs before any state touch — matches the
|
|
1682
|
+
// cross-harness contract (#464) that every agent-scoped method throws
|
|
1683
|
+
// on an unknown id. threadId is NOT validated: session context is a
|
|
1684
|
+
// per-thread channel the consumer can seed before a thread's first
|
|
1685
|
+
// stream (or before `createThread`), so an arbitrary threadId is a
|
|
1686
|
+
// legal write target — a `ThreadState` is allocated lazily for it,
|
|
1687
|
+
// mirroring `createThread`'s initial shape (`claudeSessionId ===
|
|
1688
|
+
// threadId`, empty context arrays), and only its `sessionContext`
|
|
1689
|
+
// field is set. An existing thread keeps all its other state.
|
|
1690
|
+
const agent = this.agents.get(agentId);
|
|
1691
|
+
if (!agent) {
|
|
1692
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
1693
|
+
}
|
|
1694
|
+
const threadState = agent.threads.get(threadId) ?? this.allocateThreadState(agent, threadId);
|
|
1695
|
+
// Update the in-memory source of truth first, then persist — mirrors
|
|
1696
|
+
// `addMessages`, which mutates the arrays before writing the sidecar.
|
|
1697
|
+
// Store a DEEP COPY, not the caller's reference: the consumer keeps
|
|
1698
|
+
// ownership of the object it passed, so a later mutation of it must not
|
|
1699
|
+
// reach into harness-internal state (and diverge from what was written
|
|
1700
|
+
// to disk). The deleted `SessionContextStore` got this for free by
|
|
1701
|
+
// JSON-serializing to disk on every write; `cloneSessionContext`
|
|
1702
|
+
// preserves that isolation now that the object is held in memory.
|
|
1703
|
+
threadState.sessionContext = cloneSessionContext(content);
|
|
1704
|
+
// Unconditional write (not `persistThreadContextIfPresent`): an explicit
|
|
1705
|
+
// set MUST be durable even when the thread has no addContext history and
|
|
1706
|
+
// an un-rotated `claudeSessionId`.
|
|
1707
|
+
await this.threadContextStore.write(agent.projectRoot, threadId, this.threadContextRecordFrom(threadState));
|
|
1708
|
+
}
|
|
1709
|
+
async getSessionContext(agentId, threadId) {
|
|
1710
|
+
const agent = this.agents.get(agentId);
|
|
1711
|
+
if (!agent) {
|
|
1712
|
+
throw new AgentSDKError(`No Agent found with id: "${agentId}"`, AgentSDKErrorType.AGENT_NOT_FOUND);
|
|
1713
|
+
}
|
|
1714
|
+
// In-memory `ThreadState` is the source of truth (rehydrated from the
|
|
1715
|
+
// sidecar at `createAgent`, same as `contextHistory` / `pendingContext`).
|
|
1716
|
+
// `{}`-on-miss — never `null`/`undefined`, never throws — for a thread
|
|
1717
|
+
// that was never registered or never had context set. Return a DEEP COPY
|
|
1718
|
+
// so a caller mutating the result can't reach into harness-internal state
|
|
1719
|
+
// (which would silently diverge from disk until the next persist); this
|
|
1720
|
+
// matches the fresh-object-per-read the deleted `SessionContextStore`
|
|
1721
|
+
// gave via its `JSON.parse` read path.
|
|
1722
|
+
const current = agent.threads.get(threadId)?.sessionContext;
|
|
1723
|
+
return current === undefined ? {} : cloneSessionContext(current);
|
|
1587
1724
|
}
|
|
1588
1725
|
/**
|
|
1589
1726
|
* Synchronously flips `serverState.currentStatus` (no-op when the next
|