@tpsdev-ai/flair-mcp 0.57.0 → 0.58.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.
@@ -55,6 +55,25 @@
55
55
  * file untouched (scenario S7: compaction ≠ restart; no rotation, no new
56
56
  * sessionId).
57
57
  *
58
+ * PRE-COMPACTION RECORD (flair#2069): just before a compaction, each run of
59
+ * the `flair-precompact` hook that has something to record attempts at most
60
+ * one PUT of a row into the session's journal, once its local checks pass, in
61
+ * this row shape with meta.hook "PreCompact" (a row is added or updated only
62
+ * when Flair applies that PUT); a later run for the same harness session and
63
+ * trigger within its dedup window, which starts when the marker first names
64
+ * the id (before the first PUT is attempted), reuses the row's id, so its
65
+ * PUT, if applied, creates that row if it is absent and updates it if
66
+ * present (./precompact.ts PRECOMPACT_DEDUP_WINDOW_MS). It quotes user turns, so unlike the journal
67
+ * lines it is redacted, and it is the one continuity row whose CONTENT
68
+ * flair-session-start shows (first, after a compaction or a restart, when the
69
+ * local marker names it and the GET returns an eligible live row). Its rules
70
+ * live in ./precompact.ts; nothing above
71
+ * changes for journal rows. That
72
+ * hook arms a process-level deadline, which can fire only between
73
+ * asynchronous steps, so it reads the state file through
74
+ * bumpSeqBounded (asynchronous, size-capped with fstat before any byte is
75
+ * read) instead of the synchronous bumpSeq the capture hook uses.
76
+ *
58
77
  * FAIL-OPEN THROUGHOUT: continuity is a recovery aid, not a correctness gate.
59
78
  * Flair unreachable, a #1261 guard 400, a malformed payload, a missing state
60
79
  * file — every failure degrades to "journal nothing / hint nothing" and never
@@ -121,6 +140,34 @@ export declare function readPointer(sessionDir: string, agentId: string): Sessio
121
140
  /** Read + parse a state file. null on ANY problem — capture then journals
122
141
  * nothing (continuity wasn't seeded for this harness session). */
123
142
  export declare function readState(sessionDir: string, agentId: string, harnessSessionId: string): SessionState | null;
143
+ /**
144
+ * Upper bound on a session file read through readSmallFile: the state file and
145
+ * the pre-compaction marker (./precompact.ts). Each is a few hundred bytes when
146
+ * Flair wrote it, so anything larger is refused unread.
147
+ */
148
+ export declare const SESSION_FILE_MAX_BYTES: number;
149
+ /** A bounded read's outcome. Only a missing file is "absent"; every other
150
+ * failure is "refused" with a short reason, never an empty read. */
151
+ export type SmallFileRead = {
152
+ kind: "absent";
153
+ } | {
154
+ kind: "ok";
155
+ text: string;
156
+ } | {
157
+ kind: "refused";
158
+ detail: string;
159
+ };
160
+ /**
161
+ * Read a small regular file without ever holding the event loop:
162
+ * - the open is non-blocking, so a FIFO at the path cannot stall it;
163
+ * - the opened descriptor is checked with fstat BEFORE any byte is read, and
164
+ * anything that is not a regular file of at most `maxBytes` is refused;
165
+ * - at most `maxBytes + 1` bytes are ever read, so a file that grew after
166
+ * the check is refused too;
167
+ * - every step is asynchronous, so a process-level deadline can fire
168
+ * between them.
169
+ */
170
+ export declare function readSmallFile(path: string, maxBytes?: number): Promise<SmallFileRead>;
124
171
  /**
125
172
  * Mint a fresh sessionId + processUUID, seed the per-harness-session state
126
173
  * file (seq 0) and rotate the pointer file to the new session. Called by the
@@ -143,6 +190,28 @@ export declare function seedSession(sessionDir: string, agentId: string, harness
143
190
  * its seq — gaps are fine, non-monotonicity is not.
144
191
  */
145
192
  export declare function bumpSeq(sessionDir: string, agentId: string, harnessSessionId: string, now?: Date): SessionState | null;
193
+ /** bumpSeqBounded's outcome. "absent" (never seeded) is kept apart from
194
+ * "unreadable" (a state file that exists but was refused or did not parse),
195
+ * so a caller that reports it never words the second as the first. */
196
+ export type SeqBump = {
197
+ kind: "bumped";
198
+ state: SessionState;
199
+ } | {
200
+ kind: "absent";
201
+ } | {
202
+ kind: "unreadable";
203
+ detail: string;
204
+ } | {
205
+ kind: "unwritable";
206
+ };
207
+ /**
208
+ * bumpSeq for a caller with a process-level deadline (the PreCompact hook):
209
+ * the same increment and the same temp-file + rename, with every step
210
+ * asynchronous and the read bounded by readSmallFile (size-capped with fstat
211
+ * before any byte is read; a FIFO or other non-regular file refused). Never
212
+ * throws.
213
+ */
214
+ export declare function bumpSeqBounded(sessionDir: string, agentId: string, harnessSessionId: string, now?: Date): Promise<SeqBump>;
146
215
  /** Subset of the Claude Code hook payload the capture bin reads. It extracts
147
216
  * ONLY these fields — the raw hook JSON (tool_input.command, tool_response,
148
217
  * transcript_path, …) is NEVER stored or forwarded. */
@@ -55,12 +55,32 @@
55
55
  * file untouched (scenario S7: compaction ≠ restart; no rotation, no new
56
56
  * sessionId).
57
57
  *
58
+ * PRE-COMPACTION RECORD (flair#2069): just before a compaction, each run of
59
+ * the `flair-precompact` hook that has something to record attempts at most
60
+ * one PUT of a row into the session's journal, once its local checks pass, in
61
+ * this row shape with meta.hook "PreCompact" (a row is added or updated only
62
+ * when Flair applies that PUT); a later run for the same harness session and
63
+ * trigger within its dedup window, which starts when the marker first names
64
+ * the id (before the first PUT is attempted), reuses the row's id, so its
65
+ * PUT, if applied, creates that row if it is absent and updates it if
66
+ * present (./precompact.ts PRECOMPACT_DEDUP_WINDOW_MS). It quotes user turns, so unlike the journal
67
+ * lines it is redacted, and it is the one continuity row whose CONTENT
68
+ * flair-session-start shows (first, after a compaction or a restart, when the
69
+ * local marker names it and the GET returns an eligible live row). Its rules
70
+ * live in ./precompact.ts; nothing above
71
+ * changes for journal rows. That
72
+ * hook arms a process-level deadline, which can fire only between
73
+ * asynchronous steps, so it reads the state file through
74
+ * bumpSeqBounded (asynchronous, size-capped with fstat before any byte is
75
+ * read) instead of the synchronous bumpSeq the capture hook uses.
76
+ *
58
77
  * FAIL-OPEN THROUGHOUT: continuity is a recovery aid, not a correctness gate.
59
78
  * Flair unreachable, a #1261 guard 400, a malformed payload, a missing state
60
79
  * file — every failure degrades to "journal nothing / hint nothing" and never
61
80
  * blocks the agent's turn or boot.
62
81
  */
63
- import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
82
+ import { chmodSync, constants as fsConstants, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
83
+ import { chmod, open, rename, writeFile } from "node:fs/promises";
64
84
  import { homedir } from "node:os";
65
85
  import { join } from "node:path";
66
86
  import { randomUUID } from "node:crypto";
@@ -161,31 +181,100 @@ export function readPointer(sessionDir, agentId) {
161
181
  return null;
162
182
  }
163
183
  }
164
- /** Read + parse a state file. null on ANY problem — capture then journals
165
- * nothing (continuity wasn't seeded for this harness session). */
166
- export function readState(sessionDir, agentId, harnessSessionId) {
184
+ /** Parse a state file's text: the state, or why it is not one. */
185
+ function parseState(raw, agentId, harnessSessionId) {
186
+ let parsed;
167
187
  try {
168
- const raw = readFileSync(statePath(sessionDir, agentId, harnessSessionId), "utf-8");
169
- const parsed = JSON.parse(raw);
170
- if (parsed &&
171
- typeof parsed.sessionId === "string" && parsed.sessionId !== "" &&
172
- typeof parsed.processUUID === "string" && parsed.processUUID !== "" &&
173
- typeof parsed.seq === "number" && Number.isFinite(parsed.seq)) {
174
- return {
188
+ parsed = JSON.parse(raw);
189
+ }
190
+ catch {
191
+ return { ok: false, detail: "malformed JSON" };
192
+ }
193
+ if (parsed &&
194
+ typeof parsed.sessionId === "string" && parsed.sessionId !== "" &&
195
+ typeof parsed.processUUID === "string" && parsed.processUUID !== "" &&
196
+ typeof parsed.seq === "number" && Number.isFinite(parsed.seq)) {
197
+ return {
198
+ ok: true,
199
+ state: {
175
200
  sessionId: parsed.sessionId,
176
201
  processUUID: parsed.processUUID,
177
202
  seq: parsed.seq,
178
203
  agentId: typeof parsed.agentId === "string" ? parsed.agentId : agentId,
179
204
  harnessSessionId: typeof parsed.harnessSessionId === "string" ? parsed.harnessSessionId : harnessSessionId,
180
205
  updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : "",
181
- };
182
- }
183
- return null;
206
+ },
207
+ };
208
+ }
209
+ return { ok: false, detail: "unexpected shape" };
210
+ }
211
+ /** Read + parse a state file. null on ANY problem — capture then journals
212
+ * nothing (continuity wasn't seeded for this harness session). */
213
+ export function readState(sessionDir, agentId, harnessSessionId) {
214
+ try {
215
+ const parsed = parseState(readFileSync(statePath(sessionDir, agentId, harnessSessionId), "utf-8"), agentId, harnessSessionId);
216
+ return parsed.ok ? parsed.state : null;
184
217
  }
185
218
  catch {
186
219
  return null;
187
220
  }
188
221
  }
222
+ // ── bounded asynchronous reads (the PreCompact hook's deadline) ─────────────
223
+ /**
224
+ * Upper bound on a session file read through readSmallFile: the state file and
225
+ * the pre-compaction marker (./precompact.ts). Each is a few hundred bytes when
226
+ * Flair wrote it, so anything larger is refused unread.
227
+ */
228
+ export const SESSION_FILE_MAX_BYTES = 16 * 1024;
229
+ /**
230
+ * Read a small regular file without ever holding the event loop:
231
+ * - the open is non-blocking, so a FIFO at the path cannot stall it;
232
+ * - the opened descriptor is checked with fstat BEFORE any byte is read, and
233
+ * anything that is not a regular file of at most `maxBytes` is refused;
234
+ * - at most `maxBytes + 1` bytes are ever read, so a file that grew after
235
+ * the check is refused too;
236
+ * - every step is asynchronous, so a process-level deadline can fire
237
+ * between them.
238
+ */
239
+ export async function readSmallFile(path, maxBytes = SESSION_FILE_MAX_BYTES) {
240
+ let handle;
241
+ try {
242
+ handle = await open(path, fsConstants.O_RDONLY | (fsConstants.O_NONBLOCK ?? 0));
243
+ }
244
+ catch (err) {
245
+ const code = err?.code;
246
+ if (code === "ENOENT")
247
+ return { kind: "absent" };
248
+ return { kind: "refused", detail: typeof code === "string" ? code : "unreadable" };
249
+ }
250
+ try {
251
+ const opened = await handle.stat();
252
+ if (!opened.isFile())
253
+ return { kind: "refused", detail: "not a regular file" };
254
+ if (opened.size > maxBytes)
255
+ return { kind: "refused", detail: `larger than ${maxBytes} bytes` };
256
+ // Read to EOF, not to the size fstat reported, into a buffer one byte
257
+ // larger than the cap: a file that grew past the cap is refused, never cut.
258
+ const buf = Buffer.alloc(maxBytes + 1);
259
+ let got = 0;
260
+ while (got < buf.length) {
261
+ const { bytesRead } = await handle.read(buf, got, buf.length - got, got);
262
+ if (bytesRead === 0)
263
+ break;
264
+ got += bytesRead;
265
+ }
266
+ if (got > maxBytes)
267
+ return { kind: "refused", detail: `larger than ${maxBytes} bytes` };
268
+ return { kind: "ok", text: buf.subarray(0, got).toString("utf8") };
269
+ }
270
+ catch (err) {
271
+ const code = err?.code;
272
+ return { kind: "refused", detail: typeof code === "string" ? code : "unreadable" };
273
+ }
274
+ finally {
275
+ await handle.close().catch(() => undefined);
276
+ }
277
+ }
189
278
  /**
190
279
  * Mint a fresh sessionId + processUUID, seed the per-harness-session state
191
280
  * file (seq 0) and rotate the pointer file to the new session. Called by the
@@ -237,6 +326,35 @@ export function bumpSeq(sessionDir, agentId, harnessSessionId, now = new Date())
237
326
  return null;
238
327
  }
239
328
  }
329
+ /**
330
+ * bumpSeq for a caller with a process-level deadline (the PreCompact hook):
331
+ * the same increment and the same temp-file + rename, with every step
332
+ * asynchronous and the read bounded by readSmallFile (size-capped with fstat
333
+ * before any byte is read; a FIFO or other non-regular file refused). Never
334
+ * throws.
335
+ */
336
+ export async function bumpSeqBounded(sessionDir, agentId, harnessSessionId, now = new Date()) {
337
+ const finalPath = statePath(sessionDir, agentId, harnessSessionId);
338
+ const read = await readSmallFile(finalPath);
339
+ if (read.kind === "absent")
340
+ return { kind: "absent" };
341
+ if (read.kind === "refused")
342
+ return { kind: "unreadable", detail: read.detail };
343
+ const parsed = parseState(read.text, agentId, harnessSessionId);
344
+ if (!parsed.ok)
345
+ return { kind: "unreadable", detail: parsed.detail };
346
+ const next = { ...parsed.state, seq: parsed.state.seq + 1, updatedAt: now.toISOString() };
347
+ const tmpPath = `${finalPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
348
+ try {
349
+ await writeFile(tmpPath, JSON.stringify(next, null, 2) + "\n", { mode: 0o600 });
350
+ await chmod(tmpPath, 0o600);
351
+ await rename(tmpPath, finalPath);
352
+ return { kind: "bumped", state: next };
353
+ }
354
+ catch {
355
+ return { kind: "unwritable" };
356
+ }
357
+ }
240
358
  /** HARD truncate at CAPTURE_BOUND_CHARS with a visible ellipsis, so a reader
241
359
  * knows the excerpt is incomplete and never acts on a cut sentence as if it
242
360
  * were whole. See CAPTURE_BOUND_CHARS — the bound is load-bearing. */
@@ -56,7 +56,8 @@ export declare const ENV_RESURRECTED_BY_FLAIR_CLIENT: readonly ["FLAIR_URL"];
56
56
  export declare function stripInterpolationLiteralsFromEnv(env?: NodeJS.ProcessEnv, names?: readonly string[]): void;
57
57
  /**
58
58
  * Hook probe mode (flair#1007) — shared by every hook binary this package
59
- * ships (session-start-hook.ts, continuity-capture-hook.ts). `flair doctor`
59
+ * ships (session-start-hook.ts, continuity-capture-hook.ts,
60
+ * prompt-recall-hook.ts). `flair doctor`
60
61
  * sets FLAIR_HOOK_PROBE to ask "does this command still resolve and execute?"
61
62
  * and a probed binary answers by exiting immediately, before stdin, clients,
62
63
  * network or any side effect — being reached at all IS the answer.
package/dist/env-guard.js CHANGED
@@ -70,7 +70,8 @@ export function stripInterpolationLiteralsFromEnv(env = process.env, names = ENV
70
70
  }
71
71
  /**
72
72
  * Hook probe mode (flair#1007) — shared by every hook binary this package
73
- * ships (session-start-hook.ts, continuity-capture-hook.ts). `flair doctor`
73
+ * ships (session-start-hook.ts, continuity-capture-hook.ts,
74
+ * prompt-recall-hook.ts). `flair doctor`
74
75
  * sets FLAIR_HOOK_PROBE to ask "does this command still resolve and execute?"
75
76
  * and a probed binary answers by exiting immediately, before stdin, clients,
76
77
  * network or any side effect — being reached at all IS the answer.
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Flair PreCompact hook for Claude Code (flair#2069): when the transcript
4
+ * tail holds something to record and the local checks pass, attempt one PUT
5
+ * of a bounded continuity record before context is lost (to a new row, or,
6
+ * within the dedup window, to the same row), so the next session start can
7
+ * show it first. The record's
8
+ * content, bounds, redaction, storage and dedup are defined in
9
+ * ./precompact.ts; this file is the binary around them.
10
+ *
11
+ * PER RUN
12
+ * -------
13
+ * 1. Reads Claude Code's PreCompact payload on stdin: `hook_event_name`
14
+ * ("PreCompact"), `session_id`, `transcript_path` and `trigger`
15
+ * ("manual" for /compact, "auto" for automatic compaction).
16
+ * 2. Finds this harness session's continuity state (seeded by
17
+ * flair-session-start) and consumes a journal seq, as
18
+ * flair-continuity-capture does, through the bounded asynchronous
19
+ * bumpSeqBounded.
20
+ * 3. Reads the transcript TAIL (bounded in bytes and lines) and builds the
21
+ * record: standing instructions, open tasks, in-flight work and the last
22
+ * assistant message, redacted and cut to the record bound. Nothing to
23
+ * record: nothing written.
24
+ * 4. Resolves the record id through the local marker (a later run for the
25
+ * same harness session and trigger within the dedup window reuses it),
26
+ * writes the marker, then writes the row with one signed
27
+ * `PUT /Memory/<id>` as the agent's own Ed25519 identity.
28
+ *
29
+ * EXIT 0 ON EVERY PATH IT HANDLES
30
+ * -------------------------------
31
+ * Claude Code blocks compaction when a PreCompact hook exits 2 or prints a
32
+ * `decision: "block"` object. Once this binary has started, it does neither
33
+ * on any path it handles: it exits 0 and prints either nothing or ONE
34
+ * `{"systemMessage": …}` object (a warning Claude Code shows the user). A
35
+ * failure before that (the launcher, the runtime's start-up, a static import
36
+ * that fails to load) is outside the binary; the documented command's
37
+ * `|| true` covers it. The time budget (FLAIR_PRECOMPACT_TIMEOUT_MS,
38
+ * default 5 s) is a process-level deadline armed in main(), after the module
39
+ * has loaded and the entry-point check (isDirectRun) has run, and before
40
+ * stdin is read. When it passes, the timer starts finishing whatever
41
+ * asynchronous work is still pending (stdin held open, a slow read, a write
42
+ * in flight): the hook prints the one timeout note when FLAIR_AGENT_ID is
43
+ * set, and nothing when it is not (main() decides this when it arms the
44
+ * deadline), then exits 0 once stdout drains, waiting at most a further
45
+ * STDOUT_DRAIN_GRACE_MS (1 s).
46
+ * Synchronous work can delay the timer itself; Claude Code's hook timeout is
47
+ * the outer bound. stdin is read up
48
+ * to STDIN_MAX_BYTES; a larger payload is ignored. The hook's own local files
49
+ * (the transcript, the continuity state file and the marker) are read
50
+ * asynchronously with a size cap checked (fstat) before any byte is read: the
51
+ * transcript by its tail caps, the state file and the marker by
52
+ * SESSION_FILE_MAX_BYTES. A state file or marker larger than that, or anything
53
+ * at those three paths that is not a regular file, is refused with a note.
54
+ * Its local writes are asynchronous too, so none of the hook's own file work
55
+ * can hold the process past the deadline.
56
+ * The deadline bounds asynchronous work only: a timer cannot preempt
57
+ * synchronous code, and one local read is synchronous and not this hook's
58
+ * own: flair-client reads the agent's key file synchronously while the client
59
+ * is built, outside the size caps above. The outer bound on the whole process
60
+ * is Claude Code's own hook `timeout` in the settings entry. What runs before
61
+ * main() arms the deadline (the launcher, the runtime's start-up, module
62
+ * loading, the entry-point check) is outside the budget too.
63
+ *
64
+ * NOTES (the only output)
65
+ * -----------------------
66
+ * Silent: a probe, a malformed or non-PreCompact payload, no FLAIR_AGENT_ID,
67
+ * a tail with nothing to record, and success. One note, naming the reason: no
68
+ * continuity state for the session, a state file that could not be read (too
69
+ * large, not a regular file, malformed) or updated, an unreadable transcript,
70
+ * a marker that could not be read or written, and a write that was not
71
+ * confirmed (named by its kind, from classifyPreCompactFailure's three
72
+ * inputs: a numeric HTTP status, 401 or 403 as auth and any other as
73
+ * http-<status>; the hook's own timer or an error named exactly TimeoutError,
74
+ * as timeout; anything else as unreachable. Never by an error's message text
75
+ * or URL. Worded as "may be missing", never as "not saved", because the
76
+ * server may have applied it). A note that names a state file or the marker
77
+ * shows its path through notePath, which changes it only where each rule
78
+ * applies (see notePath).
79
+ *
80
+ * IDENTITY
81
+ * --------
82
+ * The agent's own key, resolved like the other hooks (FLAIR_AGENT_ID +
83
+ * FLAIR_KEY_PATH or the standard key locations). The client is built with an
84
+ * empty admin pair, which turns off flair-client's FLAIR_ADMIN_USER /
85
+ * FLAIR_ADMIN_PASSWORD Basic fallback: the record is written as the agent or
86
+ * not at all.
87
+ *
88
+ * CONFIG (env)
89
+ * FLAIR_AGENT_ID (required; absent → silent no-op)
90
+ * FLAIR_URL (default via flair-client)
91
+ * FLAIR_KEY_PATH (default ~/.flair/keys/<agent>.key via flair-client)
92
+ * FLAIR_PRECOMPACT_TIMEOUT_MS (default 5000; 250..15000)
93
+ * FLAIR_SESSION_DIR (default ~/.flair/session; tests)
94
+ * FLAIR_HOOK_PROBE (probe mode: exit 0 before stdin, files or network)
95
+ */
96
+ import { type ContinuityClient } from "./continuity.js";
97
+ type Env = Record<string, string | undefined>;
98
+ export declare const ENV_PRECOMPACT_TIMEOUT_MS = "FLAIR_PRECOMPACT_TIMEOUT_MS";
99
+ export declare const DEFAULT_PRECOMPACT_TIMEOUT_MS = 5000;
100
+ export declare const PRECOMPACT_TIMEOUT_FLOOR_MS = 250;
101
+ export declare const PRECOMPACT_TIMEOUT_CEILING_MS = 15000;
102
+ /** Upper bound on the hook's stdin (the PreCompact payload is a few hundred bytes). */
103
+ export declare const STDIN_MAX_BYTES: number;
104
+ /** The process-level budget (armed in main(), before stdin is read; when it passes, finishing starts and the process exits once stdout drains, at most STDOUT_DRAIN_GRACE_MS later):
105
+ * FLAIR_PRECOMPACT_TIMEOUT_MS when in range, else the default. */
106
+ export declare function resolvePreCompactBudgetMs(env?: Env): number;
107
+ /** The hook's OWN budget timer. Recognized by identity, never by message. */
108
+ export declare class PreCompactTimeoutError extends Error {
109
+ constructor();
110
+ }
111
+ export type PreCompactFailureKind = "auth" | "timeout" | "unreachable" | `http-${number}`;
112
+ /** Same rules as the session-start classifier (flair#1943): a numeric HTTP
113
+ * status first (401/403 → auth), then the hook's own timer or an error named
114
+ * exactly TimeoutError, else unreachable. Message text is never read. */
115
+ export declare function classifyPreCompactFailure(err: unknown): PreCompactFailureKind;
116
+ /** The hook's only non-empty output: ONE JSON object with a `systemMessage`
117
+ * (shown to the user). Never a `decision` field: that could block compaction. */
118
+ export declare function preCompactNote(text: string): string;
119
+ /** The note for a write that was not confirmed. It never says the record
120
+ * was not saved: after a timeout the server may still complete the write,
121
+ * and after any other error the server may already have applied it (the
122
+ * client can fail while reading or parsing the answer). What is known is
123
+ * that the save was not confirmed, so the record may be missing. */
124
+ export declare function writeFailedNote(kind: PreCompactFailureKind): string;
125
+ /** The longest local path a note shows, in characters. */
126
+ export declare const NOTE_PATH_MAX_CHARS = 200;
127
+ /**
128
+ * A local path as a note shows it. Each change applies only where it fits:
129
+ * - a path inside the home directory (`env.HOME`, else the OS's) starts
130
+ * with "~" instead; a home directory of "/" collapses nothing;
131
+ * - each control or line-break character is shown as "?";
132
+ * - strings that match the credential patterns are redacted
133
+ * (redactSecrets);
134
+ * - a result longer than NOTE_PATH_MAX_CHARS is cut to that, ending in "…".
135
+ * A path outside the home directory that none of these touch is shown as it
136
+ * is. The session directory and the agent id in the path are configuration;
137
+ * a secret in them that matches no pattern is shown as written.
138
+ */
139
+ export declare function notePath(path: string, env?: Env): string;
140
+ export type PreCompactReason = "malformed-input" | "not-precompact" | "no-agent-id" | "bad-session-id" | "no-state" | "state-unreadable" | "state-unwritable" | "no-transcript" | "nothing-to-record" | "marker-unreadable" | "marker-unwritable" | "write-failed" | "written";
141
+ export interface PreCompactOutcome {
142
+ /** The exact string the binary prints ("" prints nothing). */
143
+ output: string;
144
+ /** Why. Diagnostic only: the exit code is 0 regardless. */
145
+ reason: PreCompactReason;
146
+ /** The record id, once one was resolved. */
147
+ recordId?: string;
148
+ /** True when this run reused the marker's record id. */
149
+ reused?: boolean;
150
+ }
151
+ export interface PreCompactDeps {
152
+ makeClient?: (agentId: string, timeoutMs: number, env: Env) => ContinuityClient | Promise<ContinuityClient>;
153
+ env?: Env;
154
+ sessionDir?: string;
155
+ now?: () => Date;
156
+ /** When the budget started (epoch ms). The entry point passes its own start. */
157
+ startedAt?: number;
158
+ /** The process-level budget in ms (default: resolvePreCompactBudgetMs(env)). */
159
+ budgetMs?: number;
160
+ }
161
+ /**
162
+ * Core flow with injectable dependencies. Never throws for a failed read or
163
+ * write; returns the exact output plus a diagnostic reason.
164
+ */
165
+ export declare function runPreCompact(rawInput: string, deps?: PreCompactDeps): Promise<PreCompactOutcome>;
166
+ /**
167
+ * Whether this module is the process entry point. Where the runtime provides
168
+ * `import.meta.main` (Bun; Node 22.18+), its answer decides, true or false.
169
+ * Otherwise compare FILESYSTEM paths resolved through symlinks: the module URL
170
+ * is percent-encoded and an npm bin shim is a symlink, so comparing the URL
171
+ * string with `argv[1]` misses both.
172
+ */
173
+ export declare function isDirectRun(moduleUrl: string, argv1: string | undefined, metaMain: boolean | undefined, realpath?: (p: string) => string): boolean;
174
+ export {};