sidekick-shared 0.27.0 → 0.27.2

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 CHANGED
@@ -5,6 +5,42 @@ All notable changes to sidekick-shared will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.27.2] - 2026-09-22
9
+
10
+ ### Fixed
11
+
12
+ - `collectPromptHistory()` streams session files instead of reading each unread range into one buffer. A file whose unread part exceeded `maxFileBytes` used to be skipped on every call; now it is read in 1 MiB chunks up to `maxFileBytes` per call, stops after a complete line, keeps what it returned, and continues on the next call (cursor `backlog: true`), even if its size and mtime are unchanged. Memory stays near one chunk plus one record. Deadline, abort, `maxSessions`, `maxTotalBytes`, and the new `maxEntries` stop at the last complete line the same way instead of discarding the partly read file. The first file of a call always finishes its first record, so a record larger than the budget cannot stall. Lines split only at `\n`, so UTF-8 characters across chunk edges decode exactly; half-written final lines and bytes appended during a call wait for the next call.
13
+ - Late answer metadata no longer depends on a replay window. `pending` now keeps `lineBytes`, `timestamp`, and `answer: { status, id?, model?, usage? }` (answer state, never text), the scan continues from the file's offset however far the answer is, and only the prompt's own line is re-read (and verified against its length and timestamp) when the prompt must be returned again. Replays that 0.27.1 dropped for exceeding `maxFileBytes` no longer happen.
14
+
15
+ ### Added
16
+
17
+ - Bounds `maxRecordBytes` (default 16 MiB; a longer record is skipped as it streams past, without buffering, even across calls while it is still being written) and `maxEntries` (default 10 000), and bound name `'maxEntries'`.
18
+ - `PromptHistoryResult.hasMore` (a bound left bytes or files unread) and `exclusions: PromptHistoryExclusion[]` (`{ provider, sessionId, reason: 'oversizedRecord' | 'malformedRecord', offset }`, up to 1000 per call). These are the only permanent exclusions; a prompt inside such a record is lost and consumes no ordinal.
19
+ - Stats `sessionsWithBacklog`, `sessionsRewritten`, `recordsOverSizeLimit`, and `recordsMalformed`; types `PromptHistoryAnswerState` and `PromptHistoryExclusion`.
20
+ - Rewrite detection: a file that shrank below its offset, whose first 4 KiB digest (`head`) changed, whose resume point no longer follows a newline, or whose pending prompt line no longer matches is read again from the start with fresh ordinals.
21
+
22
+ ### Changed
23
+
24
+ - The cursor is `version: 3`: sessions gain optional `backlog`, `skipping`, and `head`; `pending` gains `lineBytes`, `timestamp`, and `answer`; the cursor gains `next`, the key of the file a call-wide stop left off at, where the next call starts. Version 1 and 2 cursors are accepted: each file is re-verified once, and a version 2 `pending` prompt is replayed from its line in resumable passes. Files 0.27.1 skipped for size were never in its cursor and are read in full; metadata 0.27.1 already abandoned is not recoverable. A session carried unchanged from an older cursor and not visited keeps its old shape (for example `pending` without `answer`).
25
+ - `maxFileBytes` is now a per-file, per-call read budget instead of a skip threshold, and `stats.filesOverSizeLimit` counts files that used it up and continue on the next call.
26
+ - The Claude first-answer fallback id for assistant records without `message.id` is the line's byte offset instead of a per-scan line index, so it stays stable across calls.
27
+
28
+ ## [0.27.1] - 2026-09-22
29
+
30
+ ### Fixed
31
+
32
+ - `collectPromptHistory()` returns a prompt again when its first answering call is written after the prompt was collected. Previously the cursor moved past the prompt and a resumed scan could never attach the answer's `model` / `usage`. Now, while a file's last emitted prompt is not final, the cursor keeps `pending: { ordinal, offset, emitted, state? }`: the byte offset of the prompt's line, a 16-hex digest of the model, usage, and status last returned, and (Codex) the parse state from just before that line. The next call replays from that offset, so the prompt's `provider`, `sessionId`, `ordinal`, `timestamp`, and `text` come from the log again and are never synthesized or renumbered, and returns it only if its metadata digest changed. No prompt or assistant text is stored in the cursor. A replay that would exceed `maxFileBytes` is dropped in favour of the committed offset, and that prompt is not revisited.
33
+ - First-answer attribution: Claude split records of the first call replace usage rather than adding it, a later assistant message id, a `tool_result`, or a non-null `stop_reason` closes the first call, later tool-loop calls never replace it, and `<synthetic>` records (API errors, interrupts) are ignored instead of contributing zero usage. Codex closes on the first `token_count` with `last_token_usage`; a `token_count` without usage (rate-limit only) does not. Missing usage stays `undefined`.
34
+ - Documentation: cursor keys are path-free, but values are not (Codex `state.cwd` / `gitBranch`), so the cursor is sensitive, local-only state; a file over `maxFileBytes` is skipped and calling again with the same bounds skips it again.
35
+
36
+ ### Added
37
+
38
+ - `PromptHistoryEntry.metadataStatus`: `pending` (no answering call yet), `provisional` (Claude call seen with `stop_reason: null`), or `final` (complete, or closed without an answer). A prompt closed by the next prompt without an answer is returned once more as `final`. New exported types `PromptHistoryMetadataStatus` and `PromptHistoryPendingPrompt`.
39
+
40
+ ### Changed
41
+
42
+ - `PromptHistoryCursor.version` is now `2` and sessions may carry `pending`. Version 1 cursors are accepted as input and upgraded; prompts they already returned without metadata cannot be recovered. Unknown versions are ignored (full rescan, same identities).
43
+
8
44
  ## [0.27.0] - 2026-09-22
9
45
 
10
46
  ### Added
package/README.md CHANGED
@@ -9,6 +9,8 @@ Types, parsers, providers, readers, formatters, aggregation, search, reporting,
9
9
 
10
10
  ## Recent additions
11
11
 
12
+ - **0.27.2: streamed prompt history** — `collectPromptHistory()` reads session logs of any size in bounded, resumable passes instead of skipping files over `maxFileBytes`: every call keeps what it read, the cursor (`version: 3`) continues where each file stopped, and a pending prompt keeps its answer state so a far-away answer still settles it. Adds `maxRecordBytes` and `maxEntries` bounds, `hasMore`, permanently skipped records in `exclusions`, and rewrite detection.
13
+ - **0.27.1: late prompt metadata** — `collectPromptHistory()` returns a prompt again, with the same provider, sessionId, ordinal, timestamp, and text, when its first answering call is written after an earlier collection, and entries gain `metadataStatus` (`pending` / `provisional` / `final`). The cursor becomes `version: 2`, keeps a replay offset and metadata digest for the one open prompt per file (no prompt text), and still accepts 0.27.0 cursors.
12
14
  - **0.27.0: human prompt history** — `collectPromptHistory()` (Node-only) returns the prompts a person typed in Claude Code and Codex sessions under a set of workspace roots (plus git worktree siblings), with fail-closed cwd scoping, untruncated text, stable per-session ordinals, the answering model and usage, a JSON-serializable resume cursor, and bounds that stop cleanly and report themselves in `boundsHit`. `isHumanPrompt()` / `humanPromptText()` expose the same classification for any canonical transcript message, and transcript provenance now keeps Claude Code's `isCompactSummary`, `originKind`, and `promptSource`. Claude Opus 5.5 gets its own pricing and context entries.
13
15
  - **0.26.6: verified account switching** — `syncLiveAccountState()` reconciles the live Claude Code and Codex logins with saved profiles (registers unknown logins under their email, folds rotated tokens, merges duplicate Codex profiles, re-points the active pointer) and runs at startup, on account-change events, and as phase one of every switch; `getAccountHealth()` / `listAccountsWithHealth()` report secret-free `fresh | expiring | expired | unknown | missing` health; `switchAccount[Async]()` returns a `SwitchAccountResult` with verification, `runningConsumers`, `needsLogin`, and an `undoToken` for `undoLastSwitch()`; `detectRunningAccountConsumers()`, `getAccountLaunchEnv()`, `refreshInactiveAccounts()`, and `getCodexCredentialStoreMode()` are new. Claude logins run `claude auth login`, the Keychain suffix follows Claude Code 2.1.x, and the unused shell-hook helpers were removed.
14
16
  - **0.26.5: event-scoped collection** — `ObservedSessionCollector.subscribe()` reconciles each watch event with one `statWatchedSessionFile()` stat and runs a full `discover()` only for unknown paths, catch-up polls, and the initial pass, gated by `minReconcileGapMs`; `discover({ limit, since })` / `collect({ limit, since })` bound a pass, `maxCacheEntries` / `maxCacheBytes` bound the parse cache, `DirectoryListingCache` makes a full pass one stat per unchanged directory, and the info-severity `provider-discovery-completed` diagnostic reports what each pass cost.
@@ -294,7 +296,7 @@ const humanPrompts = transcript.messages
294
296
  .map((message) => humanPromptText(message, 'claude-code')); // `/review foo` for slash commands
295
297
  ```
296
298
 
297
- A Claude Code message is a human prompt when it has the user role, an entrypoint in `HUMAN_CLAUDE_ENTRYPOINTS` (`cli`, `claude-desktop`, `sdk-cli` — Agent SDK programs report `sdk-ts` / `sdk-py` and are excluded), is not meta, sidechain, or a compaction summary, has no non-human `origin.kind` (task notifications, coordinator messages, auto-continuations) or `promptSource: 'system'`, and is not tool-result-only, local-command output or caveat, a system reminder, or an interrupt marker. A Codex message is one when it has the user role and some text is left after injected `AGENTS.md`, `<environment_context>`, `<user_instructions>`, turn-aborted, skill, subagent-notification, and internal-context blocks and `<image>` wrappers are dropped. Codex session source is a session-level rule: `collectPromptHistory()` reads only sessions whose `session_meta.source` is in `HUMAN_CODEX_SOURCES` (`cli`, `vscode`, `exec`) and skips subagent and forked sessions.
299
+ A Claude Code message is a human prompt when it has the user role, an entrypoint in `HUMAN_CLAUDE_ENTRYPOINTS` (`cli`, `claude-desktop`, `sdk-cli` — Agent SDK programs report `sdk-ts` / `sdk-py` and are excluded), is not meta, sidechain, or a compaction summary, has no non-human `origin.kind` (task notifications, coordinator messages, auto-continuations) or `promptSource: 'system'`, and is not tool-result-only, local-command output or caveat, bash-mode input or output, a hook or task notification, a system reminder, a continued-session summary, or an interrupt marker. A Codex message is one when it has the user role and some text is left after injected `AGENTS.md`, `<environment_context>`, `<user_instructions>`, permissions and collaboration-mode, recommended-plugin, user-shell-command, turn-aborted, skill, subagent-notification, and internal-context blocks and `<image>` wrappers are dropped. Codex session source is a session-level rule: `collectPromptHistory()` reads only sessions whose `session_meta.source` is in `HUMAN_CODEX_SOURCES` (`cli`, `vscode`, `exec`) and skips subagent and forked sessions.
298
300
 
299
301
  ### Collect human prompt history for workspace roots
300
302
 
@@ -309,25 +311,32 @@ const result = await collectPromptHistory({
309
311
  cursor,
310
312
  bounds: {
311
313
  maxSessions: 1000,
312
- maxFileBytes: 16 << 20,
314
+ maxFileBytes: 16 << 20, // per file per call; the rest is read next call
313
315
  maxTotalBytes: 256 << 20,
316
+ maxRecordBytes: 16 << 20, // one longer record is skipped and reported
317
+ maxEntries: 10_000,
314
318
  deadlineMs: 30_000,
315
319
  },
316
320
  signal: AbortSignal.timeout(60_000),
317
321
  });
318
322
 
319
323
  for (const entry of result.entries) {
320
- // { provider, sessionId, ordinal, timestamp, text, cwd, gitBranch?, model?, usage? }
324
+ // { provider, sessionId, ordinal, timestamp, text, cwd, gitBranch?, model?, usage?, metadataStatus }
325
+ // Upsert by (provider, sessionId, ordinal): a prompt comes back when its answer metadata changes.
321
326
  }
322
- saveCursor(result.cursor); // unchanged files are skipped next time; appended files resume
323
- if (result.boundsHit.length > 0) console.warn('partial pass', result.boundsHit, result.stats);
327
+ saveCursor(result.cursor); // sensitive local state; fully read unchanged files are skipped next time
328
+ if (result.hasMore) scheduleAnotherCall(); // a bound stopped the pass; the cursor resumes it
329
+ for (const exclusion of result.exclusions) console.warn('record left out', exclusion); // permanent
324
330
  ```
325
331
 
326
- - **Scope fails closed.** A prompt is returned only when its recorded working directory (the Claude line's `cwd`; the Codex `session_meta` / `turn_context` cwd) realpath-resolves to a root or somewhere inside one. Missing or unresolvable cwds are excluded and counted in `stats.promptsOutOfScope`; Codex scope never comes from the rollout's location. `cwd` is the only path in the output, and cursor keys hold no paths.
332
+ - **Scope fails closed.** A prompt is returned only when its recorded working directory (the Claude line's `cwd`; the Codex `session_meta` / `turn_context` cwd) realpath-resolves to a root or somewhere inside one. Missing or unresolvable cwds are excluded and counted in `stats.promptsOutOfScope`; Codex scope never comes from the rollout's location. `cwd` is the only path in entries. Cursor keys hold no paths, but cursor values can (Codex resume state keeps the session's cwd and branch), so store the cursor as sensitive, local-only state.
327
333
  - **Parse-only.** No redaction and no network I/O; text is untruncated.
328
334
  - **Ordinals are stable.** Every human prompt in a file consumes one ordinal, even when it is out of scope, older than `since`, or lacks a timestamp (such lines are skipped and counted, never given a synthesized time). Entries are ordered by provider, session, then ordinal.
329
- - **Model and usage** come from the first model call that answered the prompt (Claude: the first assistant message; Codex: the first `token_count` with the model from `turn_context`). `inputTokens` is uncached input. The last prompt of a still-running session can be returned before its answer exists and is not revisited.
330
- - **Bounds never throw.** Hitting `maxSessions`, `maxTotalBytes`, the deadline, or an abort stops the pass; a file over `maxFileBytes` is skipped and counted in `stats.filesOverSizeLimit`. An interrupted file contributes no entries, and the cursor covers only fully processed files, so the next call picks up where this one stopped.
335
+ - **Model and usage** come from the first model call that answered the prompt (Claude: the first non-synthetic assistant message, with split records of that call replacing rather than adding usage; Codex: the first `token_count` with the model from `turn_context`). Later tool-loop calls never replace it. `inputTokens` is uncached input, and missing usage stays `undefined`, never zero.
336
+ - **Late answers enrich the same prompt.** A prompt can be collected before its answer is written. `metadataStatus` is `pending` (no answering call yet), `provisional` (Claude call still being written, `stop_reason: null`), or `final`. While a file's last prompt is not final, the cursor keeps the byte offset and length of its line, its timestamp, and its answer state so far (first-answer message id, model, and usage, never prompt or answer text). The next call continues from where the file stopped, however far the answer is, and returns the prompt again with the same provider, sessionId, ordinal, timestamp, and text only when its model, usage, or status changed; the text comes from re-reading that one line. A prompt closed by the next prompt without an answer comes back once as `final` with no usage. Consumers should upsert by identity.
337
+ - **Large files are streamed and resumable.** Files are read in 1 MiB chunks and every pass stops after a complete line, so memory stays near one chunk plus one record whatever the file size. `maxFileBytes` is the most one file may read in one call: a file with more left keeps what it returned, records `backlog` in its cursor, is counted in `stats.filesOverSizeLimit`, and continues on the next call even if its size and mtime have not changed. `maxSessions`, `maxTotalBytes`, `maxEntries`, the deadline, and an abort stop the whole call the same way, and `cursor.next` makes the next call start with the file that was stopped, so a large or busy session cannot starve the others. The first file of a call always finishes its first record, so a record larger than the budget still moves forward. Bytes appended during a call and a half-written final line wait for the next call. `boundsHit` lists these resumable stops and `hasMore` says whether calling again would read more. No bound throws.
338
+ - **Permanent exclusions are reported.** A record over `maxRecordBytes` is skipped as it streams past without being buffered, even when it is still being written, and a complete line that is not JSON is skipped. Each is counted (`stats.recordsOverSizeLimit`, `stats.recordsMalformed`) and listed in `exclusions` with its byte offset. A prompt inside such a record is lost and does not consume an ordinal. A file that shrank or whose first 4 KiB changed is treated as rewritten: it is read again from the start with fresh ordinals and counted in `stats.sessionsRewritten`.
339
+ - **Cursor versions.** Output is `version: 3`. Cursors from 0.27.0 (`version: 1`) and 0.27.1 (`version: 2`) are accepted: each file is re-verified once, and a 0.27.1 pending prompt is replayed from its line in resumable passes, so it now settles even when its answer is far away. Files 0.27.1 skipped for size were never in its cursor and are read in full on the first call. Metadata that 0.27.1 already gave up on (a pending replay that exceeded `maxFileBytes`) was not saved and cannot be recovered, and a 0.27.0 cursor still cannot recover prompts it returned without metadata.
331
340
 
332
341
  For an already-open incremental Codex rollout, keep one `CodexRolloutParser` per stream and pass each parsed `SessionEvent[]` batch to `projectSessionTranscript()` (or append the events to an existing projection). This preserves parser state for paired tool calls, model context, and cumulative token deltas; consumers should not convert raw `response_item` rows themselves.
333
342
 
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { projectSessionTranscript } from './transcript';
11
11
  export type { TranscriptFidelity, TranscriptSourceProvenance, CanonicalTranscriptBlock, CanonicalTranscriptMessage, CanonicalToolCall, CanonicalTranscriptUsageTotals, CanonicalSessionTranscript, ProjectSessionTranscriptOptions, } from './transcript';
12
12
  export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
13
13
  export { collectPromptHistory } from './promptHistory';
14
- export type { CollectPromptHistoryOptions, PromptHistoryBound, PromptHistoryBounds, PromptHistoryCursor, PromptHistoryCursorSession, PromptHistoryEntry, PromptHistoryProvider, PromptHistoryResult, PromptHistoryResumeState, PromptHistoryStats, PromptHistoryUsage, } from './promptHistory';
14
+ export type { CollectPromptHistoryOptions, PromptHistoryAnswerState, PromptHistoryBound, PromptHistoryBounds, PromptHistoryCursor, PromptHistoryCursorSession, PromptHistoryEntry, PromptHistoryExclusion, PromptHistoryMetadataStatus, PromptHistoryPendingPrompt, PromptHistoryProvider, PromptHistoryResult, PromptHistoryResumeState, PromptHistoryStats, PromptHistoryUsage, } from './promptHistory';
15
15
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
16
16
  export type { HumanPromptProvider } from './humanPrompt';
17
17
  export { computeSessionFileStats, firstUserPrompt, providerContextSizeFn, readSessionFileStats, } from './sessionStats';
package/dist/node.d.ts CHANGED
@@ -11,7 +11,7 @@ export { loadObservedContextWindows, recordObservedContextWindow, getObservedCon
11
11
  export type { ObservedContextWindowOptions, ObservedContextWindowStore, } from './observedContextWindows';
12
12
  export { listRecentSessions, readSessionTranscript } from './sessionTranscripts';
13
13
  export { collectPromptHistory } from './promptHistory';
14
- export type { CollectPromptHistoryOptions, PromptHistoryBound, PromptHistoryBounds, PromptHistoryCursor, PromptHistoryCursorSession, PromptHistoryEntry, PromptHistoryProvider, PromptHistoryResult, PromptHistoryResumeState, PromptHistoryStats, PromptHistoryUsage, } from './promptHistory';
14
+ export type { CollectPromptHistoryOptions, PromptHistoryAnswerState, PromptHistoryBound, PromptHistoryBounds, PromptHistoryCursor, PromptHistoryCursorSession, PromptHistoryEntry, PromptHistoryExclusion, PromptHistoryMetadataStatus, PromptHistoryPendingPrompt, PromptHistoryProvider, PromptHistoryResult, PromptHistoryResumeState, PromptHistoryStats, PromptHistoryUsage, } from './promptHistory';
15
15
  export { HUMAN_CLAUDE_ENTRYPOINTS, HUMAN_CODEX_SOURCES, humanPromptText, isHumanPrompt, } from './humanPrompt';
16
16
  export type { HumanPromptProvider } from './humanPrompt';
17
17
  export { computeSessionFileStats, firstUserPrompt, providerContextSizeFn, readSessionFileStats, } from './sessionStats';
@@ -7,17 +7,38 @@
7
7
  * for Codex, its session/turn context) realpath-resolves to an allowed root
8
8
  * or somewhere inside one. Every bound stops cleanly and is reported in
9
9
  * `boundsHit`; nothing is silently truncated and no bound throws.
10
+ *
11
+ * Files are streamed in fixed-size chunks and every pass stops at a complete
12
+ * line, so a large or busy log is read across several calls: the cursor holds
13
+ * where each file stopped and the next call continues from there. A single
14
+ * record over `maxRecordBytes` is skipped without being buffered and reported
15
+ * in `exclusions`, the only way a record is permanently left out.
16
+ *
17
+ * A prompt can be written before its answer. The cursor then keeps the byte
18
+ * offset of that prompt's line and its answer-tracking state (never text), and
19
+ * the prompt is returned again under the same identity once its first
20
+ * answering call brings model or usage (`metadataStatus` says how settled).
10
21
  */
11
22
  import { type HumanPromptProvider } from './humanPrompt';
12
23
  export type PromptHistoryProvider = HumanPromptProvider;
13
- export type PromptHistoryBound = 'maxSessions' | 'maxFileBytes' | 'maxTotalBytes' | 'deadline' | 'aborted';
24
+ export type PromptHistoryBound = 'maxSessions' | 'maxFileBytes' | 'maxTotalBytes' | 'maxEntries' | 'deadline' | 'aborted';
14
25
  export interface PromptHistoryBounds {
15
26
  /** Session files read in one call (unchanged files do not count). Default 1000. */
16
27
  maxSessions?: number;
17
- /** Bytes a single file may need read in this pass. Default 16 MiB. */
28
+ /**
29
+ * Bytes read from one file in one call. Default 16 MiB. A file with more to
30
+ * read stops at a line boundary and continues on the next call.
31
+ */
18
32
  maxFileBytes?: number;
19
33
  /** Bytes read across all files in this call. Default 256 MiB. */
20
34
  maxTotalBytes?: number;
35
+ /**
36
+ * Largest single log record that is parsed. Default 16 MiB. A longer record
37
+ * is skipped without being buffered and reported in `exclusions`.
38
+ */
39
+ maxRecordBytes?: number;
40
+ /** Entries returned by one call. Default 10 000. */
41
+ maxEntries?: number;
21
42
  /** Wall-clock budget for the call. Default 30 000 ms. */
22
43
  deadlineMs?: number;
23
44
  }
@@ -31,6 +52,37 @@ export interface PromptHistoryResumeState {
31
52
  /** `session_meta` has been seen. */
32
53
  meta?: boolean;
33
54
  }
55
+ /** Answer metadata gathered so far for an open prompt; never text. */
56
+ export interface PromptHistoryAnswerState {
57
+ status: 'pending' | 'provisional';
58
+ /** Claude message id of the first answering call, once one has been seen. */
59
+ id?: string;
60
+ model?: string;
61
+ usage?: PromptHistoryUsage;
62
+ }
63
+ /**
64
+ * The last emitted prompt of a file whose answering call is not final yet.
65
+ * Holds a position and answer metadata, never prompt or answer text.
66
+ */
67
+ export interface PromptHistoryPendingPrompt {
68
+ ordinal: number;
69
+ /** Byte offset where the prompt's log line starts. */
70
+ offset: number;
71
+ /** Byte length of the prompt's log line, without its newline. */
72
+ lineBytes?: number;
73
+ /** Timestamp of the prompt line, used to verify it when it is read again. */
74
+ timestamp?: string;
75
+ /** Digest of the model, usage, and status last returned for this prompt. */
76
+ emitted: string;
77
+ /** Codex parse state as it was just before the prompt line. */
78
+ state?: PromptHistoryResumeState;
79
+ /**
80
+ * Answer tracking to continue from the session offset. Absent only on a
81
+ * session carried unchanged from a version 2 cursor, which replays from
82
+ * `offset` the next time it is read.
83
+ */
84
+ answer?: PromptHistoryAnswerState;
85
+ }
34
86
  export interface PromptHistoryCursorSession {
35
87
  provider: PromptHistoryProvider;
36
88
  sessionId: string;
@@ -38,15 +90,40 @@ export interface PromptHistoryCursorSession {
38
90
  mtimeMs: number;
39
91
  /** Ordinal of the last human prompt seen in this file, or -1 when none. */
40
92
  lastOrdinal: number;
41
- /** Byte offset just past the last complete line processed. */
93
+ /** Byte offset where the next pass starts reading. */
42
94
  offset: number;
43
95
  state?: PromptHistoryResumeState;
96
+ pending?: PromptHistoryPendingPrompt;
97
+ /** A bound stopped the last pass before the end of the file. */
98
+ backlog?: true;
99
+ /** `offset` is inside a record over `maxRecordBytes`; skip to its newline. */
100
+ skipping?: true;
101
+ /** Digest of the file's first bytes, used to detect a rewritten file. */
102
+ head?: {
103
+ bytes: number;
104
+ hash: string;
105
+ };
44
106
  }
45
- /** JSON-serializable resume point. Keys are opaque; they never contain paths. */
107
+ /**
108
+ * JSON-serializable resume point. Keys are opaque and never contain paths, but
109
+ * values can (Codex `state.cwd`), so treat the cursor as sensitive local state.
110
+ * Version 1 and 2 cursors are accepted as input; output is always version 3.
111
+ */
46
112
  export interface PromptHistoryCursor {
47
- version: 1;
113
+ version: 3;
48
114
  sessions: Record<string, PromptHistoryCursorSession>;
115
+ /** Opaque key of the file the next call starts with, after a call-wide stop. */
116
+ next?: string;
49
117
  }
118
+ /**
119
+ * How settled an entry's `model` / `usage` are:
120
+ * - `pending`: no answering call seen yet.
121
+ * - `provisional`: the first answering call is still being written (Claude
122
+ * `stop_reason: null`); values may change.
123
+ * - `final`: the first answering call is complete, or the prompt was closed
124
+ * without one. Missing usage is then unknown, not zero.
125
+ */
126
+ export type PromptHistoryMetadataStatus = 'pending' | 'provisional' | 'final';
50
127
  export interface PromptHistoryUsage {
51
128
  /** Uncached input tokens. */
52
129
  inputTokens?: number;
@@ -70,15 +147,42 @@ export interface PromptHistoryEntry {
70
147
  model?: string;
71
148
  /** Usage of that first answering model call, when known. */
72
149
  usage?: PromptHistoryUsage;
150
+ /**
151
+ * Whether `model` / `usage` can still change. A prompt that is not `final`
152
+ * is returned again, with the same provider, sessionId, ordinal, timestamp,
153
+ * and text, by a later call once its metadata or status changes.
154
+ */
155
+ metadataStatus: PromptHistoryMetadataStatus;
156
+ }
157
+ /**
158
+ * A log record permanently left out. Records over `maxRecordBytes` and lines
159
+ * that are not valid JSON are never parsed, so any prompt inside is lost, and
160
+ * the ordinals of later prompts in that file do not count it.
161
+ */
162
+ export interface PromptHistoryExclusion {
163
+ provider: PromptHistoryProvider;
164
+ sessionId: string;
165
+ reason: 'oversizedRecord' | 'malformedRecord';
166
+ /** Byte offset where the record starts. */
167
+ offset: number;
73
168
  }
74
169
  export interface PromptHistoryStats {
75
170
  sessionsScanned: number;
76
171
  sessionsSkippedUnchanged: number;
77
172
  /** Scanned sessions with human prompts but none inside the allowed roots. */
78
173
  sessionsOutOfScope: number;
174
+ /** Files whose `maxFileBytes` budget ran out; they continue on the next call. */
79
175
  filesOverSizeLimit: number;
176
+ /** Scanned sessions left with unread bytes by any bound; they continue next call. */
177
+ sessionsWithBacklog: number;
178
+ /** Sessions whose file was truncated or rewritten and was read again from the start. */
179
+ sessionsRewritten: number;
80
180
  /** Codex sessions skipped as non-interactive (subagent, fork, unknown source). */
81
181
  sessionsNotInteractive: number;
182
+ /** Records skipped for exceeding `maxRecordBytes` (see `exclusions`). */
183
+ recordsOverSizeLimit: number;
184
+ /** Complete lines that were not valid JSON (see `exclusions`). */
185
+ recordsMalformed: number;
82
186
  /** Human prompts dropped because the log line had no usable timestamp. */
83
187
  promptsMissingTimestamp: number;
84
188
  /** Human prompts dropped because their cwd was missing or outside the roots. */
@@ -102,7 +206,16 @@ export interface PromptHistoryResult {
102
206
  entries: PromptHistoryEntry[];
103
207
  cursor: PromptHistoryCursor;
104
208
  stats: PromptHistoryStats;
209
+ /** Resumable stops: calling again with the returned cursor continues past them. */
105
210
  boundsHit: PromptHistoryBound[];
211
+ /** Permanent exclusions found in this call (at most 1000; stats count all). */
212
+ exclusions: PromptHistoryExclusion[];
213
+ /** A bound left bytes or files unread; call again with the cursor to continue. */
214
+ hasMore: boolean;
106
215
  }
216
+ /** Test hooks; not part of the public entry points. */
217
+ export declare const __promptHistoryTesting: {
218
+ setReadChunkBytes(bytes: number | null): void;
219
+ };
107
220
  /** Collect human-typed prompts from Claude Code and Codex logs under the given roots. */
108
221
  export declare function collectPromptHistory(options: CollectPromptHistoryOptions): Promise<PromptHistoryResult>;