@theokit/sdk 4.33.1 → 4.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/{agent-LIVADAZ4.js → agent-EQQA5VGX.js} +8 -8
- package/dist/{agent-LIVADAZ4.js.map → agent-EQQA5VGX.js.map} +1 -1
- package/dist/{agent-T2AIB3EV.cjs → agent-W6NI3HY4.cjs} +9 -9
- package/dist/{agent-T2AIB3EV.cjs.map → agent-W6NI3HY4.cjs.map} +1 -1
- package/dist/{chunk-3CMIY3N7.js → chunk-5QMXYX3X.js} +3 -3
- package/dist/{chunk-3CMIY3N7.js.map → chunk-5QMXYX3X.js.map} +1 -1
- package/dist/{chunk-SDYJESYZ.js → chunk-7BTE3RXJ.js} +13 -9
- package/dist/{chunk-SDYJESYZ.js.map → chunk-7BTE3RXJ.js.map} +1 -1
- package/dist/{chunk-E33A7YIR.cjs → chunk-G4WI3VW2.cjs} +12 -8
- package/dist/chunk-G4WI3VW2.cjs.map +1 -0
- package/dist/{chunk-2BYBFBNH.cjs → chunk-IAZX7RZM.cjs} +24 -7
- package/dist/chunk-IAZX7RZM.cjs.map +1 -0
- package/dist/{chunk-TOVGIP4L.js → chunk-IPHW3VKP.js} +14 -6
- package/dist/chunk-IPHW3VKP.js.map +1 -0
- package/dist/{chunk-KT6SWUY6.js → chunk-IX2LKBCD.js} +3 -3
- package/dist/{chunk-KT6SWUY6.js.map → chunk-IX2LKBCD.js.map} +1 -1
- package/dist/{chunk-WKSVAP6H.cjs → chunk-IZFZ6RHA.cjs} +5 -5
- package/dist/{chunk-WKSVAP6H.cjs.map → chunk-IZFZ6RHA.cjs.map} +1 -1
- package/dist/{chunk-NEPJ3TC4.cjs → chunk-JYMBHNNU.cjs} +13 -5
- package/dist/chunk-JYMBHNNU.cjs.map +1 -0
- package/dist/{chunk-FX2DAR5R.cjs → chunk-M2PU44HP.cjs} +3 -3
- package/dist/{chunk-FX2DAR5R.cjs.map → chunk-M2PU44HP.cjs.map} +1 -1
- package/dist/{chunk-GBCQ5742.js → chunk-RKG6QFCJ.js} +3 -3
- package/dist/{chunk-GBCQ5742.js.map → chunk-RKG6QFCJ.js.map} +1 -1
- package/dist/{chunk-4LXCZSIC.js → chunk-TPTHVSIG.js} +23 -9
- package/dist/chunk-TPTHVSIG.js.map +1 -0
- package/dist/{chunk-SIJZVUUD.cjs → chunk-TS6IPRME.cjs} +56 -42
- package/dist/chunk-TS6IPRME.cjs.map +1 -0
- package/dist/{chunk-F5RLC6ZH.js → chunk-VQ6NY4F5.js} +23 -6
- package/dist/chunk-VQ6NY4F5.js.map +1 -0
- package/dist/{chunk-FTRNHGAD.cjs → chunk-WEHUJ7ZS.cjs} +8 -8
- package/dist/{chunk-FTRNHGAD.cjs.map → chunk-WEHUJ7ZS.cjs.map} +1 -1
- package/dist/{compact-session-J7VNCICZ.js → compact-session-5BDD4HIB.js} +4 -4
- package/dist/{compact-session-J7VNCICZ.js.map → compact-session-5BDD4HIB.js.map} +1 -1
- package/dist/{compact-session-24PK2G4M.cjs → compact-session-BEA3IOON.cjs} +12 -12
- package/dist/{compact-session-24PK2G4M.cjs.map → compact-session-BEA3IOON.cjs.map} +1 -1
- package/dist/compaction.cjs +14 -14
- package/dist/compaction.d.cts +9 -4
- package/dist/compaction.d.ts +9 -4
- package/dist/compaction.js +1 -1
- package/dist/cron.cjs +8 -8
- package/dist/cron.js +7 -7
- package/dist/eval.cjs +7 -7
- package/dist/eval.js +6 -6
- package/dist/{fs-session-store-ENQIIMIR.cjs → fs-session-store-QS763E3Y.cjs} +5 -5
- package/dist/{fs-session-store-ENQIIMIR.cjs.map → fs-session-store-QS763E3Y.cjs.map} +1 -1
- package/dist/fs-session-store-SJ3RA5I2.js +10 -0
- package/dist/{fs-session-store-RKLORRCG.js.map → fs-session-store-SJ3RA5I2.js.map} +1 -1
- package/dist/index.cjs +29 -29
- package/dist/index.js +9 -9
- package/dist/{inject-session-343WIDCM.cjs → inject-session-SMH56NXP.cjs} +4 -4
- package/dist/{inject-session-343WIDCM.cjs.map → inject-session-SMH56NXP.cjs.map} +1 -1
- package/dist/{inject-session-RZEDWO53.js → inject-session-V5FTK72N.js} +3 -3
- package/dist/{inject-session-RZEDWO53.js.map → inject-session-V5FTK72N.js.map} +1 -1
- package/dist/internal/persistence/fs-session-store.d.cts +15 -0
- package/dist/internal/persistence/fs-session-store.d.ts +15 -0
- package/dist/internal/persistence/session-writer.d.cts +14 -4
- package/dist/internal/persistence/session-writer.d.ts +14 -4
- package/dist/persistence.cjs +3 -3
- package/dist/persistence.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-2BYBFBNH.cjs.map +0 -1
- package/dist/chunk-4LXCZSIC.js.map +0 -1
- package/dist/chunk-E33A7YIR.cjs.map +0 -1
- package/dist/chunk-F5RLC6ZH.js.map +0 -1
- package/dist/chunk-NEPJ3TC4.cjs.map +0 -1
- package/dist/chunk-SIJZVUUD.cjs.map +0 -1
- package/dist/chunk-TOVGIP4L.js.map +0 -1
- package/dist/fs-session-store-RKLORRCG.js +0 -10
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkJYMBHNNU_cjs = require('./chunk-JYMBHNNU.cjs');
|
|
4
4
|
var chunkCJVMDRQB_cjs = require('./chunk-CJVMDRQB.cjs');
|
|
5
5
|
var chunk2DYI7MIF_cjs = require('./chunk-2DYI7MIF.cjs');
|
|
6
6
|
var promises = require('fs/promises');
|
|
@@ -13,7 +13,7 @@ async function adquirirCompartilhado(path) {
|
|
|
13
13
|
existente.refs++;
|
|
14
14
|
return criarProxy(path);
|
|
15
15
|
}
|
|
16
|
-
const lease = await
|
|
16
|
+
const lease = await chunkJYMBHNNU_cjs.acquireSessionWriter(path);
|
|
17
17
|
compartilhados.set(path, { lease, refs: 1 });
|
|
18
18
|
return criarProxy(path);
|
|
19
19
|
}
|
|
@@ -49,13 +49,30 @@ var FsSessionStore = class {
|
|
|
49
49
|
if (records.length === 0) return;
|
|
50
50
|
const path$1 = chunk2DYI7MIF_cjs.transcriptPath(this.#baseDir, this.#cwd, agentId);
|
|
51
51
|
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
52
|
-
if (!this.#leases.has(agentId)) {
|
|
53
|
-
this.#leases.set(agentId, await adquirirCompartilhado(path$1));
|
|
54
|
-
}
|
|
55
52
|
await chunkCJVMDRQB_cjs.withFileLock(path$1, async () => {
|
|
56
53
|
for (const record of records) chunkCJVMDRQB_cjs.appendJsonl(path$1, record);
|
|
57
54
|
});
|
|
58
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Toma o lease de escritor da sessão. Lança `SessionBusyError` quando outro processo a detém.
|
|
58
|
+
*
|
|
59
|
+
* **Explícito, e NÃO no `appendRecords`** — a revisão adversarial do M95 mediu por que isso
|
|
60
|
+
* importa: o contrato de `SessionStore` diz que "an `appendRecords` rejection is logged to
|
|
61
|
+
* stderr, NOT thrown to the caller (best-effort write)". Adquirir ali fazia o `SessionBusyError`
|
|
62
|
+
* ser **engolido**, e o resultado era pior que o problema original: em vez de dois escritores
|
|
63
|
+
* intercalarem linhas, o perdedor **perdia o turno em silêncio** — nada em disco, um aviso em
|
|
64
|
+
* stderr invisível sob a TUI, e o chamador sem como reagir.
|
|
65
|
+
*
|
|
66
|
+
* No init o erro chega a quem pode agir: o `exec` forka para um id novo, que é o que a própria
|
|
67
|
+
* mensagem do erro prescreve. É a diferença entre falhar onde dá para decidir e falhar onde só
|
|
68
|
+
* dá para perder.
|
|
69
|
+
*/
|
|
70
|
+
async acquire(agentId) {
|
|
71
|
+
if (this.#leases.has(agentId)) return;
|
|
72
|
+
const path$1 = chunk2DYI7MIF_cjs.transcriptPath(this.#baseDir, this.#cwd, agentId);
|
|
73
|
+
await promises.mkdir(path.dirname(path$1), { recursive: true });
|
|
74
|
+
this.#leases.set(agentId, await adquirirCompartilhado(path$1));
|
|
75
|
+
}
|
|
59
76
|
/**
|
|
60
77
|
* Solta todo lease que este store detém.
|
|
61
78
|
*
|
|
@@ -71,5 +88,5 @@ var FsSessionStore = class {
|
|
|
71
88
|
};
|
|
72
89
|
|
|
73
90
|
exports.FsSessionStore = FsSessionStore;
|
|
74
|
-
//# sourceMappingURL=chunk-
|
|
75
|
-
//# sourceMappingURL=chunk-
|
|
91
|
+
//# sourceMappingURL=chunk-IAZX7RZM.cjs.map
|
|
92
|
+
//# sourceMappingURL=chunk-IAZX7RZM.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/persistence/fs-session-store.ts"],"names":["acquireSessionWriter","readTranscript","transcriptPath","path","mkdir","dirname","withFileLock","appendJsonl"],"mappings":"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","file":"chunk-IAZX7RZM.cjs","sourcesContent":["/**\n * SE41 — `FsSessionStore`, the DEFAULT reference implementation of the pluggable\n * {@link SessionStore} seam. It reads and append-writes the native Claude-shaped\n * `.jsonl` transcript at `<baseDir>/projects/<encoded-cwd>/<agentId>.jsonl` — the\n * exact on-disk format SE40 introduced (the file the Claude Code CLI can\n * `--continue`). Omitting `local.sessionStore` resolves to this store, so the\n * default persistence path is byte-identical to SE40 behavior.\n *\n * `readRecords` is `readTranscript(transcriptPath(...))` (a missing session →\n * `[]`, not an error — a fresh agent has no history). `appendRecords` is a TRUE\n * append: it reads the prior records, concatenates the new-turn delta, and\n * rewrites the whole line set atomically under the SE40 cross-process file lock\n * (`writeTranscript` never shrinks — the native format is an append-only\n * `parentUuid` DAG). The parent dir is created BEFORE acquiring the lock because\n * the lock's companion `<path>.lock` file needs an existing parent dir (the SE40\n * `mkdir(dirname)`-before-lock fix).\n *\n * @internal\n */\n\nimport { mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { SessionStore } from \"../../types/session-store.js\";\nimport { withFileLock } from \"./file-lock.js\";\nimport { appendJsonl } from \"./jsonl.js\";\nimport { readTranscript, type SessionRecord, transcriptPath } from \"./session-transcript.js\";\nimport { acquireSessionWriter, type SessionWriterLease } from \"./session-writer.js\";\n\n/** Options identifying the on-disk transcript location for the FS default store. */\nexport interface FsSessionStoreOptions {\n /** Transcript base dir (`~/.theokit` default, `~/.claude` for CLI interop) — already `~`-expanded. */\n baseDir: string;\n /** The workspace cwd whose encoded form is the transcript project dir. */\n cwd: string;\n}\n\n/**\n * The default `SessionStore` — reads/append-writes the native `.jsonl` transcript.\n *\n * @internal\n */\n/**\n * Leases compartilhados por caminho, com contagem de referências.\n *\n * `acquireSessionWriter` é ESTRITO de propósito: duas aquisições concorrentes do mesmo caminho, e\n * exatamente uma vence — \"um lease que deixasse as duas passarem seria decorativo\" (o teste do M81\n * diz isso com todas as letras, e está certo). Esse é o contrato do primitivo, e ele não muda.\n *\n * Mas dentro de UM processo é normal existir mais de um store sobre a mesma sessão: os testes\n * golden de compactação e de sends concorrentes fazem exatamente isso, e são registro de\n * comportamento real do runtime. Aplicar o primitivo cru ali transformaria um padrão legítimo em\n * `SessionBusyError`.\n *\n * A conciliação é aqui, no consumidor do primitivo: o processo tira UM lease por caminho e conta\n * quantos stores o usam. O último a soltar libera de fato. Cross-process continua estrito — que é o\n * problema que o M81 enuncia (\"`exec resume --last` pode escrever na sessão viva da TUI\").\n */\nconst compartilhados = new Map<string, { lease: SessionWriterLease; refs: number }>();\n\nasync function adquirirCompartilhado(path: string): Promise<SessionWriterLease> {\n const existente = compartilhados.get(path);\n if (existente !== undefined) {\n existente.refs++;\n return criarProxy(path);\n }\n const lease = await acquireSessionWriter(path);\n compartilhados.set(path, { lease, refs: 1 });\n return criarProxy(path);\n}\n\n/** Um handle que decrementa a contagem; o último a soltar libera o lease de verdade. */\nfunction criarProxy(path: string): SessionWriterLease {\n let solto = false;\n return {\n sessionPath: path,\n release: async (): Promise<void> => {\n if (solto) return;\n solto = true;\n const entrada = compartilhados.get(path);\n if (entrada === undefined) return;\n entrada.refs--;\n if (entrada.refs > 0) return;\n compartilhados.delete(path);\n await entrada.lease.release();\n },\n };\n}\n\nexport class FsSessionStore implements SessionStore {\n readonly #baseDir: string;\n readonly #cwd: string;\n /** Um lease por `agentId` — um store serve mais de uma sessão ao longo da vida do processo. */\n readonly #leases = new Map<string, SessionWriterLease>();\n\n constructor(options: FsSessionStoreOptions) {\n this.#baseDir = options.baseDir;\n this.#cwd = options.cwd;\n }\n\n async readRecords(agentId: string): Promise<SessionRecord[]> {\n return readTranscript(transcriptPath(this.#baseDir, this.#cwd, agentId));\n }\n\n async appendRecords(agentId: string, records: readonly SessionRecord[]): Promise<void> {\n // Empty delta → nothing to persist (avoids an unnecessary lock + rewrite).\n if (records.length === 0) return;\n const path = transcriptPath(this.#baseDir, this.#cwd, agentId);\n // mkdir BEFORE the lock: withFileLock's companion `<path>.lock` needs the parent dir.\n await mkdir(dirname(path), { recursive: true });\n\n await withFileLock(path, async () => {\n // M93 — acrescenta o DELTA em vez de reescrever o arquivo inteiro.\n //\n // Antes: `readTranscript` + `writeTranscript` de tudo, por turno. O(n) de I/O **e** de parse a\n // cada turno, O(n²) por sessão — a nota do consumidor em `agents/lib/session/backtrack.ts`\n // registra 1,4 MB / 3000 linhas em 200 turnos.\n //\n // Correto porque o formato **já é append-only**: o DAG de `parentUuid` não depende da ordem de\n // linha, e cada registro carrega o próprio pai. `appendJsonl` **já existia no pacote** e tinha um\n // único chamador (`eval/runner.ts`) — a primitiva estava lá, o store é que a ignorava (rung 4).\n //\n // O `withFileLock` permanece — mas a afirmação anterior de que \"ele é o que serializa dois\n // `appendRecords` concorrentes\" era forte demais, e a revisão adversarial do M93 mediu isso:\n // removê-lo não reprova nenhum teste. A razão é o próprio parágrafo acima — o DAG de\n // `parentUuid` não depende da ordem de linha, então dois lotes intercalados reconstroem igual.\n // O trabalho que o lock fazia (proteger um read-modify-write) sumiu junto com o rewrite.\n //\n // O que ele ainda cobre é a janela TOCTOU de `precisaDeQuebraAntes` (ler o último byte, depois\n // escrever): sem ele, dois processos podem ambos concluir \"falta \\n\" e produzir uma linha em\n // branco — que o leitor descarta, isto é, benigno. Fica como **defesa declarada, não\n // mecanizada** (a disciplina de `error-handling.md § 4`: enumerar o resíduo em vez de deixar a\n // ausência de teste passar por cobertura).\n //\n // `writeTranscript` continua existindo para **compactação**, a única operação que legitimamente\n // reescreve o arquivo.\n for (const record of records) appendJsonl(path, record);\n });\n }\n\n /**\n * Toma o lease de escritor da sessão. Lança `SessionBusyError` quando outro processo a detém.\n *\n * **Explícito, e NÃO no `appendRecords`** — a revisão adversarial do M95 mediu por que isso\n * importa: o contrato de `SessionStore` diz que \"an `appendRecords` rejection is logged to\n * stderr, NOT thrown to the caller (best-effort write)\". Adquirir ali fazia o `SessionBusyError`\n * ser **engolido**, e o resultado era pior que o problema original: em vez de dois escritores\n * intercalarem linhas, o perdedor **perdia o turno em silêncio** — nada em disco, um aviso em\n * stderr invisível sob a TUI, e o chamador sem como reagir.\n *\n * No init o erro chega a quem pode agir: o `exec` forka para um id novo, que é o que a própria\n * mensagem do erro prescreve. É a diferença entre falhar onde dá para decidir e falhar onde só\n * dá para perder.\n */\n async acquire(agentId: string): Promise<void> {\n if (this.#leases.has(agentId)) return;\n const path = transcriptPath(this.#baseDir, this.#cwd, agentId);\n await mkdir(dirname(path), { recursive: true });\n this.#leases.set(agentId, await adquirirCompartilhado(path));\n }\n\n /**\n * Solta todo lease que este store detém.\n *\n * Sem isto o `.writer.lock` sobrevive ao processo e a próxima abertura teria de esperar a janela\n * de heartbeat — recuperável, mas 30 s de espera para um encerramento LIMPO seria um defeito\n * evitável. Idempotente: chamar duas vezes não é erro.\n */\n async dispose(): Promise<void> {\n const leases = [...this.#leases.values()];\n this.#leases.clear();\n for (const lease of leases) await lease.release();\n }\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TheokitAgentError } from './chunk-WLKFWQSJ.js';
|
|
2
|
-
import { openSync,
|
|
2
|
+
import { openSync, closeSync, rmSync, readFileSync, writeSync } from 'fs';
|
|
3
3
|
import { hostname } from 'os';
|
|
4
4
|
|
|
5
5
|
var SessionBusyError = class extends TheokitAgentError {
|
|
@@ -14,6 +14,14 @@ var SessionBusyError = class extends TheokitAgentError {
|
|
|
14
14
|
name = "SessionBusyError";
|
|
15
15
|
};
|
|
16
16
|
var JANELA_DE_HEARTBEAT_MS = 3e4;
|
|
17
|
+
function gravarDono(lockPath, dono) {
|
|
18
|
+
const fd = openSync(lockPath, "w", 384);
|
|
19
|
+
try {
|
|
20
|
+
writeSync(fd, JSON.stringify(dono));
|
|
21
|
+
} finally {
|
|
22
|
+
closeSync(fd);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
17
25
|
function lerDono(lockPath) {
|
|
18
26
|
let bruto;
|
|
19
27
|
try {
|
|
@@ -51,15 +59,15 @@ async function acquireSessionWriter(sessionPath) {
|
|
|
51
59
|
const meu = { pid: process.pid, hostname: hostname(), mtime: Date.now() };
|
|
52
60
|
let fd;
|
|
53
61
|
try {
|
|
54
|
-
fd = openSync(lockPath, "wx");
|
|
62
|
+
fd = openSync(lockPath, "wx", 384);
|
|
55
63
|
} catch (err) {
|
|
56
64
|
if (err.code !== "EEXIST") throw err;
|
|
57
65
|
if (!reclamavel(lerDono(lockPath))) throw new SessionBusyError(sessionPath);
|
|
58
|
-
|
|
66
|
+
gravarDono(lockPath, meu);
|
|
59
67
|
return criarLease(sessionPath, lockPath);
|
|
60
68
|
}
|
|
61
69
|
closeSync(fd);
|
|
62
|
-
|
|
70
|
+
gravarDono(lockPath, meu);
|
|
63
71
|
return criarLease(sessionPath, lockPath);
|
|
64
72
|
}
|
|
65
73
|
function criarLease(sessionPath, lockPath) {
|
|
@@ -75,5 +83,5 @@ function criarLease(sessionPath, lockPath) {
|
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
export { SessionBusyError, acquireSessionWriter };
|
|
78
|
-
//# sourceMappingURL=chunk-
|
|
79
|
-
//# sourceMappingURL=chunk-
|
|
86
|
+
//# sourceMappingURL=chunk-IPHW3VKP.js.map
|
|
87
|
+
//# sourceMappingURL=chunk-IPHW3VKP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal/persistence/session-writer.ts"],"names":[],"mappings":"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","file":"chunk-IPHW3VKP.js","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, readFileSync, rmSync, writeSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\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 */\n/**\n * Janela de obsolescência **entre máquinas** — e só entre elas.\n *\n * 30 s a partir da AQUISIÇÃO. O nome \"heartbeat\" seria mentira: o registro é gravado uma vez, na\n * tomada do lease, e **não** é renovado a cada escrita. A versão anterior deste comentário dizia\n * \"o dono toca o arquivo a cada aquisição, então um processo ativo nunca cruza a janela\" — falso\n * duas vezes, e a revisão adversarial mediu as duas.\n *\n * No **mesmo host** isso não importa: `reclamavel` decide pelo `pid`, que é exato, e a idade não\n * entra na conta. Entre hosts, importa e é um limite real: um dono remoto **vivo** perde o lease\n * depois de 30 s, porque não há como perguntar a outra máquina se o processo dela existe.\n *\n * **Resíduo declarado, não mecanizado** (a disciplina de `error-handling.md § 4`): renovar o\n * registro a cada append fecharia essa janela, ao custo de um `write` por turno num arquivo que\n * hoje é escrito uma vez por sessão. Não foi feito, e o caso multi-host não é o que motiva este\n * milestone — o enunciado é `exec` e TUI na mesma máquina.\n */\nexport const JANELA_DE_HEARTBEAT_MS = 30_000;\n\n/** Quem detém o lock. Gravado como JSON no `.writer.lock`. */\ninterface DonoDoLock {\n pid: number;\n hostname: string;\n mtime: number;\n}\n\n/**\n * Grava o dono com permissão restrita ao usuário.\n *\n * `0600` porque o lock é uma afirmação de POSSE: com o `0664` que o umask usual produz, outro\n * usuário do mesmo grupo pode sobrescrever o arquivo e forjar a posse da sessão — e a partir daí o\n * dono legítimo é quem passa a receber `SessionBusyError`. O conteúdo (`pid`, `hostname`) é de\n * baixa sensibilidade; o que a permissão protege é a **integridade** do sinal, não o sigilo dele.\n */\nfunction gravarDono(lockPath: string, dono: DonoDoLock): void {\n const fd = openSync(lockPath, \"w\", 0o600);\n try {\n writeSync(fd, JSON.stringify(dono));\n } finally {\n closeSync(fd);\n }\n}\n\n/** Lê o dono do lock. `undefined` quando o arquivo sumiu ou o conteúdo é ilegível. */\nfunction lerDono(lockPath: string): DonoDoLock | undefined {\n let bruto: string;\n try {\n bruto = readFileSync(lockPath, \"utf8\");\n } catch {\n return undefined; // sumiu entre o EEXIST e a leitura — corrida benigna, trate como livre\n }\n try {\n const d = JSON.parse(bruto) as Partial<DonoDoLock>;\n if (\n typeof d.pid !== \"number\" ||\n typeof d.hostname !== \"string\" ||\n typeof d.mtime !== \"number\"\n ) {\n return undefined;\n }\n return { pid: d.pid, hostname: d.hostname, mtime: d.mtime };\n } catch {\n // JSON ilegível: um lock que ninguém consegue interpretar não pode trancar a sessão para\n // sempre. Tratar como obsoleto é a escolha recuperável; o custo é o mesmo de um lock velho.\n return undefined;\n }\n}\n\n/** O processo existe? `signal 0` não envia nada — só consulta permissão/existência. */\nfunction processoVivo(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM significa que ele EXISTE e é de outro usuário.\n return (err as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/**\n * O lock pode ser tomado de quem o detém?\n *\n * ADR-2 do plano: reclamar por `pid` sozinho tem falso positivo entre máquinas — o mesmo número\n * existe noutro host, apontando para um processo sem relação. Então:\n *\n * - **mesmo host:** o `pid` é autoritativo, e **só** ele. Processo morto ⇒ reclamável na hora;\n * processo vivo ⇒ nunca, por mais velho que o lock esteja.\n * - **outro host:** o `pid` não diz nada aqui. Só a janela de heartbeat vale, porque é o único\n * sinal que não mente entre máquinas.\n *\n * ## Por que a idade NÃO conta no mesmo host\n *\n * A primeira versão fazia `velho || !processoVivo(pid)`, e isso era um defeito grave: o `mtime` é\n * gravado na **aquisição** e não é tocado a cada append, então qualquer sessão que durasse mais que\n * a janela — isto é, **toda sessão real** — passava a ser roubável por outro processo. Dois\n * escritores no mesmo transcript é exatamente o que o lease existe para impedir.\n *\n * No mesmo host a pergunta \"o dono ainda existe?\" tem resposta exata, e a idade não acrescenta\n * informação nenhuma a ela — só um modo de errar. Manter a janela ali seria heurística por cima de\n * um fato.\n */\nfunction reclamavel(dono: DonoDoLock | undefined): boolean {\n if (dono === undefined) return true; // ilegível ou sumido\n if (dono.hostname !== hostname()) {\n return Date.now() - dono.mtime > JANELA_DE_HEARTBEAT_MS;\n }\n return !processoVivo(dono.pid);\n}\n\nexport async function acquireSessionWriter(sessionPath: string): Promise<SessionWriterLease> {\n // M95 — `.writer.lock`, NÃO `.lock`.\n //\n // `withFileLock(path, fn)` já usa `<path>.lock` como companheiro. Enquanto o lease não era\n // chamado de lugar nenhum (o defeito que este milestone corrige) a colisão era teórica; ligá-lo\n // ao mesmo arquivo faria o lease de vida longa bloquear toda seção crítica curta do mesmo path.\n //\n // Dois arquivos porque são duas coisas: `withFileLock` protege uma seção com início e fim; o\n // lease é POSSE, mantida através de turnos, com `release()` explícito — a distinção que o\n // docstring do M81 acima já explica.\n const lockPath = `${sessionPath}.writer.lock`;\n const meu: DonoDoLock = { pid: process.pid, hostname: hostname(), mtime: Date.now() };\n let fd: number;\n try {\n // O modo vale na CRIAÇÃO — o `w` de `gravarDono` não altera arquivo existente.\n fd = openSync(lockPath, \"wx\", 0o600);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"EEXIST\") throw err;\n // M95 — o lock existe. Até aqui isso bastava para recusar, e era o defeito: uma TUI morta por\n // SIGKILL trancava o usuário para fora da própria sessão PERMANENTEMENTE, sem caminho de\n // recuperação documentado. Agora o lock diz quem é o dono, e um dono morto cede o lugar.\n if (!reclamavel(lerDono(lockPath))) throw new SessionBusyError(sessionPath);\n gravarDono(lockPath, meu);\n return criarLease(sessionPath, lockPath);\n }\n closeSync(fd);\n gravarDono(lockPath, meu);\n\n return criarLease(sessionPath, lockPath);\n}\n\n/** O lease em si — `release()` idempotente. Extraído porque a aquisição tem dois caminhos de saída. */\nfunction criarLease(sessionPath: string, lockPath: string): SessionWriterLease {\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"]}
|
|
@@ -2,7 +2,7 @@ import { currentCredentialPool, computeBackoffMs, sleepWithAbort, newPooledCrede
|
|
|
2
2
|
import { invalidateSessionCache } from './chunk-EEZ7CFMM.js';
|
|
3
3
|
import { tryJson, isPlainObject, sanitizeToolInput } from './chunk-U54XCYG3.js';
|
|
4
4
|
import { reconstructMessages, SessionTranscript } from './chunk-Q56IYTBZ.js';
|
|
5
|
-
import { estimateTokens } from './chunk-
|
|
5
|
+
import { estimateTokens } from './chunk-RKG6QFCJ.js';
|
|
6
6
|
import { globalSingleton, registerProvider, registerBuiltins, getProviderProfile, inferModelDialect, inferRegionFromModelId, stripBedrockPrefix, resolveBedrockBaseUrl, resolveVertexBaseUrl, stripVertexPrefix } from './chunk-KAP4GJHO.js';
|
|
7
7
|
import { NetworkError, AuthenticationError, RateLimitError, ConfigurationError, UnknownAgentError, CredentialPoolExhaustedError, isTransientError, TheokitAgentError } from './chunk-WLKFWQSJ.js';
|
|
8
8
|
import { redactSecrets } from './chunk-JTFYYKDT.js';
|
|
@@ -2948,5 +2948,5 @@ async function autoCompactIfNeeded(opts) {
|
|
|
2948
2948
|
}
|
|
2949
2949
|
|
|
2950
2950
|
export { COMPACT_SUMMARY_MARKER, COMPACT_USER_MESSAGE_MAX_TOKENS, CircuitBreaker, autoCompactIfNeeded, buildDefaultSummarizer, compactSessionTranscript, detectPrimaryProvider, discoverProviderPlugins, inferProviderFromApiKey, isCompactSummary, mapOllamaHttpError, mapOllamaTransportError, mapOpenAICompatibleError, parseSseStream, relayStream, renderToolResultContentText, resolveProviderChain, resolveSummarizerRoute, shouldAutoCompact, tryFirstEvent };
|
|
2951
|
-
//# sourceMappingURL=chunk-
|
|
2952
|
-
//# sourceMappingURL=chunk-
|
|
2951
|
+
//# sourceMappingURL=chunk-IX2LKBCD.js.map
|
|
2952
|
+
//# sourceMappingURL=chunk-IX2LKBCD.js.map
|