@lucascouts/claude-agent-tui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +191 -0
  2. package/NOTICE +14 -0
  3. package/README.md +50 -0
  4. package/dist/acp-agent.d.ts +594 -0
  5. package/dist/acp-agent.d.ts.map +1 -0
  6. package/dist/acp-agent.js +2139 -0
  7. package/dist/ansi-mirror.d.ts +42 -0
  8. package/dist/ansi-mirror.d.ts.map +1 -0
  9. package/dist/ansi-mirror.js +61 -0
  10. package/dist/besteffort.d.ts +44 -0
  11. package/dist/besteffort.d.ts.map +1 -0
  12. package/dist/besteffort.js +100 -0
  13. package/dist/billing/entrypoint-guard.d.ts +97 -0
  14. package/dist/billing/entrypoint-guard.d.ts.map +1 -0
  15. package/dist/billing/entrypoint-guard.js +166 -0
  16. package/dist/claude-path.d.ts +12 -0
  17. package/dist/claude-path.d.ts.map +1 -0
  18. package/dist/claude-path.js +61 -0
  19. package/dist/diff-enriched-reader.d.ts +41 -0
  20. package/dist/diff-enriched-reader.d.ts.map +1 -0
  21. package/dist/diff-enriched-reader.js +106 -0
  22. package/dist/diff-source.d.ts +104 -0
  23. package/dist/diff-source.d.ts.map +1 -0
  24. package/dist/diff-source.js +164 -0
  25. package/dist/end-of-turn.d.ts +172 -0
  26. package/dist/end-of-turn.d.ts.map +1 -0
  27. package/dist/end-of-turn.js +415 -0
  28. package/dist/engine-lifecycle.d.ts +222 -0
  29. package/dist/engine-lifecycle.d.ts.map +1 -0
  30. package/dist/engine-lifecycle.js +236 -0
  31. package/dist/engine-pty.d.ts +143 -0
  32. package/dist/engine-pty.d.ts.map +1 -0
  33. package/dist/engine-pty.js +222 -0
  34. package/dist/engine-watcher.d.ts +83 -0
  35. package/dist/engine-watcher.d.ts.map +1 -0
  36. package/dist/engine-watcher.js +173 -0
  37. package/dist/engine.d.ts +30 -0
  38. package/dist/engine.d.ts.map +1 -0
  39. package/dist/engine.js +34 -0
  40. package/dist/event-switch.d.ts +164 -0
  41. package/dist/event-switch.d.ts.map +1 -0
  42. package/dist/event-switch.js +206 -0
  43. package/dist/gate/port.d.ts +38 -0
  44. package/dist/gate/port.d.ts.map +1 -0
  45. package/dist/gate/port.js +126 -0
  46. package/dist/gate/settings-writer.d.ts +130 -0
  47. package/dist/gate/settings-writer.d.ts.map +1 -0
  48. package/dist/gate/settings-writer.js +349 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +106 -0
  52. package/dist/jsonl.d.ts +267 -0
  53. package/dist/jsonl.d.ts.map +1 -0
  54. package/dist/jsonl.js +527 -0
  55. package/dist/lib.d.ts +6 -0
  56. package/dist/lib.d.ts.map +1 -0
  57. package/dist/lib.js +5 -0
  58. package/dist/linearize.d.ts +219 -0
  59. package/dist/linearize.d.ts.map +1 -0
  60. package/dist/linearize.js +444 -0
  61. package/dist/live-diff-env.d.ts +7 -0
  62. package/dist/live-diff-env.d.ts.map +1 -0
  63. package/dist/live-diff-env.js +18 -0
  64. package/dist/live-subagent-env.d.ts +7 -0
  65. package/dist/live-subagent-env.d.ts.map +1 -0
  66. package/dist/live-subagent-env.js +19 -0
  67. package/dist/permissions/allow-inject.d.ts +67 -0
  68. package/dist/permissions/allow-inject.d.ts.map +1 -0
  69. package/dist/permissions/allow-inject.js +85 -0
  70. package/dist/permissions/deny.d.ts +60 -0
  71. package/dist/permissions/deny.d.ts.map +1 -0
  72. package/dist/permissions/deny.js +81 -0
  73. package/dist/permissions/gate-wiring.d.ts +112 -0
  74. package/dist/permissions/gate-wiring.d.ts.map +1 -0
  75. package/dist/permissions/gate-wiring.js +350 -0
  76. package/dist/permissions/hook-server.d.ts +72 -0
  77. package/dist/permissions/hook-server.d.ts.map +1 -0
  78. package/dist/permissions/hook-server.js +179 -0
  79. package/dist/permissions/permission-mode.d.ts +67 -0
  80. package/dist/permissions/permission-mode.d.ts.map +1 -0
  81. package/dist/permissions/permission-mode.js +100 -0
  82. package/dist/permissions/request-permission.d.ts +102 -0
  83. package/dist/permissions/request-permission.d.ts.map +1 -0
  84. package/dist/permissions/request-permission.js +124 -0
  85. package/dist/settings.d.ts +68 -0
  86. package/dist/settings.d.ts.map +1 -0
  87. package/dist/settings.js +182 -0
  88. package/dist/stop-reason-map.d.ts +17 -0
  89. package/dist/stop-reason-map.d.ts.map +1 -0
  90. package/dist/stop-reason-map.js +33 -0
  91. package/dist/subagent-source.d.ts +63 -0
  92. package/dist/subagent-source.d.ts.map +1 -0
  93. package/dist/subagent-source.js +132 -0
  94. package/dist/subagent-watcher.d.ts +40 -0
  95. package/dist/subagent-watcher.d.ts.map +1 -0
  96. package/dist/subagent-watcher.js +108 -0
  97. package/dist/tools.d.ts +119 -0
  98. package/dist/tools.d.ts.map +1 -0
  99. package/dist/tools.js +729 -0
  100. package/dist/usage-env.d.ts +7 -0
  101. package/dist/usage-env.d.ts.map +1 -0
  102. package/dist/usage-env.js +16 -0
  103. package/dist/usage.d.ts +54 -0
  104. package/dist/usage.d.ts.map +1 -0
  105. package/dist/usage.js +53 -0
  106. package/dist/utils.d.ts +16 -0
  107. package/dist/utils.d.ts.map +1 -0
  108. package/dist/utils.js +83 -0
  109. package/dist/zed-register.d.ts +26 -0
  110. package/dist/zed-register.d.ts.map +1 -0
  111. package/dist/zed-register.js +106 -0
  112. package/package.json +79 -0
@@ -0,0 +1,219 @@
1
+ import type { SessionMessage, GetMessages } from "./engine-watcher.js";
2
+ /**
3
+ * One ordered turn in the linearized stream the §7 translator consumes.
4
+ *
5
+ * `role` is typed as `string` (NOT the design's `'user' | 'assistant'`) — a DOCUMENTED DEVIATION:
6
+ * the linearizer must forward-compat unknown `.type` values without breaking the order (story.md
7
+ * constraint; story 016 tolerant switch), so it echoes `.type` verbatim and leaves an unknown type
8
+ * for the downstream tolerant switch to ignore. `parentUuid` is present only when a raw-shaped
9
+ * fixture / the R1.3 fallback carries it; it is `null` for the reduced REUSE-live shape.
10
+ */
11
+ export interface Turn {
12
+ /** Stable, append-only, uuid-anchored order key (Task 3.1). Sortable by chain position. */
13
+ orderKey: string;
14
+ /** Message uuid (identity; the dedupe key upstream). Empty string if the input lacks one. */
15
+ uuid: string;
16
+ /** Parent uuid when present (raw fixture / R1.3 fallback); `null` in the reduced live shape. */
17
+ parentUuid: string | null;
18
+ /** Message role — `.type` echoed verbatim (`'user'`/`'assistant'`/unknown), not enumerated. */
19
+ role: string;
20
+ /** The source message, passed through UNTOUCHED to the §7 translator. */
21
+ message: SessionMessage;
22
+ /**
23
+ * Set when THIS turn is itself a sidechain row that appears in the top level — under `inline`, or
24
+ * as an orphan fallback under `nested` (Task 2.3). Points up to its (claimed) spawning Task id.
25
+ */
26
+ sidechain?: {
27
+ parentToolUseId: string;
28
+ };
29
+ /**
30
+ * Under the `nested` policy (Task 2.1): the sidechain (subagent) rows spawned by a `tool_use` in
31
+ * THIS turn, attached so the translator renders them under the spawning `tool_call`. Absent when
32
+ * the turn spawned no resolved sidechain. (Reinterprets the design's ambiguous `sidechain` field
33
+ * into a concrete parent→children attachment — see .draft/deviations.yaml.)
34
+ */
35
+ nested?: SessionMessage[];
36
+ }
37
+ /** The sidechain flattening policy (Task 2). `nested` is the default (design Key Decision 2). */
38
+ export type SidechainPolicy = "nested" | "hidden" | "inline";
39
+ /** Options for {@link linearizeTurns}. Extends the policy options with the orphan-drift sink. */
40
+ export interface LinearizeOptions extends ApplyPolicyOptions {
41
+ /** Sidechain flattening policy (Task 2); defaults to `'nested'`. */
42
+ sidechainPolicy?: SidechainPolicy;
43
+ }
44
+ /**
45
+ * The stable, uuid-anchored, append-only order key (Task 3.1, R3.1/R3.2).
46
+ *
47
+ * RECONCILED signature: the design specifies `chainPositionKey(uuid, parentUuid)`, but `parentUuid`
48
+ * is absent in the reduced REUSE-live shape, so the key is derived from the monotonic chain POSITION
49
+ * (array index) anchored to `uuid` — `pad(index):uuid`. The E5 monotonic-ordered-superset property
50
+ * guarantees a prefix re-parse keeps each position, so a turn at order k keeps key k after appends;
51
+ * the leading zero-padded index makes keys sort by chain position, and the `:uuid` suffix anchors
52
+ * identity (two turns can never collide). parentUuid-derived keying is reserved for the R1.3 fallback
53
+ * path (where parentUuid actually exists). See .draft/deviations.yaml.
54
+ *
55
+ * @param uuid the turn's message uuid (identity anchor).
56
+ * @param index the turn's monotonic position in the top-level stream.
57
+ * @returns the sortable, stable, append-only order key.
58
+ */
59
+ export declare function chainPositionKey(uuid: string, index: number): string;
60
+ /** A drift record for an orphan sidechain whose `parent_tool_use_id` does not resolve (Task 2.3). */
61
+ export interface SidechainDriftRecord {
62
+ kind: "orphan-sidechain";
63
+ /** The unresolved parent tool-use id (or `null` when the sidechain carried none). */
64
+ parentToolUseId: string | null;
65
+ /** The orphan sidechain's uuid. */
66
+ uuid: string;
67
+ }
68
+ /** Options for {@link applySidechainPolicy} (and forwarded by {@link linearizeTurns}). */
69
+ export interface ApplyPolicyOptions {
70
+ /** Injectable orphan-drift sink; defaults to {@link defaultSidechainDrift} (stderr). */
71
+ onDrift?: (record: SidechainDriftRecord) => void;
72
+ }
73
+ /** The structured result of {@link applySidechainPolicy}. */
74
+ export interface AppliedPolicy {
75
+ /** The top-level message stream (main turns; plus inline sidechains under `inline`/orphan). */
76
+ topLevel: SessionMessage[];
77
+ /** `nested` policy only: spawning `tool_use` id → its sidechain rows. */
78
+ nestedByToolUseId?: Map<string, SessionMessage[]>;
79
+ }
80
+ /**
81
+ * Apply the sidechain flattening policy (R2), encapsulated so it is one decision, not scattered
82
+ * branching. Partitions out `isMeta`, then:
83
+ * - `nested` (default, R2.2): resolved sidechains group under their spawning `tool_use` id in
84
+ * `nestedByToolUseId` and are kept OUT of `topLevel` (top-level count = main-chain count). An
85
+ * ORPHAN sidechain whose `parent_tool_use_id` does not resolve to a main-stream tool_use is
86
+ * placed INLINE in chronological position (never dropped) and logged as drift once (R2.3).
87
+ * - `hidden` (R2.3): sidechains dropped; `topLevel` = main, no `nestedByToolUseId`.
88
+ * - `inline` (R2.3): sidechains merged in chronological position WITHOUT reordering any main turn
89
+ * (`topLevel` = all non-meta rows in input order; main is a subsequence).
90
+ *
91
+ * @param messages the SDK-ordered message sequence.
92
+ * @param policy the flattening policy.
93
+ * @param opts injectable orphan-drift sink.
94
+ * @returns the {@link AppliedPolicy}.
95
+ */
96
+ export declare function applySidechainPolicy(messages: SessionMessage[], policy: SidechainPolicy, opts?: ApplyPolicyOptions): AppliedPolicy;
97
+ /**
98
+ * Linearize the SDK's `SessionMessage[]` into an ordered top-level `Turn[]` for the §7 translator:
99
+ * adopt the SDK order (R1.1), apply the sidechain flattening policy (R2, default `nested`), and
100
+ * assign each turn a stable, uuid-anchored order key (R3.1/R3.2).
101
+ *
102
+ * Under `nested`, each main turn that spawned resolved sidechains carries them on `Turn.nested`
103
+ * (attached by its `tool_use` ids); a top-level sidechain (inline policy, or orphan) carries
104
+ * `Turn.sidechain = { parentToolUseId }`. The main-turn subsequence is identical across all three
105
+ * policies (R2.3) — only sidechain placement differs.
106
+ *
107
+ * Pure and synchronous: no I/O, no SDK call (the caller supplies `messages`).
108
+ *
109
+ * @param messages the SDK-ordered message sequence (from {@link readOrderedMessages}).
110
+ * @param opts flattening-policy + orphan-drift options (`nested` default).
111
+ * @returns the ordered top-level `Turn[]`.
112
+ */
113
+ export declare function linearizeTurns(messages: SessionMessage[], opts?: LinearizeOptions): Turn[];
114
+ /**
115
+ * Thin SOURCE wrapper (Task 1.1, R1.1): call the injected {@link GetMessages} seam (the SDK's pure,
116
+ * billing-free `getSessionMessages`) and return its `SessionMessage[]` in the SAME chronological
117
+ * order the SDK produced. Performs NO reordering and NO file I/O of its own — the SDK is the single
118
+ * parser (E5 REUSE-live). The seam may be sync or async; we await either way.
119
+ *
120
+ * On an SDK throw (binary/version drift, R1.3 trigger condition) the error is SURFACED with the
121
+ * resolved `sessionId`/`dir` rather than silently swallowed into `[]`, so a drift is loud and
122
+ * diagnosable (the caller decides whether to engage the R1.3 fallback).
123
+ *
124
+ * @param sessionId the session id; equals the transcript filename basename.
125
+ * @param dir the runtime cwd (story 015 `readCwdFromInside`) passed as `{ dir }`; may be undefined.
126
+ * @param opts injectable `getMessages` seam (defaults to the SDK reader via dynamic import).
127
+ * @returns the SDK's ordered `SessionMessage[]` (the chronological main chain).
128
+ */
129
+ export declare function readOrderedMessages(sessionId: string, dir: string | undefined, opts?: {
130
+ getMessages?: GetMessages;
131
+ }): Promise<SessionMessage[]>;
132
+ /**
133
+ * Default {@link GetMessages}: the SDK's pure, billing-free `getSessionMessages`. Imported lazily
134
+ * (dynamic import) so this module — and the deterministic unit tests, which always inject a stub —
135
+ * never force-loads the SDK at module-eval time. Mirrors engine-watcher.ts `defaultGetMessages`.
136
+ *
137
+ * Exported (story 043 R2.1) so acp-agent.ts can reuse it as the PRODUCTION reduced base it wraps in
138
+ * the diff-enriched reader when `liveDiff` is ON (it is the same reduced reader `readOrderedMessages`
139
+ * already falls back to). linearize.ts is a fork module imported directly from ../dist/ — NOT via
140
+ * lib.ts, whose public surface is frozen to upstream v0.39.0 — so this export is safe.
141
+ */
142
+ export declare function defaultGetMessages(sessionId: string, opts?: {
143
+ dir?: string;
144
+ }): Promise<SessionMessage[]>;
145
+ /** Options for {@link buildChainEquivalent}. The fallback is OFF unless `enabled` is `true`. */
146
+ export interface FallbackOptions {
147
+ /** Gate: the contingency mirror runs ONLY when this is explicitly `true`. Default OFF. */
148
+ enabled?: boolean;
149
+ }
150
+ /**
151
+ * Contingency mirror of getSessionMessages over RAW JSONL lines (R1.3) — GATED OFF by default.
152
+ *
153
+ * Reproduces the SDK's linearization behaviorally: parse each line (corrupt lines skipped), keep
154
+ * only main-chain user/assistant rows (drop `isMeta`/`isSidechain`), then follow the parentUuid
155
+ * chain BACKWARD from the latest main row (the tip) to the root and reverse — so abandoned fork
156
+ * branches (not on the tip's ancestry) are excluded (R4.1) and the surviving chain is returned in
157
+ * chronological order. Projects each surviving record into the reduced SDK shape.
158
+ *
159
+ * STUB LIMITATION (documented, contingency-only): the tip is taken as the LAST main row in file
160
+ * order, assuming — as getSessionMessages resolves — that the surviving chain ends at the latest
161
+ * record. A more elaborate anchor-selection is out of scope for a gated-OFF mirror.
162
+ *
163
+ * @param rawLines the transcript's raw JSONL lines (full universal-field shape).
164
+ * @param opts the gate; the mirror runs ONLY with `{ enabled: true }`.
165
+ * @returns the surviving main chain as reduced-shape `SessionMessage[]`, in chronological order.
166
+ */
167
+ export declare function buildChainEquivalent(rawLines: string[], opts?: FallbackOptions): SessionMessage[];
168
+ /** Options for {@link readOrderedTurns}: the SDK seam plus the linearizer's policy/drift options. */
169
+ export interface ReadOrderedTurnsOptions extends LinearizeOptions {
170
+ /** Injectable `getSessionMessages` seam (defaults to the SDK reader). */
171
+ getMessages?: GetMessages;
172
+ }
173
+ /**
174
+ * The SINGLE seam both the live re-parse path and the `session/load` replay path call (R3.3):
175
+ * `readOrderedMessages` (adopt the SDK's ordered main chain) + `linearizeTurns` (partition, apply
176
+ * the sidechain policy, assign stable order keys). Because both paths funnel through this one
177
+ * function, live and replay CANNOT diverge in ordering — the guarantee holds by construction.
178
+ *
179
+ * The live read path streams mid-turn content on the story-015 debounced-write signal; this
180
+ * stable (re-)ordering re-runs on the story-024 end-of-turn signal (see {@link bindEndOfTurnReparse},
181
+ * Task 3.3). `replaySessionHistory` (story 011/023) calls it once for `session/load`.
182
+ *
183
+ * @param sessionId the session id; equals the transcript filename basename.
184
+ * @param dir the runtime cwd (story 015) passed to `getSessionMessages`; may be undefined.
185
+ * @param opts the SDK seam + sidechain policy / orphan-drift options.
186
+ * @returns the ordered `Turn[]` (identical whether requested via the live or replay seam).
187
+ */
188
+ export declare function readOrderedTurns(sessionId: string, dir: string | undefined, opts?: ReadOrderedTurnsOptions): Promise<Turn[]>;
189
+ /** The turn watchdog window (ms) — DEGRAU-0 decision 2. DISTINCT from the 2000ms file-discovery one. */
190
+ export declare const TURN_WATCHDOG_MS = 5583;
191
+ /** Options for {@link bindEndOfTurnReparse}. */
192
+ export interface BindEndOfTurnOptions {
193
+ /** Re-parse callback (re-invoke readOrderedTurns). Called once per end-of-turn / watchdog. */
194
+ reparse: () => void | Promise<void>;
195
+ /** Turn-watchdog window (ms); defaults to {@link TURN_WATCHDOG_MS}. */
196
+ watchdogMs?: number;
197
+ /** Injectable timer seam for deterministic tests; defaults to {@link defaultSchedule}. */
198
+ schedule?: (fn: () => void, ms: number) => () => void;
199
+ }
200
+ /** Handle returned by {@link bindEndOfTurnReparse}. */
201
+ export interface EndOfTurnBinding {
202
+ /** Call on the story-024 end-of-turn signal: re-parse once and re-arm the watchdog. */
203
+ notifyEndOfTurn(): void;
204
+ /** Detach: cancel the watchdog and suppress further re-parses. Idempotent. */
205
+ stop(): void;
206
+ }
207
+ /**
208
+ * Subscribe a re-parse to the end-of-turn cadence (R3.1): re-invoke `reparse` exactly once per
209
+ * detected end-of-turn (the story-024 detector already debounces on terminal stop_reason + Δt=200ms,
210
+ * so we do NOT re-debounce). A turn watchdog (default {@link TURN_WATCHDOG_MS}) re-parses once as a
211
+ * safety net if no end-of-turn signal arrives; a real signal cancels the pending watchdog so a turn
212
+ * never triggers two re-parses. Because the re-parse re-runs `linearizeTurns` over the full monotonic
213
+ * superset, prior orderKeys are reused (R3.1).
214
+ *
215
+ * @param opts the reparse callback + watchdog/timer seams.
216
+ * @returns an {@link EndOfTurnBinding} (`notifyEndOfTurn` + `stop`).
217
+ */
218
+ export declare function bindEndOfTurnReparse(opts: BindEndOfTurnOptions): EndOfTurnBinding;
219
+ //# sourceMappingURL=linearize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"linearize.d.ts","sourceRoot":"","sources":["../src/linearize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,444 @@
1
+ // === §7 Tree linearization: SessionMessage[] → ordered Turn[] (story 017) ====================
2
+ //
3
+ // The fork's read path turns a <sessionId>.jsonl transcript into ACP session/update notifications.
4
+ // Between "read the file" (story 015 watcher + getSessionMessages) and "translate each message"
5
+ // (stories 018-021) sits THIS module: it adopts the SDK's already-ordered, already-filtered main
6
+ // chain and exposes it as an ordered Turn[] the translator consumes.
7
+ //
8
+ // BINDING DECISION E5 "REUSE-live": we do NOT hand-roll a parentUuid tree walker. The SDK's
9
+ // getSessionMessages already parses the transcript, builds the conversation chain via parentUuid
10
+ // links INTERNALLY, resolves forks/re-prompts/compact anchors, filters isSidechain/isMeta, and
11
+ // returns ONLY the main-chain user/assistant messages in chronological order (sdk.d.ts ~L715; E5
12
+ // DECISION-E5.md: "0 linearization divergences", "returning 4 main-chain messages").
13
+ //
14
+ // RECONCILIATION (orchestrator decision, Option 1 — user-confirmed) ============================
15
+ // The design.md was written against an ASSUMED SessionMessage carrying parentUuid/isSidechain/cwd/
16
+ // timestamp. The REAL, empirically-verified shape (story 015, jsonl.ts:467-485; sdk.d.ts:3837) is
17
+ // REDUCED: { type: 'user'|'assistant'|'system', uuid, session_id, message, parent_tool_use_id }.
18
+ // So this module is reconciled to the real shape:
19
+ // - Order (R1.1): adopt the returned ARRAY order verbatim — it is already chronological.
20
+ // - isSidechain/isMeta filtering (R1.2): ALREADY done by the SDK; the live input is main-chain
21
+ // only. The partition in linearizeTurns therefore reads `isSidechain` ONLY if a raw-shaped
22
+ // fixture/fallback carries it, ELSE infers a sidechain from a non-null `parent_tool_use_id`
23
+ // (the only sidechain signal present in the reduced live shape).
24
+ // - Order key (R3): uuid-anchored + monotonic position (NOT parentUuid, which is absent live).
25
+ // Justified by the E5 monotonic-ordered-superset property: a prefix re-parse keeps every prior
26
+ // position, so the key is stable and append-only.
27
+ // - The full parentUuid/isSidechain TREE logic genuinely belongs to the R1.3 fallback
28
+ // (buildChainEquivalent over RAW JSONL lines), where those fields actually exist.
29
+ // Each deviation is recorded in LINEARIZATION.md (Task 4.5) and .draft/deviations.yaml.
30
+ /** Narrowing helper: a non-null plain object (so we can read string-keyed props safely). */
31
+ function isObject(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
33
+ }
34
+ /**
35
+ * The sidechain's spawning Task `tool_use.id`, read from `parent_tool_use_id` (SDK snake_case) or
36
+ * `parentToolUseId` (story-015 typed name) — whichever is a non-empty string. `undefined` when the
37
+ * message has neither (a main-chain row, or an orphan whose parent is absent — Task 2.3).
38
+ */
39
+ function parentToolUseIdOf(m) {
40
+ const rec = m;
41
+ const v = rec.parent_tool_use_id ?? rec.parentToolUseId;
42
+ return typeof v === "string" && v.length > 0 ? v : undefined;
43
+ }
44
+ /**
45
+ * Whether a message is a sidechain (subagent) row. RECONCILED to the real SDK shape: there is no
46
+ * `isSidechain` field in the reduced live output, so we read `isSidechain` ONLY when a raw-shaped
47
+ * fixture / the R1.3 fallback supplies it, and otherwise infer a sidechain from a non-null
48
+ * `parent_tool_use_id` (the only sidechain signal in the reduced shape).
49
+ */
50
+ function isSidechainMsg(m) {
51
+ const rec = m;
52
+ if (typeof rec.isSidechain === "boolean")
53
+ return rec.isSidechain;
54
+ return parentToolUseIdOf(m) !== undefined;
55
+ }
56
+ /**
57
+ * Whether a message is a non-conversational anchor/meta row that must NEVER be a top-level turn:
58
+ * a raw `isMeta` flag, OR a `summary` (post-`/compact` continuity anchor, R4.3), OR a `system` row.
59
+ * getSessionMessages already excludes these on the live path (system/summary not returned by
60
+ * default); honouring them here keeps a raw fixture / `includeSystemMessages` input consistent and
61
+ * the linear order continuous across a compaction boundary.
62
+ */
63
+ function isMetaMsg(m) {
64
+ const rec = m;
65
+ if (rec.isMeta === true)
66
+ return true;
67
+ return m.type === "summary" || m.type === "system";
68
+ }
69
+ /** Parent uuid when carried by the input (raw fixture / fallback); `null` otherwise. */
70
+ function parentUuidOf(m) {
71
+ const rec = m;
72
+ return typeof rec.parentUuid === "string" ? rec.parentUuid : null;
73
+ }
74
+ /** The message uuid, or `''` when absent (mirrors story-015 projectEvent tolerance). */
75
+ function uuidOf(m) {
76
+ return typeof m.uuid === "string" ? m.uuid : "";
77
+ }
78
+ /**
79
+ * The stable, uuid-anchored, append-only order key (Task 3.1, R3.1/R3.2).
80
+ *
81
+ * RECONCILED signature: the design specifies `chainPositionKey(uuid, parentUuid)`, but `parentUuid`
82
+ * is absent in the reduced REUSE-live shape, so the key is derived from the monotonic chain POSITION
83
+ * (array index) anchored to `uuid` — `pad(index):uuid`. The E5 monotonic-ordered-superset property
84
+ * guarantees a prefix re-parse keeps each position, so a turn at order k keeps key k after appends;
85
+ * the leading zero-padded index makes keys sort by chain position, and the `:uuid` suffix anchors
86
+ * identity (two turns can never collide). parentUuid-derived keying is reserved for the R1.3 fallback
87
+ * path (where parentUuid actually exists). See .draft/deviations.yaml.
88
+ *
89
+ * @param uuid the turn's message uuid (identity anchor).
90
+ * @param index the turn's monotonic position in the top-level stream.
91
+ * @returns the sortable, stable, append-only order key.
92
+ */
93
+ export function chainPositionKey(uuid, index) {
94
+ return `${String(index).padStart(8, "0")}:${uuid}`;
95
+ }
96
+ /** The `tool_use` block ids inside a message's `message.content` array (the spawning Task ids). */
97
+ function toolUseIdsOf(m) {
98
+ const message = m.message;
99
+ if (!isObject(message))
100
+ return [];
101
+ const content = message.content;
102
+ if (!Array.isArray(content))
103
+ return [];
104
+ const ids = [];
105
+ for (const block of content) {
106
+ if (isObject(block) && block.type === "tool_use" && typeof block.id === "string") {
107
+ ids.push(block.id);
108
+ }
109
+ }
110
+ return ids;
111
+ }
112
+ /** All `tool_use` ids present across the main stream — the set a sidechain must resolve against. */
113
+ function collectToolUseIds(mainMessages) {
114
+ const ids = new Set();
115
+ for (const m of mainMessages) {
116
+ for (const id of toolUseIdsOf(m))
117
+ ids.add(id);
118
+ }
119
+ return ids;
120
+ }
121
+ /** Default orphan-drift sink: log to STDERR (never stdout — the ACP protocol channel). */
122
+ function defaultSidechainDrift(record) {
123
+ console.error(`[lin-drift] ${JSON.stringify(record)}`);
124
+ }
125
+ /**
126
+ * Apply the sidechain flattening policy (R2), encapsulated so it is one decision, not scattered
127
+ * branching. Partitions out `isMeta`, then:
128
+ * - `nested` (default, R2.2): resolved sidechains group under their spawning `tool_use` id in
129
+ * `nestedByToolUseId` and are kept OUT of `topLevel` (top-level count = main-chain count). An
130
+ * ORPHAN sidechain whose `parent_tool_use_id` does not resolve to a main-stream tool_use is
131
+ * placed INLINE in chronological position (never dropped) and logged as drift once (R2.3).
132
+ * - `hidden` (R2.3): sidechains dropped; `topLevel` = main, no `nestedByToolUseId`.
133
+ * - `inline` (R2.3): sidechains merged in chronological position WITHOUT reordering any main turn
134
+ * (`topLevel` = all non-meta rows in input order; main is a subsequence).
135
+ *
136
+ * @param messages the SDK-ordered message sequence.
137
+ * @param policy the flattening policy.
138
+ * @param opts injectable orphan-drift sink.
139
+ * @returns the {@link AppliedPolicy}.
140
+ */
141
+ export function applySidechainPolicy(messages, policy, opts = {}) {
142
+ const onDrift = opts.onDrift ?? defaultSidechainDrift;
143
+ const nonMeta = [];
144
+ const main = [];
145
+ const sidechain = [];
146
+ for (const m of messages) {
147
+ if (!isObject(m) || isMetaMsg(m))
148
+ continue;
149
+ nonMeta.push(m);
150
+ if (isSidechainMsg(m))
151
+ sidechain.push(m);
152
+ else
153
+ main.push(m);
154
+ }
155
+ if (policy === "hidden") {
156
+ return { topLevel: main };
157
+ }
158
+ if (policy === "inline") {
159
+ // Chronological merge without reordering main: input order already chronological, so the
160
+ // non-meta sequence keeps main as a subsequence with sidechains in place.
161
+ return { topLevel: nonMeta };
162
+ }
163
+ // nested (default): group resolved sidechains; place orphans inline + log drift (Task 2.3).
164
+ const validToolUseIds = collectToolUseIds(main);
165
+ const nestedByToolUseId = new Map();
166
+ const orphans = new Set();
167
+ for (const s of sidechain) {
168
+ const pid = parentToolUseIdOf(s);
169
+ if (pid !== undefined && validToolUseIds.has(pid)) {
170
+ const arr = nestedByToolUseId.get(pid) ?? [];
171
+ arr.push(s);
172
+ nestedByToolUseId.set(pid, arr);
173
+ }
174
+ else {
175
+ orphans.add(s);
176
+ onDrift({ kind: "orphan-sidechain", parentToolUseId: pid ?? null, uuid: uuidOf(s) });
177
+ }
178
+ }
179
+ // topLevel = main turns + orphan sidechains placed INLINE in chronological position. Resolved
180
+ // (nested) sidechains are excluded from topLevel — they live in nestedByToolUseId.
181
+ const topLevel = [];
182
+ for (const m of nonMeta) {
183
+ if (isSidechainMsg(m)) {
184
+ if (orphans.has(m))
185
+ topLevel.push(m); // orphan kept inline (R2.3), never silently dropped
186
+ }
187
+ else {
188
+ topLevel.push(m);
189
+ }
190
+ }
191
+ return { topLevel, nestedByToolUseId };
192
+ }
193
+ /**
194
+ * Linearize the SDK's `SessionMessage[]` into an ordered top-level `Turn[]` for the §7 translator:
195
+ * adopt the SDK order (R1.1), apply the sidechain flattening policy (R2, default `nested`), and
196
+ * assign each turn a stable, uuid-anchored order key (R3.1/R3.2).
197
+ *
198
+ * Under `nested`, each main turn that spawned resolved sidechains carries them on `Turn.nested`
199
+ * (attached by its `tool_use` ids); a top-level sidechain (inline policy, or orphan) carries
200
+ * `Turn.sidechain = { parentToolUseId }`. The main-turn subsequence is identical across all three
201
+ * policies (R2.3) — only sidechain placement differs.
202
+ *
203
+ * Pure and synchronous: no I/O, no SDK call (the caller supplies `messages`).
204
+ *
205
+ * @param messages the SDK-ordered message sequence (from {@link readOrderedMessages}).
206
+ * @param opts flattening-policy + orphan-drift options (`nested` default).
207
+ * @returns the ordered top-level `Turn[]`.
208
+ */
209
+ export function linearizeTurns(messages, opts = {}) {
210
+ const policy = opts.sidechainPolicy ?? "nested";
211
+ const { topLevel, nestedByToolUseId } = applySidechainPolicy(messages, policy, {
212
+ onDrift: opts.onDrift,
213
+ });
214
+ return topLevel.map((m, index) => {
215
+ const turn = {
216
+ orderKey: chainPositionKey(uuidOf(m), index),
217
+ uuid: uuidOf(m),
218
+ parentUuid: parentUuidOf(m),
219
+ role: typeof m.type === "string" ? m.type : "",
220
+ message: m,
221
+ };
222
+ if (isSidechainMsg(m)) {
223
+ // A sidechain in the top level (inline policy, or orphan under nested): point up to its Task.
224
+ const pid = parentToolUseIdOf(m);
225
+ if (pid !== undefined)
226
+ turn.sidechain = { parentToolUseId: pid };
227
+ }
228
+ else if (policy === "nested" && nestedByToolUseId) {
229
+ // Attach the sidechain rows spawned by this main turn's tool_use blocks. The live merge path
230
+ // (story 041) concatenates the SEPARATE sub-agent stream onto the main chain, so insertion
231
+ // order is NOT a reliable render order; sort the attached children by uuid (R1.2) so the
232
+ // nested block is deterministic regardless of how the two streams interleaved on input.
233
+ const children = [];
234
+ for (const id of toolUseIdsOf(m)) {
235
+ const group = nestedByToolUseId.get(id);
236
+ if (group)
237
+ children.push(...group);
238
+ }
239
+ if (children.length > 0) {
240
+ children.sort((x, y) => uuidOf(x).localeCompare(uuidOf(y)));
241
+ turn.nested = children;
242
+ }
243
+ }
244
+ return turn;
245
+ });
246
+ }
247
+ /**
248
+ * Thin SOURCE wrapper (Task 1.1, R1.1): call the injected {@link GetMessages} seam (the SDK's pure,
249
+ * billing-free `getSessionMessages`) and return its `SessionMessage[]` in the SAME chronological
250
+ * order the SDK produced. Performs NO reordering and NO file I/O of its own — the SDK is the single
251
+ * parser (E5 REUSE-live). The seam may be sync or async; we await either way.
252
+ *
253
+ * On an SDK throw (binary/version drift, R1.3 trigger condition) the error is SURFACED with the
254
+ * resolved `sessionId`/`dir` rather than silently swallowed into `[]`, so a drift is loud and
255
+ * diagnosable (the caller decides whether to engage the R1.3 fallback).
256
+ *
257
+ * @param sessionId the session id; equals the transcript filename basename.
258
+ * @param dir the runtime cwd (story 015 `readCwdFromInside`) passed as `{ dir }`; may be undefined.
259
+ * @param opts injectable `getMessages` seam (defaults to the SDK reader via dynamic import).
260
+ * @returns the SDK's ordered `SessionMessage[]` (the chronological main chain).
261
+ */
262
+ export async function readOrderedMessages(sessionId, dir, opts = {}) {
263
+ const getMessages = opts.getMessages ?? defaultGetMessages;
264
+ try {
265
+ return await getMessages(sessionId, { dir });
266
+ }
267
+ catch (err) {
268
+ // R1.3 contingency signal: surface the drift loudly with the resolved coordinates. Never `[]`.
269
+ throw new Error(`getSessionMessages failed for sessionId="${sessionId}" dir="${dir ?? "<none>"}": ${String(err)}`);
270
+ }
271
+ }
272
+ /**
273
+ * Default {@link GetMessages}: the SDK's pure, billing-free `getSessionMessages`. Imported lazily
274
+ * (dynamic import) so this module — and the deterministic unit tests, which always inject a stub —
275
+ * never force-loads the SDK at module-eval time. Mirrors engine-watcher.ts `defaultGetMessages`.
276
+ *
277
+ * Exported (story 043 R2.1) so acp-agent.ts can reuse it as the PRODUCTION reduced base it wraps in
278
+ * the diff-enriched reader when `liveDiff` is ON (it is the same reduced reader `readOrderedMessages`
279
+ * already falls back to). linearize.ts is a fork module imported directly from ../dist/ — NOT via
280
+ * lib.ts, whose public surface is frozen to upstream v0.39.0 — so this export is safe.
281
+ */
282
+ export async function defaultGetMessages(sessionId, opts) {
283
+ const sdk = await import("@anthropic-ai/claude-agent-sdk");
284
+ return sdk.getSessionMessages(sessionId, opts);
285
+ }
286
+ /** The two conversational roles getSessionMessages returns by default (system excluded). */
287
+ const MAIN_ROLES = new Set(["user", "assistant"]);
288
+ /**
289
+ * Contingency mirror of getSessionMessages over RAW JSONL lines (R1.3) — GATED OFF by default.
290
+ *
291
+ * Reproduces the SDK's linearization behaviorally: parse each line (corrupt lines skipped), keep
292
+ * only main-chain user/assistant rows (drop `isMeta`/`isSidechain`), then follow the parentUuid
293
+ * chain BACKWARD from the latest main row (the tip) to the root and reverse — so abandoned fork
294
+ * branches (not on the tip's ancestry) are excluded (R4.1) and the surviving chain is returned in
295
+ * chronological order. Projects each surviving record into the reduced SDK shape.
296
+ *
297
+ * STUB LIMITATION (documented, contingency-only): the tip is taken as the LAST main row in file
298
+ * order, assuming — as getSessionMessages resolves — that the surviving chain ends at the latest
299
+ * record. A more elaborate anchor-selection is out of scope for a gated-OFF mirror.
300
+ *
301
+ * @param rawLines the transcript's raw JSONL lines (full universal-field shape).
302
+ * @param opts the gate; the mirror runs ONLY with `{ enabled: true }`.
303
+ * @returns the surviving main chain as reduced-shape `SessionMessage[]`, in chronological order.
304
+ */
305
+ export function buildChainEquivalent(rawLines, opts = {}) {
306
+ if (opts.enabled !== true) {
307
+ throw new Error("buildChainEquivalent is the R1.3 contingency and is DISABLED by default; " +
308
+ "pass { enabled: true } to engage it. The v1 path is getSessionMessages (E5 REUSE-live).");
309
+ }
310
+ // 1. Parse (skip corrupt/blank lines — forward-compat parser tolerance, §6).
311
+ const records = [];
312
+ for (const line of rawLines) {
313
+ if (typeof line !== "string" || line.length === 0)
314
+ continue;
315
+ try {
316
+ const rec = JSON.parse(line);
317
+ if (isObject(rec))
318
+ records.push(rec);
319
+ }
320
+ catch {
321
+ continue;
322
+ }
323
+ }
324
+ // 2. Keep only main-chain user/assistant rows (mirror the SDK isSidechain/isMeta/system filter).
325
+ const main = records.filter((r) => {
326
+ if (r.isMeta === true || r.isSidechain === true)
327
+ return false;
328
+ return typeof r.type === "string" && MAIN_ROLES.has(r.type);
329
+ });
330
+ if (main.length === 0)
331
+ return [];
332
+ // 3. Walk the parentUuid chain backward from the tip (latest main row) to the root, then reverse.
333
+ const byUuid = new Map();
334
+ for (const r of main) {
335
+ if (typeof r.uuid === "string")
336
+ byUuid.set(r.uuid, r);
337
+ }
338
+ const tip = main[main.length - 1];
339
+ const reverseChain = [];
340
+ const guard = new Set(); // cycle guard (a malformed parentUuid loop must not hang)
341
+ let cursor = tip;
342
+ while (cursor) {
343
+ const uuid = typeof cursor.uuid === "string" ? cursor.uuid : "";
344
+ if (uuid && guard.has(uuid))
345
+ break;
346
+ if (uuid)
347
+ guard.add(uuid);
348
+ reverseChain.push(cursor);
349
+ const parent = typeof cursor.parentUuid === "string" ? cursor.parentUuid : null;
350
+ cursor = parent ? byUuid.get(parent) : undefined;
351
+ }
352
+ reverseChain.reverse();
353
+ // 4. Project each surviving record into the reduced SDK shape getSessionMessages would emit.
354
+ return reverseChain.map((r) => ({
355
+ type: typeof r.type === "string" ? r.type : "",
356
+ uuid: typeof r.uuid === "string" ? r.uuid : "",
357
+ session_id: typeof r.sessionId === "string" ? r.sessionId : "",
358
+ message: r.message,
359
+ parent_tool_use_id: typeof r.parentToolUseId === "string"
360
+ ? r.parentToolUseId
361
+ : typeof r.parent_tool_use_id === "string"
362
+ ? r.parent_tool_use_id
363
+ : null,
364
+ }));
365
+ }
366
+ /**
367
+ * The SINGLE seam both the live re-parse path and the `session/load` replay path call (R3.3):
368
+ * `readOrderedMessages` (adopt the SDK's ordered main chain) + `linearizeTurns` (partition, apply
369
+ * the sidechain policy, assign stable order keys). Because both paths funnel through this one
370
+ * function, live and replay CANNOT diverge in ordering — the guarantee holds by construction.
371
+ *
372
+ * The live read path streams mid-turn content on the story-015 debounced-write signal; this
373
+ * stable (re-)ordering re-runs on the story-024 end-of-turn signal (see {@link bindEndOfTurnReparse},
374
+ * Task 3.3). `replaySessionHistory` (story 011/023) calls it once for `session/load`.
375
+ *
376
+ * @param sessionId the session id; equals the transcript filename basename.
377
+ * @param dir the runtime cwd (story 015) passed to `getSessionMessages`; may be undefined.
378
+ * @param opts the SDK seam + sidechain policy / orphan-drift options.
379
+ * @returns the ordered `Turn[]` (identical whether requested via the live or replay seam).
380
+ */
381
+ export async function readOrderedTurns(sessionId, dir, opts = {}) {
382
+ const messages = await readOrderedMessages(sessionId, dir, { getMessages: opts.getMessages });
383
+ return linearizeTurns(messages, {
384
+ sidechainPolicy: opts.sidechainPolicy,
385
+ onDrift: opts.onDrift,
386
+ });
387
+ }
388
+ // === Task 3.3: bind the live re-parse cadence to the story-024 end-of-turn signal ============
389
+ //
390
+ // CONSUMES the end-of-turn signal — it does NOT detect it (that is story 024). Re-invokes the
391
+ // re-parse (readOrderedTurns) exactly once per detected end-of-turn, with a turn-watchdog safety
392
+ // net. Mirrors engine-watcher.ts notifyEndOfTurn cadence, but produces the stable Turn[] ordering.
393
+ /** The turn watchdog window (ms) — DEGRAU-0 decision 2. DISTINCT from the 2000ms file-discovery one. */
394
+ export const TURN_WATCHDOG_MS = 5583;
395
+ /** Default timer seam: fire `fn` after `ms`; the returned fn cancels it. */
396
+ function defaultSchedule(fn, ms) {
397
+ const handle = setTimeout(fn, ms);
398
+ return () => clearTimeout(handle);
399
+ }
400
+ /**
401
+ * Subscribe a re-parse to the end-of-turn cadence (R3.1): re-invoke `reparse` exactly once per
402
+ * detected end-of-turn (the story-024 detector already debounces on terminal stop_reason + Δt=200ms,
403
+ * so we do NOT re-debounce). A turn watchdog (default {@link TURN_WATCHDOG_MS}) re-parses once as a
404
+ * safety net if no end-of-turn signal arrives; a real signal cancels the pending watchdog so a turn
405
+ * never triggers two re-parses. Because the re-parse re-runs `linearizeTurns` over the full monotonic
406
+ * superset, prior orderKeys are reused (R3.1).
407
+ *
408
+ * @param opts the reparse callback + watchdog/timer seams.
409
+ * @returns an {@link EndOfTurnBinding} (`notifyEndOfTurn` + `stop`).
410
+ */
411
+ export function bindEndOfTurnReparse(opts) {
412
+ const watchdogMs = opts.watchdogMs ?? TURN_WATCHDOG_MS;
413
+ const schedule = opts.schedule ?? defaultSchedule;
414
+ let stopped = false;
415
+ let cancelWatchdog;
416
+ const armWatchdog = () => {
417
+ cancelWatchdog?.();
418
+ cancelWatchdog = schedule(() => {
419
+ cancelWatchdog = undefined;
420
+ if (stopped)
421
+ return;
422
+ void opts.reparse(); // safety-net re-parse — the signal never came within the window
423
+ armWatchdog(); // re-arm for the next turn
424
+ }, watchdogMs);
425
+ };
426
+ armWatchdog(); // arm for the first turn
427
+ return {
428
+ notifyEndOfTurn() {
429
+ if (stopped)
430
+ return;
431
+ cancelWatchdog?.(); // a real signal cancels the safety net (no double re-parse)
432
+ cancelWatchdog = undefined;
433
+ void opts.reparse(); // exactly one re-parse for this turn
434
+ armWatchdog(); // arm for the next turn
435
+ },
436
+ stop() {
437
+ if (stopped)
438
+ return;
439
+ stopped = true;
440
+ cancelWatchdog?.();
441
+ cancelWatchdog = undefined;
442
+ },
443
+ };
444
+ }