@theokit/sdk 4.0.1 → 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/dist/index.cjs CHANGED
@@ -3520,6 +3520,72 @@ var init_cloud_tool_parity = __esm({
3520
3520
  init_errors();
3521
3521
  }
3522
3522
  });
3523
+
3524
+ // src/internal/persistence/file-lock.ts
3525
+ async function getProperLockfile() {
3526
+ if (cached !== void 0) return cached;
3527
+ try {
3528
+ const mod = await import('proper-lockfile');
3529
+ if (!validateLockModule(mod)) {
3530
+ if (!warnedStructural) {
3531
+ warnedStructural = true;
3532
+ process.stderr.write(
3533
+ "[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"
3534
+ );
3535
+ }
3536
+ cached = null;
3537
+ return cached;
3538
+ }
3539
+ cached = mod;
3540
+ } catch {
3541
+ cached = null;
3542
+ }
3543
+ return cached;
3544
+ }
3545
+ function validateLockModule(mod) {
3546
+ if (mod === null || mod === void 0 || typeof mod !== "object") return false;
3547
+ const m = mod;
3548
+ return typeof m.lock === "function" && typeof m.unlock === "function";
3549
+ }
3550
+ async function withFileLock(path, fn, options) {
3551
+ const lib = await getProperLockfile();
3552
+ if (lib === null) {
3553
+ if (!warnedMissing) {
3554
+ warnedMissing = true;
3555
+ process.stderr.write(
3556
+ "[theokit-sdk] proper-lockfile not installed; cross-process file lock unavailable. Install with: pnpm add proper-lockfile\n"
3557
+ );
3558
+ }
3559
+ return withCwdMutex(`file-lock:${path}`, fn);
3560
+ }
3561
+ return withCwdMutex(`file-lock:${path}`, async () => {
3562
+ const release = await lib.lock(path, {
3563
+ // EC-1: companion lockfile, target path may not exist yet.
3564
+ lockfilePath: `${path}.lock`,
3565
+ realpath: false,
3566
+ stale: 3e4,
3567
+ retries: {
3568
+ retries: 5,
3569
+ factor: 1.5,
3570
+ minTimeout: 100,
3571
+ maxTimeout: 5e3
3572
+ }
3573
+ });
3574
+ try {
3575
+ return await fn();
3576
+ } finally {
3577
+ await release();
3578
+ }
3579
+ });
3580
+ }
3581
+ var cached, warnedMissing, warnedStructural;
3582
+ var init_file_lock = __esm({
3583
+ "src/internal/persistence/file-lock.ts"() {
3584
+ init_cwd_mutex();
3585
+ warnedMissing = false;
3586
+ warnedStructural = false;
3587
+ }
3588
+ });
3523
3589
  function encodeProjectDir(cwd) {
3524
3590
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
3525
3591
  }
@@ -3746,70 +3812,31 @@ var init_session_transcript = __esm({
3746
3812
  };
3747
3813
  }
3748
3814
  });
3749
-
3750
- // src/internal/persistence/file-lock.ts
3751
- async function getProperLockfile() {
3752
- if (cached !== void 0) return cached;
3753
- try {
3754
- const mod = await import('proper-lockfile');
3755
- if (!validateLockModule(mod)) {
3756
- if (!warnedStructural) {
3757
- warnedStructural = true;
3758
- process.stderr.write(
3759
- "[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"
3760
- );
3761
- }
3762
- cached = null;
3763
- return cached;
3764
- }
3765
- cached = mod;
3766
- } catch {
3767
- cached = null;
3768
- }
3769
- return cached;
3770
- }
3771
- function validateLockModule(mod) {
3772
- if (mod === null || mod === void 0 || typeof mod !== "object") return false;
3773
- const m = mod;
3774
- return typeof m.lock === "function" && typeof m.unlock === "function";
3775
- }
3776
- async function withFileLock(path, fn, options) {
3777
- const lib = await getProperLockfile();
3778
- if (lib === null) {
3779
- if (!warnedMissing) {
3780
- warnedMissing = true;
3781
- process.stderr.write(
3782
- "[theokit-sdk] proper-lockfile not installed; cross-process file lock unavailable. Install with: pnpm add proper-lockfile\n"
3783
- );
3784
- }
3785
- return withCwdMutex(`file-lock:${path}`, fn);
3786
- }
3787
- return withCwdMutex(`file-lock:${path}`, async () => {
3788
- const release = await lib.lock(path, {
3789
- // EC-1: companion lockfile, target path may not exist yet.
3790
- lockfilePath: `${path}.lock`,
3791
- realpath: false,
3792
- stale: 3e4,
3793
- retries: {
3794
- retries: 5,
3795
- factor: 1.5,
3796
- minTimeout: 100,
3797
- maxTimeout: 5e3
3815
+ var FsSessionStore;
3816
+ var init_fs_session_store = __esm({
3817
+ "src/internal/persistence/fs-session-store.ts"() {
3818
+ init_file_lock();
3819
+ init_session_transcript();
3820
+ FsSessionStore = class {
3821
+ #baseDir;
3822
+ #cwd;
3823
+ constructor(options) {
3824
+ this.#baseDir = options.baseDir;
3825
+ this.#cwd = options.cwd;
3826
+ }
3827
+ async readRecords(agentId) {
3828
+ return readTranscript(transcriptPath(this.#baseDir, this.#cwd, agentId));
3829
+ }
3830
+ async appendRecords(agentId, records) {
3831
+ if (records.length === 0) return;
3832
+ const path$1 = transcriptPath(this.#baseDir, this.#cwd, agentId);
3833
+ await promises.mkdir(path.dirname(path$1), { recursive: true });
3834
+ await withFileLock(path$1, async () => {
3835
+ const prior = await readTranscript(path$1);
3836
+ await writeTranscript(path$1, [...prior, ...records]);
3837
+ });
3798
3838
  }
3799
- });
3800
- try {
3801
- return await fn();
3802
- } finally {
3803
- await release();
3804
- }
3805
- });
3806
- }
3807
- var cached, warnedMissing, warnedStructural;
3808
- var init_file_lock = __esm({
3809
- "src/internal/persistence/file-lock.ts"() {
3810
- init_cwd_mutex();
3811
- warnedMissing = false;
3812
- warnedStructural = false;
3839
+ };
3813
3840
  }
3814
3841
  });
3815
3842
  function getTheokitHome(cwd) {
@@ -5141,6 +5168,8 @@ var init_memory_path_selector = __esm({
5141
5168
  PORT_MEMORY_PATH_ENV_VAR = "THEOKIT_PORT_MEMORY_PATH";
5142
5169
  }
5143
5170
  });
5171
+
5172
+ // src/internal/runtime/session/agent-session-store.ts
5144
5173
  function seedTranscript(prior, opts) {
5145
5174
  return SessionTranscript.fromRecords(prior, opts);
5146
5175
  }
@@ -5178,8 +5207,8 @@ function appendConversation(transcript, conversation) {
5178
5207
  if (toolResults.length > 0) transcript.appendToolResults(toolResults);
5179
5208
  }
5180
5209
  }
5181
- async function readSessionMessages(baseDir, cwd, agentId) {
5182
- const records = await readTranscript(transcriptPath(baseDir, cwd, agentId));
5210
+ async function readSessionMessages(store, agentId) {
5211
+ const records = await store.readRecords(agentId);
5183
5212
  return reconstructMessages(records).map(narrowToSessionMessage);
5184
5213
  }
5185
5214
  function partToText(p) {
@@ -5196,37 +5225,31 @@ function narrowToSessionMessage(m) {
5196
5225
  const text = m.content.map(partToText).filter((s) => s.length > 0).join("\n");
5197
5226
  return { role, text };
5198
5227
  }
5199
- async function persistTurn(loc, sessionId, turn) {
5200
- const path$1 = transcriptPath(loc.baseDir, loc.cwd, loc.agentId);
5201
- await promises.mkdir(path.dirname(path$1), { recursive: true });
5202
- await withFileLock(path$1, async () => {
5203
- const prior = await readTranscript(path$1);
5204
- const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5205
- transcript.appendUserTurn(turn.userText);
5206
- appendConversation(transcript, turn.conversation);
5207
- await writeTranscript(path$1, transcript.records());
5208
- });
5228
+ function deltaRecords(transcript, priorLength) {
5229
+ return transcript.records().slice(priorLength);
5209
5230
  }
5210
- async function appendCompactBoundaryRecord(loc, sessionId, meta) {
5211
- const path$1 = transcriptPath(loc.baseDir, loc.cwd, loc.agentId);
5212
- await promises.mkdir(path.dirname(path$1), { recursive: true });
5213
- await withFileLock(path$1, async () => {
5214
- const prior = await readTranscript(path$1);
5215
- const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5216
- transcript.appendCompactBoundary(meta);
5217
- await writeTranscript(path$1, transcript.records());
5218
- });
5231
+ async function persistTurn(store, loc, sessionId, turn) {
5232
+ const prior = await store.readRecords(loc.agentId);
5233
+ const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5234
+ transcript.appendUserTurn(turn.userText);
5235
+ appendConversation(transcript, turn.conversation);
5236
+ await store.appendRecords(loc.agentId, deltaRecords(transcript, prior.length));
5237
+ }
5238
+ async function appendCompactBoundaryRecord(store, loc, sessionId, meta) {
5239
+ const prior = await store.readRecords(loc.agentId);
5240
+ const transcript = seedTranscript(prior, { cwd: loc.cwd, sessionId, model: loc.model });
5241
+ transcript.appendCompactBoundary(meta);
5242
+ await store.appendRecords(loc.agentId, deltaRecords(transcript, prior.length));
5219
5243
  }
5220
5244
  var init_agent_session_store = __esm({
5221
5245
  "src/internal/runtime/session/agent-session-store.ts"() {
5222
- init_file_lock();
5223
5246
  init_session_transcript();
5224
5247
  }
5225
5248
  });
5226
5249
 
5227
5250
  // src/internal/runtime/session/agent-session.ts
5228
- function transcriptKey(baseDir, cwd, agentId) {
5229
- return `${baseDir}::${cwd}::${agentId}`;
5251
+ function transcriptKey(cwd, agentId) {
5252
+ return `${cwd}::${agentId}`;
5230
5253
  }
5231
5254
  function appendSessionMessage(agentId, message) {
5232
5255
  const existing = sessions.get(agentId) ?? [];
@@ -5236,15 +5259,15 @@ function appendSessionMessage(agentId, message) {
5236
5259
  function getSessionMessages(agentId) {
5237
5260
  return sessions.get(agentId) ?? [];
5238
5261
  }
5239
- function persistTurnToTranscript(loc, sessionId, turn, onCompact) {
5240
- const key2 = transcriptKey(loc.baseDir, loc.cwd, loc.agentId);
5262
+ function persistTurnToTranscript(store, loc, sessionId, turn, onCompact) {
5263
+ const key2 = transcriptKey(loc.cwd, loc.agentId);
5241
5264
  const chained = (pendingWrites.get(key2) ?? Promise.resolve()).then(async () => {
5242
5265
  try {
5243
- await persistTurn(loc, sessionId, turn);
5266
+ await persistTurn(store, loc, sessionId, turn);
5244
5267
  const count = (recordCounts.get(key2) ?? 0) + 1;
5245
5268
  recordCounts.set(key2, count);
5246
5269
  if (count % COMPACTION_CHECK_INTERVAL === 0) {
5247
- await appendCompactBoundaryRecord(loc, sessionId, {
5270
+ await appendCompactBoundaryRecord(store, loc, sessionId, {
5248
5271
  preTokens: 0,
5249
5272
  trigger: "auto"
5250
5273
  });
@@ -5267,10 +5290,10 @@ function persistTurnToTranscript(loc, sessionId, turn, onCompact) {
5267
5290
  );
5268
5291
  }
5269
5292
  async function hydrateSession(agentId, loc) {
5270
- const key2 = transcriptKey(loc.baseDir, loc.cwd, agentId);
5293
+ const key2 = transcriptKey(loc.cwd, agentId);
5271
5294
  if (hydratedKeys.has(key2)) return;
5272
5295
  hydratedKeys.add(key2);
5273
- const persisted = await readSessionMessages(loc.baseDir, loc.cwd, agentId);
5296
+ const persisted = await readSessionMessages(loc.store, agentId);
5274
5297
  if (persisted.length === 0) return;
5275
5298
  if (!sessions.has(agentId) || sessions.get(agentId)?.length === 0) {
5276
5299
  sessions.set(agentId, persisted);
@@ -5305,7 +5328,7 @@ async function runPostRunLifecycle(inputs) {
5305
5328
  userText,
5306
5329
  agentId,
5307
5330
  workspaceCwd,
5308
- baseDir,
5331
+ sessionStore,
5309
5332
  model,
5310
5333
  onRunEvent,
5311
5334
  hooksExecutor,
@@ -5324,7 +5347,8 @@ async function runPostRunLifecycle(inputs) {
5324
5347
  }
5325
5348
  const conversation = await safeConversation(run);
5326
5349
  persistTurnToTranscript(
5327
- { baseDir, cwd: workspaceCwd, agentId, model },
5350
+ sessionStore,
5351
+ { cwd: workspaceCwd, agentId, model },
5328
5352
  agentId,
5329
5353
  { userText, conversation },
5330
5354
  onRunEvent !== void 0 ? () => emitRunEvent(onRunEvent, { type: "compact_boundary", trigger: "auto" }) : void 0
@@ -18139,6 +18163,7 @@ var init_local_agent = __esm({
18139
18163
  init_errors();
18140
18164
  init_ids();
18141
18165
  init_cwd_mutex();
18166
+ init_fs_session_store();
18142
18167
  init_session_transcript();
18143
18168
  init_store();
18144
18169
  init_manager();
@@ -18182,6 +18207,12 @@ var init_local_agent = __esm({
18182
18207
  * `~/.theokit`; set `local.baseDir: "~/.claude"` for Claude Code CLI interop.
18183
18208
  */
18184
18209
  transcriptBaseDir;
18210
+ /**
18211
+ * SE41 — the session record store. Defaults to the FS transcript store (byte-
18212
+ * identical to SE40); `local.sessionStore` injects an external store (Postgres /
18213
+ * Redis / KV) so resume works on serverless (ephemeral FS) and multi-host.
18214
+ */
18215
+ sessionStore;
18185
18216
  /**
18186
18217
  * D319: lifecycle AbortController fired on `dispose()`. Composed with the
18187
18218
  * caller's `SendOptions.signal` via `anySignal` so the LLM `fetch()`
@@ -18224,6 +18255,7 @@ var init_local_agent = __esm({
18224
18255
  this.options = options;
18225
18256
  this.workspaceCwd = resolveCwd(options.local?.cwd);
18226
18257
  this.transcriptBaseDir = resolveBaseDir(options.local?.baseDir);
18258
+ this.sessionStore = options.local?.sessionStore ?? new FsSessionStore({ baseDir: this.transcriptBaseDir, cwd: this.workspaceCwd });
18227
18259
  this.settingSourcesIncludeProject = includesSetting(options, "project");
18228
18260
  this.settingSourcesIncludePlugins = includesSetting(options, "plugins");
18229
18261
  const sub = bootstrapSubmanagers({
@@ -18271,7 +18303,7 @@ var init_local_agent = __esm({
18271
18303
  this.settingSourcesIncludeProject,
18272
18304
  this.options.agents
18273
18305
  );
18274
- await hydrateSession(this.agentId, { baseDir: this.transcriptBaseDir, cwd: this.workspaceCwd });
18306
+ await hydrateSession(this.agentId, { store: this.sessionStore, cwd: this.workspaceCwd });
18275
18307
  await this.personalityStore.hydrate(this.agentId);
18276
18308
  }
18277
18309
  /** T4.2 — expose PluginManager so agent-loop can fire pre_tool_call hooks. @internal */
@@ -18327,7 +18359,7 @@ var init_local_agent = __esm({
18327
18359
  userText,
18328
18360
  agentId: this.agentId,
18329
18361
  workspaceCwd: this.workspaceCwd,
18330
- baseDir: this.transcriptBaseDir,
18362
+ sessionStore: this.sessionStore,
18331
18363
  model: this.model?.id ?? "unknown",
18332
18364
  ...options.onRunEvent !== void 0 ? { onRunEvent: options.onRunEvent } : {},
18333
18365
  hooksExecutor: this.hooksExecutor,