@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.
- package/dist/agent-7YUGJJTU.js +41 -0
- package/dist/agent-7YUGJJTU.js.map +1 -0
- package/dist/{cron-BN6JJlDw.d.ts → agent-Bzg2_uGr.d.cts} +56 -577
- package/dist/{cron-CoQREV_k.d.cts → agent-Ekv8y7_c.d.ts} +56 -577
- package/dist/agent-LD76RI74.cjs +50 -0
- package/dist/agent-LD76RI74.cjs.map +1 -0
- package/dist/{chunk-AKSBP7OO.js → chunk-5IB2UKSN.js} +5 -41
- package/dist/chunk-5IB2UKSN.js.map +1 -0
- package/dist/{chunk-PF5Y5BHG.js → chunk-5YCAR23S.js} +3 -3
- package/dist/{chunk-PF5Y5BHG.js.map → chunk-5YCAR23S.js.map} +1 -1
- package/dist/chunk-BNIFFFH3.js +129 -0
- package/dist/chunk-BNIFFFH3.js.map +1 -0
- package/dist/{chunk-LSLPF6OI.cjs → chunk-DHLZIMP6.cjs} +5 -43
- package/dist/chunk-DHLZIMP6.cjs.map +1 -0
- package/dist/chunk-FKCC2MED.js +43 -0
- package/dist/chunk-FKCC2MED.js.map +1 -0
- package/dist/{chunk-PN53WGEI.cjs → chunk-GWMQ5CS3.cjs} +633 -741
- package/dist/chunk-GWMQ5CS3.cjs.map +1 -0
- package/dist/{chunk-QUTUJ6P6.cjs → chunk-HK7ZHSAX.cjs} +25 -16
- package/dist/chunk-HK7ZHSAX.cjs.map +1 -0
- package/dist/{chunk-XW6RXVGB.js → chunk-ISZ3MBUD.js} +610 -718
- package/dist/chunk-ISZ3MBUD.js.map +1 -0
- package/dist/{chunk-MQLCZ4HJ.cjs → chunk-JLMXKTBX.cjs} +4 -4
- package/dist/{chunk-MQLCZ4HJ.cjs.map → chunk-JLMXKTBX.cjs.map} +1 -1
- package/dist/chunk-LALRCFSN.cjs +46 -0
- package/dist/chunk-LALRCFSN.cjs.map +1 -0
- package/dist/{chunk-I53QDJVI.cjs → chunk-NG6B2PMA.cjs} +5 -5
- package/dist/{chunk-I53QDJVI.cjs.map → chunk-NG6B2PMA.cjs.map} +1 -1
- package/dist/{chunk-DUK5WMKJ.js → chunk-R6ZP6KNB.js} +25 -16
- package/dist/chunk-R6ZP6KNB.js.map +1 -0
- package/dist/chunk-RZQ6E5YZ.cjs +131 -0
- package/dist/chunk-RZQ6E5YZ.cjs.map +1 -0
- package/dist/{chunk-G6GO6BXD.js → chunk-XAUBQMQU.js} +3 -3
- package/dist/{chunk-G6GO6BXD.js.map → chunk-XAUBQMQU.js.map} +1 -1
- package/dist/compact-session-OO2GD76G.js +1 -1
- package/dist/compact-session-OO7NVWPB.cjs +1 -1
- package/dist/cron-CjzvXGT2.d.cts +579 -0
- package/dist/cron-D0Z29gI-.d.ts +579 -0
- package/dist/cron.cjs +13 -11
- package/dist/cron.d.cts +2 -1
- package/dist/cron.d.ts +2 -1
- package/dist/cron.js +12 -10
- package/dist/eval.cjs +17 -15
- package/dist/eval.cjs.map +1 -1
- package/dist/eval.js +13 -11
- package/dist/eval.js.map +1 -1
- package/dist/{index-manager-CB6I7XFQ.cjs → index-manager-H7HUMW52.cjs} +5 -5
- package/dist/{index-manager-CB6I7XFQ.cjs.map → index-manager-H7HUMW52.cjs.map} +1 -1
- package/dist/{index-manager-L27Q3QAC.js → index-manager-P7ZUX2RG.js} +4 -4
- package/dist/{index-manager-L27Q3QAC.js.map → index-manager-P7ZUX2RG.js.map} +1 -1
- package/dist/index.cjs +65 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +41 -20
- package/dist/index.js.map +1 -1
- package/dist/internal/persistence/index.cjs +10 -10
- package/dist/internal/persistence/index.js +2 -2
- package/dist/internal/persistence/jsonl.d.cts +7 -0
- package/dist/internal/persistence/jsonl.d.ts +7 -0
- package/dist/internal/persistence/session-writer.d.cts +56 -0
- package/dist/internal/persistence/session-writer.d.ts +56 -0
- package/dist/internal/persistence/transcript-ops.d.cts +71 -0
- package/dist/internal/persistence/transcript-ops.d.ts +71 -0
- package/dist/internal/plugins/types.d.cts +1 -1
- package/dist/internal/plugins/types.d.ts +1 -1
- package/dist/persistence.cjs +115 -12
- package/dist/persistence.cjs.map +1 -1
- package/dist/persistence.d.cts +2 -0
- package/dist/persistence.d.ts +2 -0
- package/dist/persistence.js +105 -3
- package/dist/persistence.js.map +1 -1
- package/dist/skills.cjs +4 -3
- package/dist/skills.js +2 -1
- package/dist/subagents-loader.cjs +19 -0
- package/dist/subagents-loader.cjs.map +1 -0
- package/dist/subagents-loader.d.cts +39 -0
- package/dist/subagents-loader.d.ts +39 -0
- package/dist/subagents-loader.js +16 -0
- package/dist/subagents-loader.js.map +1 -0
- package/dist/types/plugin.d.ts +32 -0
- package/dist/workflow.cjs +1 -1
- package/dist/workflow.js +1 -1
- package/package.json +12 -2
- package/dist/chunk-AKSBP7OO.js.map +0 -1
- package/dist/chunk-DUK5WMKJ.js.map +0 -1
- package/dist/chunk-LSLPF6OI.cjs.map +0 -1
- package/dist/chunk-PN53WGEI.cjs.map +0 -1
- package/dist/chunk-QUTUJ6P6.cjs.map +0 -1
- package/dist/chunk-XW6RXVGB.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
|
41
|
+
get: function () { return chunkHK7ZHSAX_cjs.JsonlParseError; }
|
|
42
42
|
});
|
|
43
43
|
Object.defineProperty(exports, "appendJsonl", {
|
|
44
44
|
enumerable: true,
|
|
45
|
-
get: function () { return
|
|
45
|
+
get: function () { return chunkHK7ZHSAX_cjs.appendJsonl; }
|
|
46
46
|
});
|
|
47
47
|
Object.defineProperty(exports, "loadJsonl", {
|
|
48
48
|
enumerable: true,
|
|
49
|
-
get: function () { return
|
|
49
|
+
get: function () { return chunkHK7ZHSAX_cjs.loadJsonl; }
|
|
50
50
|
});
|
|
51
51
|
Object.defineProperty(exports, "readJsonlIds", {
|
|
52
52
|
enumerable: true,
|
|
53
|
-
get: function () { return
|
|
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-
|
|
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
|
package/dist/persistence.cjs
CHANGED
|
@@ -1,37 +1,139 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
|
115
|
+
get: function () { return chunkHK7ZHSAX_cjs.JsonlParseError; }
|
|
18
116
|
});
|
|
19
117
|
Object.defineProperty(exports, "appendJsonl", {
|
|
20
118
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
119
|
+
get: function () { return chunkHK7ZHSAX_cjs.appendJsonl; }
|
|
22
120
|
});
|
|
23
121
|
Object.defineProperty(exports, "loadJsonl", {
|
|
24
122
|
enumerable: true,
|
|
25
|
-
get: function () { return
|
|
123
|
+
get: function () { return chunkHK7ZHSAX_cjs.loadJsonl; }
|
|
26
124
|
});
|
|
27
125
|
Object.defineProperty(exports, "readJsonlIds", {
|
|
28
126
|
enumerable: true,
|
|
29
|
-
get: function () { return
|
|
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
|
package/dist/persistence.cjs.map
CHANGED
|
@@ -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"]}
|
package/dist/persistence.d.cts
CHANGED
|
@@ -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";
|
package/dist/persistence.d.ts
CHANGED
|
@@ -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";
|