@theokit/sdk 4.28.0 → 4.30.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.
Files changed (90) hide show
  1. package/dist/agent-7YUGJJTU.js +41 -0
  2. package/dist/agent-7YUGJJTU.js.map +1 -0
  3. package/dist/{cron-BN6JJlDw.d.ts → agent-Bzg2_uGr.d.cts} +56 -577
  4. package/dist/{cron-CoQREV_k.d.cts → agent-Ekv8y7_c.d.ts} +56 -577
  5. package/dist/agent-LD76RI74.cjs +50 -0
  6. package/dist/agent-LD76RI74.cjs.map +1 -0
  7. package/dist/{chunk-AKSBP7OO.js → chunk-5IB2UKSN.js} +5 -41
  8. package/dist/chunk-5IB2UKSN.js.map +1 -0
  9. package/dist/{chunk-PF5Y5BHG.js → chunk-5YCAR23S.js} +3 -3
  10. package/dist/{chunk-PF5Y5BHG.js.map → chunk-5YCAR23S.js.map} +1 -1
  11. package/dist/chunk-BNIFFFH3.js +129 -0
  12. package/dist/chunk-BNIFFFH3.js.map +1 -0
  13. package/dist/{chunk-LSLPF6OI.cjs → chunk-DHLZIMP6.cjs} +5 -43
  14. package/dist/chunk-DHLZIMP6.cjs.map +1 -0
  15. package/dist/chunk-FKCC2MED.js +43 -0
  16. package/dist/chunk-FKCC2MED.js.map +1 -0
  17. package/dist/{chunk-PN53WGEI.cjs → chunk-GWMQ5CS3.cjs} +633 -741
  18. package/dist/chunk-GWMQ5CS3.cjs.map +1 -0
  19. package/dist/{chunk-QUTUJ6P6.cjs → chunk-HK7ZHSAX.cjs} +25 -16
  20. package/dist/chunk-HK7ZHSAX.cjs.map +1 -0
  21. package/dist/{chunk-XW6RXVGB.js → chunk-ISZ3MBUD.js} +610 -718
  22. package/dist/chunk-ISZ3MBUD.js.map +1 -0
  23. package/dist/{chunk-MQLCZ4HJ.cjs → chunk-JLMXKTBX.cjs} +4 -4
  24. package/dist/{chunk-MQLCZ4HJ.cjs.map → chunk-JLMXKTBX.cjs.map} +1 -1
  25. package/dist/chunk-LALRCFSN.cjs +46 -0
  26. package/dist/chunk-LALRCFSN.cjs.map +1 -0
  27. package/dist/{chunk-I53QDJVI.cjs → chunk-NG6B2PMA.cjs} +5 -5
  28. package/dist/{chunk-I53QDJVI.cjs.map → chunk-NG6B2PMA.cjs.map} +1 -1
  29. package/dist/{chunk-DUK5WMKJ.js → chunk-R6ZP6KNB.js} +25 -16
  30. package/dist/chunk-R6ZP6KNB.js.map +1 -0
  31. package/dist/chunk-RZQ6E5YZ.cjs +131 -0
  32. package/dist/chunk-RZQ6E5YZ.cjs.map +1 -0
  33. package/dist/{chunk-G6GO6BXD.js → chunk-XAUBQMQU.js} +3 -3
  34. package/dist/{chunk-G6GO6BXD.js.map → chunk-XAUBQMQU.js.map} +1 -1
  35. package/dist/compact-session-OO2GD76G.js +1 -1
  36. package/dist/compact-session-OO7NVWPB.cjs +1 -1
  37. package/dist/cron-CjzvXGT2.d.cts +579 -0
  38. package/dist/cron-D0Z29gI-.d.ts +579 -0
  39. package/dist/cron.cjs +13 -11
  40. package/dist/cron.d.cts +2 -1
  41. package/dist/cron.d.ts +2 -1
  42. package/dist/cron.js +12 -10
  43. package/dist/eval.cjs +17 -15
  44. package/dist/eval.cjs.map +1 -1
  45. package/dist/eval.js +13 -11
  46. package/dist/eval.js.map +1 -1
  47. package/dist/{index-manager-CB6I7XFQ.cjs → index-manager-H7HUMW52.cjs} +5 -5
  48. package/dist/{index-manager-CB6I7XFQ.cjs.map → index-manager-H7HUMW52.cjs.map} +1 -1
  49. package/dist/{index-manager-L27Q3QAC.js → index-manager-P7ZUX2RG.js} +4 -4
  50. package/dist/{index-manager-L27Q3QAC.js.map → index-manager-P7ZUX2RG.js.map} +1 -1
  51. package/dist/index.cjs +65 -44
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +18 -4
  54. package/dist/index.d.ts +18 -4
  55. package/dist/index.js +41 -20
  56. package/dist/index.js.map +1 -1
  57. package/dist/internal/persistence/index.cjs +10 -10
  58. package/dist/internal/persistence/index.js +2 -2
  59. package/dist/internal/persistence/jsonl.d.cts +7 -0
  60. package/dist/internal/persistence/jsonl.d.ts +7 -0
  61. package/dist/internal/persistence/session-writer.d.cts +56 -0
  62. package/dist/internal/persistence/session-writer.d.ts +56 -0
  63. package/dist/internal/persistence/transcript-ops.d.cts +71 -0
  64. package/dist/internal/persistence/transcript-ops.d.ts +71 -0
  65. package/dist/internal/plugins/types.d.cts +1 -1
  66. package/dist/internal/plugins/types.d.ts +1 -1
  67. package/dist/persistence.cjs +115 -12
  68. package/dist/persistence.cjs.map +1 -1
  69. package/dist/persistence.d.cts +2 -0
  70. package/dist/persistence.d.ts +2 -0
  71. package/dist/persistence.js +105 -3
  72. package/dist/persistence.js.map +1 -1
  73. package/dist/skills.cjs +4 -3
  74. package/dist/skills.js +2 -1
  75. package/dist/subagents-loader.cjs +19 -0
  76. package/dist/subagents-loader.cjs.map +1 -0
  77. package/dist/subagents-loader.d.cts +39 -0
  78. package/dist/subagents-loader.d.ts +39 -0
  79. package/dist/subagents-loader.js +16 -0
  80. package/dist/subagents-loader.js.map +1 -0
  81. package/dist/types/plugin.d.ts +32 -0
  82. package/dist/workflow.cjs +1 -1
  83. package/dist/workflow.js +1 -1
  84. package/package.json +12 -2
  85. package/dist/chunk-AKSBP7OO.js.map +0 -1
  86. package/dist/chunk-DUK5WMKJ.js.map +0 -1
  87. package/dist/chunk-LSLPF6OI.cjs.map +0 -1
  88. package/dist/chunk-PN53WGEI.cjs.map +0 -1
  89. package/dist/chunk-QUTUJ6P6.cjs.map +0 -1
  90. package/dist/chunk-XW6RXVGB.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var chunkQUTUJ6P6_cjs = require('../../chunk-QUTUJ6P6.cjs');
3
+ var chunkHK7ZHSAX_cjs = require('../../chunk-HK7ZHSAX.cjs');
4
4
  var chunkPNVDQL5Y_cjs = require('../../chunk-PNVDQL5Y.cjs');
5
5
  var chunkOIYQBUW4_cjs = require('../../chunk-OIYQBUW4.cjs');
6
- var chunkJD74LN7R_cjs = require('../../chunk-JD74LN7R.cjs');
7
6
  var chunkC7PICHRF_cjs = require('../../chunk-C7PICHRF.cjs');
7
+ var chunkJD74LN7R_cjs = require('../../chunk-JD74LN7R.cjs');
8
8
  var chunkYLQQX5W2_cjs = require('../../chunk-YLQQX5W2.cjs');
9
9
  var chunkTIZ4TS5V_cjs = require('../../chunk-TIZ4TS5V.cjs');
10
10
  require('../../chunk-A44COQJJ.cjs');
@@ -38,19 +38,19 @@ function casUpdate(db, sql, params, expectedChanges = 1) {
38
38
 
39
39
  Object.defineProperty(exports, "JsonlParseError", {
40
40
  enumerable: true,
41
- get: function () { return chunkQUTUJ6P6_cjs.JsonlParseError; }
41
+ get: function () { return chunkHK7ZHSAX_cjs.JsonlParseError; }
42
42
  });
43
43
  Object.defineProperty(exports, "appendJsonl", {
44
44
  enumerable: true,
45
- get: function () { return chunkQUTUJ6P6_cjs.appendJsonl; }
45
+ get: function () { return chunkHK7ZHSAX_cjs.appendJsonl; }
46
46
  });
47
47
  Object.defineProperty(exports, "loadJsonl", {
48
48
  enumerable: true,
49
- get: function () { return chunkQUTUJ6P6_cjs.loadJsonl; }
49
+ get: function () { return chunkHK7ZHSAX_cjs.loadJsonl; }
50
50
  });
51
51
  Object.defineProperty(exports, "readJsonlIds", {
52
52
  enumerable: true,
53
- get: function () { return chunkQUTUJ6P6_cjs.readJsonlIds; }
53
+ get: function () { return chunkHK7ZHSAX_cjs.readJsonlIds; }
54
54
  });
55
55
  Object.defineProperty(exports, "PersistenceSchema", {
56
56
  enumerable: true,
@@ -80,6 +80,10 @@ Object.defineProperty(exports, "writeVersionedJson", {
80
80
  enumerable: true,
81
81
  get: function () { return chunkOIYQBUW4_cjs.writeVersionedJson; }
82
82
  });
83
+ Object.defineProperty(exports, "withFileLock", {
84
+ enumerable: true,
85
+ get: function () { return chunkC7PICHRF_cjs.withFileLock; }
86
+ });
83
87
  Object.defineProperty(exports, "applyWalWithFallback", {
84
88
  enumerable: true,
85
89
  get: function () { return chunkJD74LN7R_cjs.applyWalWithFallback; }
@@ -100,10 +104,6 @@ Object.defineProperty(exports, "sanitizeFts5Query", {
100
104
  enumerable: true,
101
105
  get: function () { return chunkJD74LN7R_cjs.sanitizeFts5Query; }
102
106
  });
103
- Object.defineProperty(exports, "withFileLock", {
104
- enumerable: true,
105
- get: function () { return chunkC7PICHRF_cjs.withFileLock; }
106
- });
107
107
  Object.defineProperty(exports, "withCwdMutex", {
108
108
  enumerable: true,
109
109
  get: function () { return chunkYLQQX5W2_cjs.withCwdMutex; }
@@ -1,8 +1,8 @@
1
- export { JsonlParseError, appendJsonl, loadJsonl, readJsonlIds } from '../../chunk-DUK5WMKJ.js';
1
+ export { JsonlParseError, appendJsonl, loadJsonl, readJsonlIds } from '../../chunk-R6ZP6KNB.js';
2
2
  export { PersistenceSchema } from '../../chunk-CAH3G4IS.js';
3
3
  export { displayTheokitHome, getProfilesRoot, getTheokitHome, migrateSchema, readVersionedJson, writeVersionedJson } from '../../chunk-DGRZNMTU.js';
4
- export { applyWalWithFallback, containsCjk, isCorruptionError, openSqliteResilient, sanitizeFts5Query } from '../../chunk-77FOZCOT.js';
5
4
  export { withFileLock } from '../../chunk-HZQEDNF6.js';
5
+ export { applyWalWithFallback, containsCjk, isCorruptionError, openSqliteResilient, sanitizeFts5Query } from '../../chunk-77FOZCOT.js';
6
6
  export { withCwdMutex } from '../../chunk-2XLKLVVR.js';
7
7
  export { atomicWriteJson, atomicWriteText, replaceFileAtomic } from '../../chunk-S2BBRVQX.js';
8
8
  import '../../chunk-WLKFWQSJ.js';
@@ -11,6 +11,13 @@ export declare class JsonlParseError extends Error {
11
11
  */
12
12
  export declare function loadJsonl<T = Record<string, unknown>>(path: string, opts?: {
13
13
  map?: (raw: Record<string, unknown>, lineNumber: number) => T;
14
+ /**
15
+ * M81 — tolerate a truncated LAST line (a crash artifact: the process died mid-write).
16
+ *
17
+ * Opt-in on purpose. As a default it would also swallow corruption in the MIDDLE of the file,
18
+ * turning loud data loss into silent data loss — the wrong trade for a session store.
19
+ */
20
+ tolerateTrailingPartialLine?: boolean;
14
21
  }): T[];
15
22
  /**
16
23
  * Append one record as a whole `\n`-terminated JSON line. Creates the parent dir
@@ -11,6 +11,13 @@ export declare class JsonlParseError extends Error {
11
11
  */
12
12
  export declare function loadJsonl<T = Record<string, unknown>>(path: string, opts?: {
13
13
  map?: (raw: Record<string, unknown>, lineNumber: number) => T;
14
+ /**
15
+ * M81 — tolerate a truncated LAST line (a crash artifact: the process died mid-write).
16
+ *
17
+ * Opt-in on purpose. As a default it would also swallow corruption in the MIDDLE of the file,
18
+ * turning loud data loss into silent data loss — the wrong trade for a session store.
19
+ */
20
+ tolerateTrailingPartialLine?: boolean;
14
21
  }): T[];
15
22
  /**
16
23
  * Append one record as a whole `\n`-terminated JSON line. Creates the parent dir
@@ -0,0 +1,56 @@
1
+ /**
2
+ * M81 — single-writer lease for a session transcript.
3
+ *
4
+ * ## The problem
5
+ *
6
+ * Nothing stops two processes appending to the same JSONL transcript. The concrete case: `exec
7
+ * resume --last` can write into the TUI's live session. Two interleaved appends to an append-only
8
+ * file produce lines that are each individually valid and whose SEQUENCE is fiction — and nothing
9
+ * reports it, because every line parses.
10
+ *
11
+ * ## Why an exclusive lockfile rather than `withFileLock`
12
+ *
13
+ * The plan's ADR D2 said to compose `withFileLock`, and that was the right instinct — do not build a
14
+ * second lock mechanism. It turned out not to fit the SHAPE: `withFileLock(path, fn)` is
15
+ * scope-based — it holds the lock for the duration of a callback. A session lease is **held across
16
+ * turns**, for as long as the process owns the session, with an explicit `release()`. Wrapping the
17
+ * whole session lifetime in a callback would invert control of the entire agent loop.
18
+ *
19
+ * So this uses the same underlying primitive `withFileLock` uses (an exclusive-create lockfile,
20
+ * `wx`) with lease semantics on top. That keeps the mechanism single — the file-existence lock —
21
+ * while giving it the lifetime the caller needs. The deviation from D2 is recorded here because the
22
+ * plan's rationale (no second mechanism) still holds; only its shape assumption did not.
23
+ *
24
+ * ## Fail fast, never wait
25
+ *
26
+ * A second writer that WAITED would block `exec` behind a TUI session that can last hours. The typed
27
+ * error lets the caller choose: fork to a new id, or give up with a real diagnosis.
28
+ *
29
+ * @internal
30
+ */
31
+ import { TheokitAgentError } from "../../errors.js";
32
+ /**
33
+ * M81 — another process already holds the writer lease for this session.
34
+ *
35
+ * Carries `sessionPath` because knowing WHICH session is busy is what lets the caller decide between
36
+ * forking and waiting for the user to close the TUI (`rules/error-handling.md § 2` — context enough
37
+ * to act on).
38
+ */
39
+ export declare class SessionBusyError extends TheokitAgentError {
40
+ readonly sessionPath: string;
41
+ readonly name = "SessionBusyError";
42
+ constructor(sessionPath: string);
43
+ }
44
+ /** A held writer lease. `release()` is idempotent. */
45
+ export interface SessionWriterLease {
46
+ readonly sessionPath: string;
47
+ release(): Promise<void>;
48
+ }
49
+ /**
50
+ * Take the exclusive writer lease for `sessionPath`, or reject with {@link SessionBusyError}.
51
+ *
52
+ * The lock is a sibling `.lock` file created with `wx` — the same file-existence primitive the
53
+ * SDK's `withFileLock` builds on. Exclusivity comes from the filesystem, so it holds across
54
+ * processes, not just across async tasks in one process.
55
+ */
56
+ export declare function acquireSessionWriter(sessionPath: string): Promise<SessionWriterLease>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * M81 — single-writer lease for a session transcript.
3
+ *
4
+ * ## The problem
5
+ *
6
+ * Nothing stops two processes appending to the same JSONL transcript. The concrete case: `exec
7
+ * resume --last` can write into the TUI's live session. Two interleaved appends to an append-only
8
+ * file produce lines that are each individually valid and whose SEQUENCE is fiction — and nothing
9
+ * reports it, because every line parses.
10
+ *
11
+ * ## Why an exclusive lockfile rather than `withFileLock`
12
+ *
13
+ * The plan's ADR D2 said to compose `withFileLock`, and that was the right instinct — do not build a
14
+ * second lock mechanism. It turned out not to fit the SHAPE: `withFileLock(path, fn)` is
15
+ * scope-based — it holds the lock for the duration of a callback. A session lease is **held across
16
+ * turns**, for as long as the process owns the session, with an explicit `release()`. Wrapping the
17
+ * whole session lifetime in a callback would invert control of the entire agent loop.
18
+ *
19
+ * So this uses the same underlying primitive `withFileLock` uses (an exclusive-create lockfile,
20
+ * `wx`) with lease semantics on top. That keeps the mechanism single — the file-existence lock —
21
+ * while giving it the lifetime the caller needs. The deviation from D2 is recorded here because the
22
+ * plan's rationale (no second mechanism) still holds; only its shape assumption did not.
23
+ *
24
+ * ## Fail fast, never wait
25
+ *
26
+ * A second writer that WAITED would block `exec` behind a TUI session that can last hours. The typed
27
+ * error lets the caller choose: fork to a new id, or give up with a real diagnosis.
28
+ *
29
+ * @internal
30
+ */
31
+ import { TheokitAgentError } from "../../errors.js";
32
+ /**
33
+ * M81 — another process already holds the writer lease for this session.
34
+ *
35
+ * Carries `sessionPath` because knowing WHICH session is busy is what lets the caller decide between
36
+ * forking and waiting for the user to close the TUI (`rules/error-handling.md § 2` — context enough
37
+ * to act on).
38
+ */
39
+ export declare class SessionBusyError extends TheokitAgentError {
40
+ readonly sessionPath: string;
41
+ readonly name = "SessionBusyError";
42
+ constructor(sessionPath: string);
43
+ }
44
+ /** A held writer lease. `release()` is idempotent. */
45
+ export interface SessionWriterLease {
46
+ readonly sessionPath: string;
47
+ release(): Promise<void>;
48
+ }
49
+ /**
50
+ * Take the exclusive writer lease for `sessionPath`, or reject with {@link SessionBusyError}.
51
+ *
52
+ * The lock is a sibling `.lock` file created with `wx` — the same file-existence primitive the
53
+ * SDK's `withFileLock` builds on. Exclusivity comes from the filesystem, so it holds across
54
+ * processes, not just across async tasks in one process.
55
+ */
56
+ export declare function acquireSessionWriter(sessionPath: string): Promise<SessionWriterLease>;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.
3
+ *
4
+ * ## What this replaces
5
+ *
6
+ * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a
7
+ * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and
8
+ * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.
9
+ *
10
+ * ## The rule that travels WITH the operation
11
+ *
12
+ * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live
13
+ * pointer, the most recent transcript, and any active registry entry. That rule lived in the
14
+ * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy
15
+ * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:
16
+ * critical knowledge outside the module that needs it, applied by convention.
17
+ *
18
+ * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of
19
+ * them. The caller supplies the list because only the caller knows which session is live; the
20
+ * enforcement lives here because that is where the write happens.
21
+ *
22
+ * @internal
23
+ */
24
+ import { TheokitAgentError } from "../../errors.js";
25
+ /**
26
+ * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).
27
+ *
28
+ * Typed rather than a bare `Error` because the caller must distinguish "this session is protected"
29
+ * from "the disk is full": the first is a correct refusal, the second is an incident.
30
+ */
31
+ export declare class LiveSessionError extends TheokitAgentError {
32
+ readonly path: string;
33
+ readonly name = "LiveSessionError";
34
+ constructor(path: string);
35
+ }
36
+ /** Options for {@link forkTranscript}. */
37
+ export interface ForkTranscriptOptions {
38
+ /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */
39
+ readonly beforeRecordIndex?: number;
40
+ /**
41
+ * Paths that must never be written over — the live pointer, the most recent transcript, any active
42
+ * registry entry. The caller supplies them because only the caller knows which session is live.
43
+ */
44
+ readonly liveSessionPaths?: readonly string[];
45
+ }
46
+ /**
47
+ * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.
48
+ *
49
+ * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot
50
+ * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also
51
+ * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an
52
+ * error is the worst failure mode for an operation that touches user sessions.
53
+ */
54
+ export declare function forkTranscript(src: string, dst: string, options?: ForkTranscriptOptions): void;
55
+ /** Options for {@link readJsonlTail}. */
56
+ export interface ReadJsonlTailOptions {
57
+ /** Maximum records to return, counted from the END. */
58
+ readonly maxRecords?: number;
59
+ /** Stop once a line contains this marker (exclusive). */
60
+ readonly sinceMarker?: string;
61
+ /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */
62
+ readonly _stats?: boolean;
63
+ }
64
+ /**
65
+ * Read the LAST records of a JSONL file without loading the whole thing.
66
+ *
67
+ * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session
68
+ * transcript grows without bound; loading megabytes to show the last three turns is the cost this
69
+ * exists to avoid — and a `slice` over a full read would be that same cost with a better name.
70
+ */
71
+ export declare function readJsonlTail<T = Record<string, unknown>>(path: string, options?: ReadJsonlTailOptions): T[];
@@ -0,0 +1,71 @@
1
+ /**
2
+ * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.
3
+ *
4
+ * ## What this replaces
5
+ *
6
+ * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a
7
+ * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and
8
+ * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.
9
+ *
10
+ * ## The rule that travels WITH the operation
11
+ *
12
+ * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live
13
+ * pointer, the most recent transcript, and any active registry entry. That rule lived in the
14
+ * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy
15
+ * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:
16
+ * critical knowledge outside the module that needs it, applied by convention.
17
+ *
18
+ * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of
19
+ * them. The caller supplies the list because only the caller knows which session is live; the
20
+ * enforcement lives here because that is where the write happens.
21
+ *
22
+ * @internal
23
+ */
24
+ import { TheokitAgentError } from "../../errors.js";
25
+ /**
26
+ * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).
27
+ *
28
+ * Typed rather than a bare `Error` because the caller must distinguish "this session is protected"
29
+ * from "the disk is full": the first is a correct refusal, the second is an incident.
30
+ */
31
+ export declare class LiveSessionError extends TheokitAgentError {
32
+ readonly path: string;
33
+ readonly name = "LiveSessionError";
34
+ constructor(path: string);
35
+ }
36
+ /** Options for {@link forkTranscript}. */
37
+ export interface ForkTranscriptOptions {
38
+ /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */
39
+ readonly beforeRecordIndex?: number;
40
+ /**
41
+ * Paths that must never be written over — the live pointer, the most recent transcript, any active
42
+ * registry entry. The caller supplies them because only the caller knows which session is live.
43
+ */
44
+ readonly liveSessionPaths?: readonly string[];
45
+ }
46
+ /**
47
+ * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.
48
+ *
49
+ * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot
50
+ * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also
51
+ * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an
52
+ * error is the worst failure mode for an operation that touches user sessions.
53
+ */
54
+ export declare function forkTranscript(src: string, dst: string, options?: ForkTranscriptOptions): void;
55
+ /** Options for {@link readJsonlTail}. */
56
+ export interface ReadJsonlTailOptions {
57
+ /** Maximum records to return, counted from the END. */
58
+ readonly maxRecords?: number;
59
+ /** Stop once a line contains this marker (exclusive). */
60
+ readonly sinceMarker?: string;
61
+ /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */
62
+ readonly _stats?: boolean;
63
+ }
64
+ /**
65
+ * Read the LAST records of a JSONL file without loading the whole thing.
66
+ *
67
+ * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session
68
+ * transcript grows without bound; loading megabytes to show the last three turns is the cost this
69
+ * exists to avoid — and a `slice` over a full read would be that same cost with a better name.
70
+ */
71
+ export declare function readJsonlTail<T = Record<string, unknown>>(path: string, options?: ReadJsonlTailOptions): T[];
@@ -11,7 +11,7 @@
11
11
  * @public
12
12
  */
13
13
  import type { Plugin as PluginType } from "../../types/plugin.js";
14
- export type { CommandHandler, CommandOptions, HookHandler, HookName, LlmCallContext, PluginContext, PostAssistantReplyContext, PostToolCallContext, PreToolCallContext, PreToolCallDecision, PreUserSendContext, PreUserSendResult, SessionLifecycleContext, ToolContext, TransformContext, } from "../../types/plugin.js";
14
+ export type { CommandHandler, CommandOptions, HookHandler, HookName, LlmCallContext, PluginContext, PostAssistantReplyContext, PostToolCallContext, PreToolCallContext, PreToolCallDecision, PreUserSendContext, PreUserSendResult, SessionLifecycleContext, ToolCallSummary, ToolContext, ToolResultTransformContext, TransformContext, } from "../../types/plugin.js";
15
15
  export type Plugin = PluginType;
16
16
  /**
17
17
  * Identity helper for plugin authors. TS-only convenience — preserves
@@ -11,7 +11,7 @@
11
11
  * @public
12
12
  */
13
13
  import type { Plugin as PluginType } from "../../types/plugin.js";
14
- export type { CommandHandler, CommandOptions, HookHandler, HookName, LlmCallContext, PluginContext, PostAssistantReplyContext, PostToolCallContext, PreToolCallContext, PreToolCallDecision, PreUserSendContext, PreUserSendResult, SessionLifecycleContext, ToolContext, TransformContext, } from "../../types/plugin.js";
14
+ export type { CommandHandler, CommandOptions, HookHandler, HookName, LlmCallContext, PluginContext, PostAssistantReplyContext, PostToolCallContext, PreToolCallContext, PreToolCallDecision, PreUserSendContext, PreUserSendResult, SessionLifecycleContext, ToolCallSummary, ToolContext, ToolResultTransformContext, TransformContext, } from "../../types/plugin.js";
15
15
  export type Plugin = PluginType;
16
16
  /**
17
17
  * Identity helper for plugin authors. TS-only convenience — preserves
@@ -1,37 +1,139 @@
1
1
  'use strict';
2
2
 
3
- var chunkQUTUJ6P6_cjs = require('./chunk-QUTUJ6P6.cjs');
3
+ var chunkHK7ZHSAX_cjs = require('./chunk-HK7ZHSAX.cjs');
4
4
  var chunkPNVDQL5Y_cjs = require('./chunk-PNVDQL5Y.cjs');
5
- var chunkJD74LN7R_cjs = require('./chunk-JD74LN7R.cjs');
6
5
  var chunkC7PICHRF_cjs = require('./chunk-C7PICHRF.cjs');
6
+ var chunkJD74LN7R_cjs = require('./chunk-JD74LN7R.cjs');
7
7
  var chunkYLQQX5W2_cjs = require('./chunk-YLQQX5W2.cjs');
8
8
  var chunk4SXHEZIV_cjs = require('./chunk-4SXHEZIV.cjs');
9
9
  var chunkTIZ4TS5V_cjs = require('./chunk-TIZ4TS5V.cjs');
10
- require('./chunk-A44COQJJ.cjs');
10
+ var chunkA44COQJJ_cjs = require('./chunk-A44COQJJ.cjs');
11
11
  require('./chunk-BEURCLDV.cjs');
12
+ var fs = require('fs');
12
13
 
13
-
14
+ var SessionBusyError = class extends chunkA44COQJJ_cjs.TheokitAgentError {
15
+ constructor(sessionPath) {
16
+ super(
17
+ `another process is already writing this session: ${sessionPath}. Fork it to a new id instead of appending \u2014 two writers interleave lines into a sequence that parses but is not what either process wrote.`,
18
+ { code: "session_busy", isRetryable: false }
19
+ );
20
+ this.sessionPath = sessionPath;
21
+ }
22
+ sessionPath;
23
+ name = "SessionBusyError";
24
+ };
25
+ async function acquireSessionWriter(sessionPath) {
26
+ const lockPath = `${sessionPath}.lock`;
27
+ let fd;
28
+ try {
29
+ fd = fs.openSync(lockPath, "wx");
30
+ } catch (err) {
31
+ if (err.code === "EEXIST") {
32
+ throw new SessionBusyError(sessionPath);
33
+ }
34
+ throw err;
35
+ }
36
+ fs.closeSync(fd);
37
+ let released = false;
38
+ return {
39
+ sessionPath,
40
+ release: async () => {
41
+ if (released) return;
42
+ released = true;
43
+ fs.rmSync(lockPath, { force: true });
44
+ }
45
+ };
46
+ }
47
+ var LiveSessionError = class extends chunkA44COQJJ_cjs.TheokitAgentError {
48
+ constructor(path) {
49
+ super(
50
+ `refusing to write over a live session transcript: ${path}. Fork to a new id instead \u2014 the live pointer, the most recent transcript and any active registry entry are never overwritten (audit-trail rotation, M60).`,
51
+ { code: "live_session_protected", isRetryable: false }
52
+ );
53
+ this.path = path;
54
+ }
55
+ path;
56
+ name = "LiveSessionError";
57
+ };
58
+ function forkTranscript(src, dst, options = {}) {
59
+ for (const live of options.liveSessionPaths ?? []) {
60
+ if (live === dst) throw new LiveSessionError(dst);
61
+ }
62
+ const lines = fs.readFileSync(src, "utf8").split("\n").filter((l) => l.trim().length > 0);
63
+ const kept = options.beforeRecordIndex === void 0 ? lines : lines.slice(0, options.beforeRecordIndex);
64
+ const body = kept.length > 0 ? `${kept.join("\n")}
65
+ ` : "";
66
+ const fd = fs.openSync(dst, "wx");
67
+ try {
68
+ fs.writeSync(fd, body);
69
+ } finally {
70
+ fs.closeSync(fd);
71
+ }
72
+ }
73
+ var TAIL_CHUNK = 64 * 1024;
74
+ function lerCaudaBruta(path, want) {
75
+ const size = fs.statSync(path).size;
76
+ const fd = fs.openSync(path, "r");
77
+ let bytesRead = 0;
78
+ let tail = "";
79
+ let pos = size;
80
+ try {
81
+ while (pos > 0) {
82
+ const len = Math.min(TAIL_CHUNK, pos);
83
+ pos -= len;
84
+ const buf = Buffer.alloc(len);
85
+ fs.readSync(fd, buf, 0, len, pos);
86
+ bytesRead += len;
87
+ tail = buf.toString("utf8") + tail;
88
+ if (naoVazias(tail).length > want) break;
89
+ }
90
+ } finally {
91
+ fs.closeSync(fd);
92
+ }
93
+ const linhas = naoVazias(tail);
94
+ return { linhas: pos > 0 ? linhas.slice(1) : linhas, bytesRead };
95
+ }
96
+ function naoVazias(texto) {
97
+ return texto.split("\n").filter((l) => l.trim().length > 0);
98
+ }
99
+ function readJsonlTail(path, options = {}) {
100
+ const want = options.maxRecords ?? Number.POSITIVE_INFINITY;
101
+ const { linhas, bytesRead } = lerCaudaBruta(path, want);
102
+ let sel = linhas;
103
+ if (options.sinceMarker !== void 0) {
104
+ const marcador = options.sinceMarker;
105
+ const idx = sel.findLastIndex((l) => l.includes(marcador));
106
+ if (idx >= 0) sel = sel.slice(idx + 1);
107
+ }
108
+ if (Number.isFinite(want)) sel = sel.slice(-want);
109
+ const out = sel.map((l) => JSON.parse(l));
110
+ return options._stats === true ? Object.assign(out, { bytesRead }) : out;
111
+ }
14
112
 
15
113
  Object.defineProperty(exports, "JsonlParseError", {
16
114
  enumerable: true,
17
- get: function () { return chunkQUTUJ6P6_cjs.JsonlParseError; }
115
+ get: function () { return chunkHK7ZHSAX_cjs.JsonlParseError; }
18
116
  });
19
117
  Object.defineProperty(exports, "appendJsonl", {
20
118
  enumerable: true,
21
- get: function () { return chunkQUTUJ6P6_cjs.appendJsonl; }
119
+ get: function () { return chunkHK7ZHSAX_cjs.appendJsonl; }
22
120
  });
23
121
  Object.defineProperty(exports, "loadJsonl", {
24
122
  enumerable: true,
25
- get: function () { return chunkQUTUJ6P6_cjs.loadJsonl; }
123
+ get: function () { return chunkHK7ZHSAX_cjs.loadJsonl; }
26
124
  });
27
125
  Object.defineProperty(exports, "readJsonlIds", {
28
126
  enumerable: true,
29
- get: function () { return chunkQUTUJ6P6_cjs.readJsonlIds; }
127
+ get: function () { return chunkHK7ZHSAX_cjs.readJsonlIds; }
30
128
  });
31
129
  Object.defineProperty(exports, "PersistenceSchema", {
32
130
  enumerable: true,
33
131
  get: function () { return chunkPNVDQL5Y_cjs.PersistenceSchema; }
34
132
  });
133
+ Object.defineProperty(exports, "withFileLock", {
134
+ enumerable: true,
135
+ get: function () { return chunkC7PICHRF_cjs.withFileLock; }
136
+ });
35
137
  Object.defineProperty(exports, "applyWalWithFallback", {
36
138
  enumerable: true,
37
139
  get: function () { return chunkJD74LN7R_cjs.applyWalWithFallback; }
@@ -48,10 +150,6 @@ Object.defineProperty(exports, "sanitizeFts5Query", {
48
150
  enumerable: true,
49
151
  get: function () { return chunkJD74LN7R_cjs.sanitizeFts5Query; }
50
152
  });
51
- Object.defineProperty(exports, "withFileLock", {
52
- enumerable: true,
53
- get: function () { return chunkC7PICHRF_cjs.withFileLock; }
54
- });
55
153
  Object.defineProperty(exports, "withCwdMutex", {
56
154
  enumerable: true,
57
155
  get: function () { return chunkYLQQX5W2_cjs.withCwdMutex; }
@@ -76,5 +174,10 @@ Object.defineProperty(exports, "replaceFileAtomic", {
76
174
  enumerable: true,
77
175
  get: function () { return chunkTIZ4TS5V_cjs.replaceFileAtomic; }
78
176
  });
177
+ exports.LiveSessionError = LiveSessionError;
178
+ exports.SessionBusyError = SessionBusyError;
179
+ exports.acquireSessionWriter = acquireSessionWriter;
180
+ exports.forkTranscript = forkTranscript;
181
+ exports.readJsonlTail = readJsonlTail;
79
182
  //# sourceMappingURL=persistence.cjs.map
80
183
  //# sourceMappingURL=persistence.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"persistence.cjs"}
1
+ {"version":3,"sources":["../src/internal/persistence/session-writer.ts","../src/internal/persistence/transcript-ops.ts"],"names":["TheokitAgentError","openSync","closeSync","rmSync","readFileSync","writeSync","statSync","readSync"],"mappings":"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","file":"persistence.cjs","sourcesContent":["/**\n * M81 — single-writer lease for a session transcript.\n *\n * ## The problem\n *\n * Nothing stops two processes appending to the same JSONL transcript. The concrete case: `exec\n * resume --last` can write into the TUI's live session. Two interleaved appends to an append-only\n * file produce lines that are each individually valid and whose SEQUENCE is fiction — and nothing\n * reports it, because every line parses.\n *\n * ## Why an exclusive lockfile rather than `withFileLock`\n *\n * The plan's ADR D2 said to compose `withFileLock`, and that was the right instinct — do not build a\n * second lock mechanism. It turned out not to fit the SHAPE: `withFileLock(path, fn)` is\n * scope-based — it holds the lock for the duration of a callback. A session lease is **held across\n * turns**, for as long as the process owns the session, with an explicit `release()`. Wrapping the\n * whole session lifetime in a callback would invert control of the entire agent loop.\n *\n * So this uses the same underlying primitive `withFileLock` uses (an exclusive-create lockfile,\n * `wx`) with lease semantics on top. That keeps the mechanism single — the file-existence lock —\n * while giving it the lifetime the caller needs. The deviation from D2 is recorded here because the\n * plan's rationale (no second mechanism) still holds; only its shape assumption did not.\n *\n * ## Fail fast, never wait\n *\n * A second writer that WAITED would block `exec` behind a TUI session that can last hours. The typed\n * error lets the caller choose: fork to a new id, or give up with a real diagnosis.\n *\n * @internal\n */\n\nimport { closeSync, openSync, rmSync } from \"node:fs\";\n\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/**\n * M81 — another process already holds the writer lease for this session.\n *\n * Carries `sessionPath` because knowing WHICH session is busy is what lets the caller decide between\n * forking and waiting for the user to close the TUI (`rules/error-handling.md § 2` — context enough\n * to act on).\n */\nexport class SessionBusyError extends TheokitAgentError {\n override readonly name = \"SessionBusyError\";\n\n constructor(readonly sessionPath: string) {\n super(\n `another process is already writing this session: ${sessionPath}. ` +\n \"Fork it to a new id instead of appending — two writers interleave lines into a sequence \" +\n \"that parses but is not what either process wrote.\",\n { code: \"session_busy\", isRetryable: false },\n );\n }\n}\n\n/** A held writer lease. `release()` is idempotent. */\nexport interface SessionWriterLease {\n readonly sessionPath: string;\n release(): Promise<void>;\n}\n\n/**\n * Take the exclusive writer lease for `sessionPath`, or reject with {@link SessionBusyError}.\n *\n * The lock is a sibling `.lock` file created with `wx` — the same file-existence primitive the\n * SDK's `withFileLock` builds on. Exclusivity comes from the filesystem, so it holds across\n * processes, not just across async tasks in one process.\n */\nexport async function acquireSessionWriter(sessionPath: string): Promise<SessionWriterLease> {\n const lockPath = `${sessionPath}.lock`;\n let fd: number;\n try {\n fd = openSync(lockPath, \"wx\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"EEXIST\") {\n throw new SessionBusyError(sessionPath);\n }\n throw err;\n }\n closeSync(fd);\n\n let released = false;\n return {\n sessionPath,\n release: async (): Promise<void> => {\n if (released) return;\n released = true;\n rmSync(lockPath, { force: true });\n },\n };\n}\n","/**\n * M81 — transcript operations the consumer was doing by hand INSIDE the framework's own store.\n *\n * ## What this replaces\n *\n * `agents/lib/session/backtrack.ts:188` (agent-builder) wrote straight into the session store with a\n * bare `writeFileSync` — no atomicity, no lock, no API. 243 lines re-implementing parse, cut and\n * write for a format the framework owns. The consumer is not at fault: nothing here was reachable.\n *\n * ## The rule that travels WITH the operation\n *\n * `rules/audit-trail-rotation.md § Session transcripts (M60)` defines a NEVER-delete list — the live\n * pointer, the most recent transcript, and any active registry entry. That rule lived in the\n * CONSUMER. Moving the operation here without moving the rule would ship an API able to destroy\n * exactly what the rule protects — the same shape of defect as M80's `reconcileUpdateGoalStatus`:\n * critical knowledge outside the module that needs it, applied by convention.\n *\n * So `forkTranscript` takes `liveSessionPaths` and refuses, with a TYPED error, to write over any of\n * them. The caller supplies the list because only the caller knows which session is live; the\n * enforcement lives here because that is where the write happens.\n *\n * @internal\n */\n\nimport { closeSync, openSync, readFileSync, readSync, statSync, writeSync } from \"node:fs\";\n\nimport { TheokitAgentError } from \"../../errors.js\";\n\n/**\n * M81 — the target is a protected session (live pointer / most-recent transcript / active entry).\n *\n * Typed rather than a bare `Error` because the caller must distinguish \"this session is protected\"\n * from \"the disk is full\": the first is a correct refusal, the second is an incident.\n */\nexport class LiveSessionError extends TheokitAgentError {\n override readonly name = \"LiveSessionError\";\n\n constructor(readonly path: string) {\n super(\n `refusing to write over a live session transcript: ${path}. ` +\n \"Fork to a new id instead — the live pointer, the most recent transcript and any active \" +\n \"registry entry are never overwritten (audit-trail rotation, M60).\",\n { code: \"live_session_protected\", isRetryable: false },\n );\n }\n}\n\n/** Options for {@link forkTranscript}. */\nexport interface ForkTranscriptOptions {\n /** Keep records `[0, beforeRecordIndex)`. Omit to copy the whole transcript. */\n readonly beforeRecordIndex?: number;\n /**\n * Paths that must never be written over — the live pointer, the most recent transcript, any active\n * registry entry. The caller supplies them because only the caller knows which session is live.\n */\n readonly liveSessionPaths?: readonly string[];\n}\n\n/**\n * Copy `src` into `dst`, keeping the first `beforeRecordIndex` records. The SOURCE is never touched.\n *\n * Atomicity comes from `wx` (exclusive create): two concurrent forks to the same destination cannot\n * both succeed — the loser gets `EEXIST` rather than writing over a half-written file. That is also\n * why an existing destination is a refusal, not a silent overwrite: losing a transcript without an\n * error is the worst failure mode for an operation that touches user sessions.\n */\nexport function forkTranscript(\n src: string,\n dst: string,\n options: ForkTranscriptOptions = {},\n): void {\n for (const live of options.liveSessionPaths ?? []) {\n if (live === dst) throw new LiveSessionError(dst);\n }\n\n const lines = readFileSync(src, \"utf8\")\n .split(\"\\n\")\n .filter((l) => l.trim().length > 0);\n const kept =\n options.beforeRecordIndex === undefined ? lines : lines.slice(0, options.beforeRecordIndex);\n const body = kept.length > 0 ? `${kept.join(\"\\n\")}\\n` : \"\";\n\n // `wx` — fails with EEXIST instead of truncating. The exclusivity IS the concurrency guarantee.\n const fd = openSync(dst, \"wx\");\n try {\n writeSync(fd, body);\n } finally {\n closeSync(fd);\n }\n}\n\n/** Options for {@link readJsonlTail}. */\nexport interface ReadJsonlTailOptions {\n /** Maximum records to return, counted from the END. */\n readonly maxRecords?: number;\n /** Stop once a line contains this marker (exclusive). */\n readonly sinceMarker?: string;\n /** Test-only: also report how many bytes were read, to prove the read is not whole-file. */\n readonly _stats?: boolean;\n}\n\nconst TAIL_CHUNK = 64 * 1024;\n\n/**\n * Lê chunks de trás para frente até acumular linhas completas suficientes.\n *\n * Extraído de `readJsonlTail` porque o laço de leitura e a seleção de registros são duas\n * responsabilidades — e juntas passavam do teto de complexidade. A primeira linha do buffer pode\n * estar cortada ao meio quando a leitura parou antes do início do arquivo; por isso ela é descartada.\n */\nfunction lerCaudaBruta(path: string, want: number): { linhas: string[]; bytesRead: number } {\n const size = statSync(path).size;\n const fd = openSync(path, \"r\");\n let bytesRead = 0;\n let tail = \"\";\n let pos = size;\n try {\n while (pos > 0) {\n const len = Math.min(TAIL_CHUNK, pos);\n pos -= len;\n const buf = Buffer.alloc(len);\n readSync(fd, buf, 0, len, pos);\n bytesRead += len;\n tail = buf.toString(\"utf8\") + tail;\n if (naoVazias(tail).length > want) break;\n }\n } finally {\n closeSync(fd);\n }\n const linhas = naoVazias(tail);\n return { linhas: pos > 0 ? linhas.slice(1) : linhas, bytesRead };\n}\n\n/** Linhas com conteúdo, na ordem do arquivo. */\nfunction naoVazias(texto: string): string[] {\n return texto.split(\"\\n\").filter((l) => l.trim().length > 0);\n}\n\n/**\n * Read the LAST records of a JSONL file without loading the whole thing.\n *\n * Reads fixed-size chunks backwards from EOF until enough newlines have been seen. A session\n * transcript grows without bound; loading megabytes to show the last three turns is the cost this\n * exists to avoid — and a `slice` over a full read would be that same cost with a better name.\n */\nexport function readJsonlTail<T = Record<string, unknown>>(\n path: string,\n options: ReadJsonlTailOptions = {},\n): T[] {\n const want = options.maxRecords ?? Number.POSITIVE_INFINITY;\n const { linhas, bytesRead } = lerCaudaBruta(path, want);\n\n let sel = linhas;\n if (options.sinceMarker !== undefined) {\n const marcador = options.sinceMarker;\n const idx = sel.findLastIndex((l) => l.includes(marcador));\n if (idx >= 0) sel = sel.slice(idx + 1);\n }\n if (Number.isFinite(want)) sel = sel.slice(-want);\n\n const out = sel.map((l) => JSON.parse(l) as T);\n return options._stats === true ? (Object.assign(out, { bytesRead }) as T[]) : out;\n}\n"]}
@@ -22,7 +22,9 @@ export { sanitizeFts5Query } from "./internal/persistence/fts5-sanitize.js";
22
22
  export { appendJsonl, JsonlParseError, loadJsonl, readJsonlIds, } from "./internal/persistence/jsonl.js";
23
23
  export { PersistenceSchema } from "./internal/persistence/persistence-schema.js";
24
24
  export { encodeProjectDir, transcriptPath, } from "./internal/persistence/session-transcript.js";
25
+ export { acquireSessionWriter, SessionBusyError, type SessionWriterLease, } from "./internal/persistence/session-writer.js";
25
26
  export type { OpenSqliteResilientOptions, ResilientSqliteDb, } from "./internal/persistence/sqlite-open.js";
26
27
  export { isCorruptionError, openSqliteResilient } from "./internal/persistence/sqlite-open.js";
27
28
  export type { WalApplyResult } from "./internal/persistence/sqlite-wal.js";
28
29
  export { applyWalWithFallback } from "./internal/persistence/sqlite-wal.js";
30
+ export { type ForkTranscriptOptions, forkTranscript, LiveSessionError, type ReadJsonlTailOptions, readJsonlTail, } from "./internal/persistence/transcript-ops.js";
@@ -22,7 +22,9 @@ export { sanitizeFts5Query } from "./internal/persistence/fts5-sanitize.js";
22
22
  export { appendJsonl, JsonlParseError, loadJsonl, readJsonlIds, } from "./internal/persistence/jsonl.js";
23
23
  export { PersistenceSchema } from "./internal/persistence/persistence-schema.js";
24
24
  export { encodeProjectDir, transcriptPath, } from "./internal/persistence/session-transcript.js";
25
+ export { acquireSessionWriter, SessionBusyError, type SessionWriterLease, } from "./internal/persistence/session-writer.js";
25
26
  export type { OpenSqliteResilientOptions, ResilientSqliteDb, } from "./internal/persistence/sqlite-open.js";
26
27
  export { isCorruptionError, openSqliteResilient } from "./internal/persistence/sqlite-open.js";
27
28
  export type { WalApplyResult } from "./internal/persistence/sqlite-wal.js";
28
29
  export { applyWalWithFallback } from "./internal/persistence/sqlite-wal.js";
30
+ export { type ForkTranscriptOptions, forkTranscript, LiveSessionError, type ReadJsonlTailOptions, readJsonlTail, } from "./internal/persistence/transcript-ops.js";