@theokit/sdk 4.0.2 → 4.1.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c4d410c: SE41 — pluggable `SessionStore` seam over the native transcript. A minimal, two-method public port (`SessionStore.readRecords(agentId)` / `appendRecords(agentId, records)`) over the native `SessionRecord` shape, injected via `local.sessionStore`, so an external store (Postgres / Redis / KV / durable object) can be the **primary store AND resume source** — restoring the serverless (ephemeral FS) and multi-host / multi-pod resume use case SE40 dropped when it removed `ConversationStorageAdapter`, WITHOUT reverting that removed ~10-method adapter. DEFAULTS to a shipped `FsSessionStore` that reads and append-writes the native Claude-shaped `.jsonl` transcript (the same file the Claude Code CLI can `--continue`), so omitting `sessionStore` is byte-identical to current behavior — zero consumer change. Resume works across a simulated cold start (`Agent.resume(agentId, { local: { sessionStore } })` rebuilds history via the native DAG reader over `await store.readRecords(agentId)`); append-only compaction (`compact_boundary`) flows through `appendRecords`. `readRecords` throwing on resume surfaces as a typed error (fail-fast — never a silent empty history that would drop the conversation). Additive and back-compat.
8
+
3
9
  ## 4.0.2
4
10
 
5
11
  ### Patch Changes
@@ -3506,6 +3506,72 @@ var init_cloud_tool_parity = __esm({
3506
3506
  init_errors();
3507
3507
  }
3508
3508
  });
3509
+
3510
+ // src/internal/persistence/file-lock.ts
3511
+ async function getProperLockfile() {
3512
+ if (cached !== void 0) return cached;
3513
+ try {
3514
+ const mod = await import('proper-lockfile');
3515
+ if (!validateLockModule(mod)) {
3516
+ if (!warnedStructural) {
3517
+ warnedStructural = true;
3518
+ process.stderr.write(
3519
+ "[theokit-sdk] proper-lockfile: imported module does NOT expose the expected `lock`/`unlock` API surface. This may indicate a supply-chain compromise or an incompatible major version. Falling back to in-process mutex (no cross-process safety). Reinstall with: pnpm add proper-lockfile@^11\n"
3520
+ );
3521
+ }
3522
+ cached = null;
3523
+ return cached;
3524
+ }
3525
+ cached = mod;
3526
+ } catch {
3527
+ cached = null;
3528
+ }
3529
+ return cached;
3530
+ }
3531
+ function validateLockModule(mod) {
3532
+ if (mod === null || mod === void 0 || typeof mod !== "object") return false;
3533
+ const m = mod;
3534
+ return typeof m.lock === "function" && typeof m.unlock === "function";
3535
+ }
3536
+ async function withFileLock(path, fn, options) {
3537
+ const lib = await getProperLockfile();
3538
+ if (lib === null) {
3539
+ if (!warnedMissing) {
3540
+ warnedMissing = true;
3541
+ process.stderr.write(
3542
+ "[theokit-sdk] proper-lockfile not installed; cross-process file lock unavailable. Install with: pnpm add proper-lockfile\n"
3543
+ );
3544
+ }
3545
+ return withCwdMutex(`file-lock:${path}`, fn);
3546
+ }
3547
+ return withCwdMutex(`file-lock:${path}`, async () => {
3548
+ const release = await lib.lock(path, {
3549
+ // EC-1: companion lockfile, target path may not exist yet.
3550
+ lockfilePath: `${path}.lock`,
3551
+ realpath: false,
3552
+ stale: 3e4,
3553
+ retries: {
3554
+ retries: 5,
3555
+ factor: 1.5,
3556
+ minTimeout: 100,
3557
+ maxTimeout: 5e3
3558
+ }
3559
+ });
3560
+ try {
3561
+ return await fn();
3562
+ } finally {
3563
+ await release();
3564
+ }
3565
+ });
3566
+ }
3567
+ var cached, warnedMissing, warnedStructural;
3568
+ var init_file_lock = __esm({
3569
+ "src/internal/persistence/file-lock.ts"() {
3570
+ init_cwd_mutex();
3571
+ warnedMissing = false;
3572
+ warnedStructural = false;
3573
+ }
3574
+ });
3509
3575
  function encodeProjectDir(cwd) {
3510
3576
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
3511
3577
  }
@@ -3732,70 +3798,31 @@ var init_session_transcript = __esm({
3732
3798
  };
3733
3799
  }
3734
3800
  });
3735
-
3736
- // src/internal/persistence/file-lock.ts
3737
- async function getProperLockfile() {
3738
- if (cached !== void 0) return cached;
3739
- try {
3740
- const mod = await import('proper-lockfile');
3741
- if (!validateLockModule(mod)) {
3742
- if (!warnedStructural) {
3743
- warnedStructural = true;
3744
- process.stderr.write(
3745
- "[theokit-sdk] proper-lockfile: imported module does NOT expose the expected `lock`/`unlock` API surface. This may indicate a supply-chain compromise or an incompatible major version. Falling back to in-process mutex (no cross-process safety). Reinstall with: pnpm add proper-lockfile@^11\n"
3746
- );
3747
- }
3748
- cached = null;
3749
- return cached;
3750
- }
3751
- cached = mod;
3752
- } catch {
3753
- cached = null;
3754
- }
3755
- return cached;
3756
- }
3757
- function validateLockModule(mod) {
3758
- if (mod === null || mod === void 0 || typeof mod !== "object") return false;
3759
- const m = mod;
3760
- return typeof m.lock === "function" && typeof m.unlock === "function";
3761
- }
3762
- async function withFileLock(path, fn, options) {
3763
- const lib = await getProperLockfile();
3764
- if (lib === null) {
3765
- if (!warnedMissing) {
3766
- warnedMissing = true;
3767
- process.stderr.write(
3768
- "[theokit-sdk] proper-lockfile not installed; cross-process file lock unavailable. Install with: pnpm add proper-lockfile\n"
3769
- );
3770
- }
3771
- return withCwdMutex(`file-lock:${path}`, fn);
3772
- }
3773
- return withCwdMutex(`file-lock:${path}`, async () => {
3774
- const release = await lib.lock(path, {
3775
- // EC-1: companion lockfile, target path may not exist yet.
3776
- lockfilePath: `${path}.lock`,
3777
- realpath: false,
3778
- stale: 3e4,
3779
- retries: {
3780
- retries: 5,
3781
- factor: 1.5,
3782
- minTimeout: 100,
3783
- maxTimeout: 5e3
3801
+ var FsSessionStore;
3802
+ var init_fs_session_store = __esm({
3803
+ "src/internal/persistence/fs-session-store.ts"() {
3804
+ init_file_lock();
3805
+ init_session_transcript();
3806
+ FsSessionStore = class {
3807
+ #baseDir;
3808
+ #cwd;
3809
+ constructor(options) {
3810
+ this.#baseDir = options.baseDir;
3811
+ this.#cwd = options.cwd;
3812
+ }
3813
+ async readRecords(agentId) {
3814
+ return readTranscript(transcriptPath(this.#baseDir, this.#cwd, agentId));
3815
+ }
3816
+ async appendRecords(agentId, records) {
3817
+ if (records.length === 0) return;
3818
+ const path$1 = transcriptPath(this.#baseDir, this.#cwd, agentId);
3819
+ await promises.mkdir(path.dirname(path$1), { recursive: true });
3820
+ await withFileLock(path$1, async () => {
3821
+ const prior = await readTranscript(path$1);
3822
+ await writeTranscript(path$1, [...prior, ...records]);
3823
+ });
3784
3824
  }
3785
- });
3786
- try {
3787
- return await fn();
3788
- } finally {
3789
- await release();
3790
- }
3791
- });
3792
- }
3793
- var cached, warnedMissing, warnedStructural;
3794
- var init_file_lock = __esm({
3795
- "src/internal/persistence/file-lock.ts"() {
3796
- init_cwd_mutex();
3797
- warnedMissing = false;
3798
- warnedStructural = false;
3825
+ };
3799
3826
  }
3800
3827
  });
3801
3828
  function getTheokitHome(cwd) {
@@ -5127,6 +5154,8 @@ var init_memory_path_selector = __esm({
5127
5154
  PORT_MEMORY_PATH_ENV_VAR = "THEOKIT_PORT_MEMORY_PATH";
5128
5155
  }
5129
5156
  });
5157
+
5158
+ // src/internal/runtime/session/agent-session-store.ts
5130
5159
  function seedTranscript(prior, opts) {
5131
5160
  return SessionTranscript.fromRecords(prior, opts);
5132
5161
  }
@@ -5164,8 +5193,8 @@ function appendConversation(transcript, conversation) {
5164
5193
  if (toolResults.length > 0) transcript.appendToolResults(toolResults);
5165
5194
  }
5166
5195
  }
5167
- async function readSessionMessages(baseDir, cwd, agentId) {
5168
- const records = await readTranscript(transcriptPath(baseDir, cwd, agentId));
5196
+ async function readSessionMessages(store, agentId) {
5197
+ const records = await store.readRecords(agentId);
5169
5198
  return reconstructMessages(records).map(narrowToSessionMessage);
5170
5199
  }
5171
5200
  function partToText(p) {
@@ -5182,37 +5211,31 @@ function narrowToSessionMessage(m) {
5182
5211
  const text = m.content.map(partToText).filter((s) => s.length > 0).join("\n");
5183
5212
  return { role, text };
5184
5213
  }
5185
- async function persistTurn(loc, sessionId, turn) {
5186
- const path$1 = transcriptPath(loc.baseDir, loc.cwd, loc.agentId);
5187
- await promises.mkdir(path.dirname(path$1), { recursive: true });
5188
- await withFileLock(path$1, async () => {
5189
- const prior = await readTranscript(path$1);
5190
- const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5191
- transcript.appendUserTurn(turn.userText);
5192
- appendConversation(transcript, turn.conversation);
5193
- await writeTranscript(path$1, transcript.records());
5194
- });
5214
+ function deltaRecords(transcript, priorLength) {
5215
+ return transcript.records().slice(priorLength);
5195
5216
  }
5196
- async function appendCompactBoundaryRecord(loc, sessionId, meta) {
5197
- const path$1 = transcriptPath(loc.baseDir, loc.cwd, loc.agentId);
5198
- await promises.mkdir(path.dirname(path$1), { recursive: true });
5199
- await withFileLock(path$1, async () => {
5200
- const prior = await readTranscript(path$1);
5201
- const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5202
- transcript.appendCompactBoundary(meta);
5203
- await writeTranscript(path$1, transcript.records());
5204
- });
5217
+ async function persistTurn(store, loc, sessionId, turn) {
5218
+ const prior = await store.readRecords(loc.agentId);
5219
+ const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5220
+ transcript.appendUserTurn(turn.userText);
5221
+ appendConversation(transcript, turn.conversation);
5222
+ await store.appendRecords(loc.agentId, deltaRecords(transcript, prior.length));
5223
+ }
5224
+ async function appendCompactBoundaryRecord(store, loc, sessionId, meta) {
5225
+ const prior = await store.readRecords(loc.agentId);
5226
+ const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5227
+ transcript.appendCompactBoundary(meta);
5228
+ await store.appendRecords(loc.agentId, deltaRecords(transcript, prior.length));
5205
5229
  }
5206
5230
  var init_agent_session_store = __esm({
5207
5231
  "src/internal/runtime/session/agent-session-store.ts"() {
5208
- init_file_lock();
5209
5232
  init_session_transcript();
5210
5233
  }
5211
5234
  });
5212
5235
 
5213
5236
  // src/internal/runtime/session/agent-session.ts
5214
- function transcriptKey(baseDir, cwd, agentId) {
5215
- return `${baseDir}::${cwd}::${agentId}`;
5237
+ function transcriptKey(cwd, agentId) {
5238
+ return `${cwd}::${agentId}`;
5216
5239
  }
5217
5240
  function appendSessionMessage(agentId, message) {
5218
5241
  const existing = sessions.get(agentId) ?? [];
@@ -5222,15 +5245,15 @@ function appendSessionMessage(agentId, message) {
5222
5245
  function getSessionMessages(agentId) {
5223
5246
  return sessions.get(agentId) ?? [];
5224
5247
  }
5225
- function persistTurnToTranscript(loc, sessionId, turn, onCompact) {
5226
- const key = transcriptKey(loc.baseDir, loc.cwd, loc.agentId);
5248
+ function persistTurnToTranscript(store, loc, sessionId, turn, onCompact) {
5249
+ const key = transcriptKey(loc.cwd, loc.agentId);
5227
5250
  const chained = (pendingWrites.get(key) ?? Promise.resolve()).then(async () => {
5228
5251
  try {
5229
- await persistTurn(loc, sessionId, turn);
5252
+ await persistTurn(store, loc, sessionId, turn);
5230
5253
  const count = (recordCounts.get(key) ?? 0) + 1;
5231
5254
  recordCounts.set(key, count);
5232
5255
  if (count % COMPACTION_CHECK_INTERVAL === 0) {
5233
- await appendCompactBoundaryRecord(loc, sessionId, {
5256
+ await appendCompactBoundaryRecord(store, loc, sessionId, {
5234
5257
  preTokens: 0,
5235
5258
  trigger: "auto"
5236
5259
  });
@@ -5253,10 +5276,10 @@ function persistTurnToTranscript(loc, sessionId, turn, onCompact) {
5253
5276
  );
5254
5277
  }
5255
5278
  async function hydrateSession(agentId, loc) {
5256
- const key = transcriptKey(loc.baseDir, loc.cwd, agentId);
5279
+ const key = transcriptKey(loc.cwd, agentId);
5257
5280
  if (hydratedKeys.has(key)) return;
5258
5281
  hydratedKeys.add(key);
5259
- const persisted = await readSessionMessages(loc.baseDir, loc.cwd, agentId);
5282
+ const persisted = await readSessionMessages(loc.store, agentId);
5260
5283
  if (persisted.length === 0) return;
5261
5284
  if (!sessions.has(agentId) || sessions.get(agentId)?.length === 0) {
5262
5285
  sessions.set(agentId, persisted);
@@ -5291,7 +5314,7 @@ async function runPostRunLifecycle(inputs) {
5291
5314
  userText,
5292
5315
  agentId,
5293
5316
  workspaceCwd,
5294
- baseDir,
5317
+ sessionStore,
5295
5318
  model,
5296
5319
  onRunEvent,
5297
5320
  hooksExecutor,
@@ -5310,7 +5333,8 @@ async function runPostRunLifecycle(inputs) {
5310
5333
  }
5311
5334
  const conversation = await safeConversation(run);
5312
5335
  persistTurnToTranscript(
5313
- { baseDir, cwd: workspaceCwd, agentId, model },
5336
+ sessionStore,
5337
+ { cwd: workspaceCwd, agentId, model },
5314
5338
  agentId,
5315
5339
  { userText, conversation },
5316
5340
  onRunEvent !== void 0 ? () => emitRunEvent(onRunEvent, { type: "compact_boundary", trigger: "auto" }) : void 0
@@ -17758,6 +17782,7 @@ var init_local_agent = __esm({
17758
17782
  init_errors();
17759
17783
  init_ids();
17760
17784
  init_cwd_mutex();
17785
+ init_fs_session_store();
17761
17786
  init_session_transcript();
17762
17787
  init_store();
17763
17788
  init_manager();
@@ -17801,6 +17826,12 @@ var init_local_agent = __esm({
17801
17826
  * `~/.theokit`; set `local.baseDir: "~/.claude"` for Claude Code CLI interop.
17802
17827
  */
17803
17828
  transcriptBaseDir;
17829
+ /**
17830
+ * SE41 — the session record store. Defaults to the FS transcript store (byte-
17831
+ * identical to SE40); `local.sessionStore` injects an external store (Postgres /
17832
+ * Redis / KV) so resume works on serverless (ephemeral FS) and multi-host.
17833
+ */
17834
+ sessionStore;
17804
17835
  /**
17805
17836
  * D319: lifecycle AbortController fired on `dispose()`. Composed with the
17806
17837
  * caller's `SendOptions.signal` via `anySignal` so the LLM `fetch()`
@@ -17843,6 +17874,7 @@ var init_local_agent = __esm({
17843
17874
  this.options = options;
17844
17875
  this.workspaceCwd = resolveCwd(options.local?.cwd);
17845
17876
  this.transcriptBaseDir = resolveBaseDir(options.local?.baseDir);
17877
+ this.sessionStore = options.local?.sessionStore ?? new FsSessionStore({ baseDir: this.transcriptBaseDir, cwd: this.workspaceCwd });
17846
17878
  this.settingSourcesIncludeProject = includesSetting(options, "project");
17847
17879
  this.settingSourcesIncludePlugins = includesSetting(options, "plugins");
17848
17880
  const sub = bootstrapSubmanagers({
@@ -17890,7 +17922,7 @@ var init_local_agent = __esm({
17890
17922
  this.settingSourcesIncludeProject,
17891
17923
  this.options.agents
17892
17924
  );
17893
- await hydrateSession(this.agentId, { baseDir: this.transcriptBaseDir, cwd: this.workspaceCwd });
17925
+ await hydrateSession(this.agentId, { store: this.sessionStore, cwd: this.workspaceCwd });
17894
17926
  await this.personalityStore.hydrate(this.agentId);
17895
17927
  }
17896
17928
  /** T4.2 — expose PluginManager so agent-loop can fire pre_tool_call hooks. @internal */
@@ -17946,7 +17978,7 @@ var init_local_agent = __esm({
17946
17978
  userText,
17947
17979
  agentId: this.agentId,
17948
17980
  workspaceCwd: this.workspaceCwd,
17949
- baseDir: this.transcriptBaseDir,
17981
+ sessionStore: this.sessionStore,
17950
17982
  model: this.model?.id ?? "unknown",
17951
17983
  ...options.onRunEvent !== void 0 ? { onRunEvent: options.onRunEvent } : {},
17952
17984
  hooksExecutor: this.hooksExecutor,