byterover-cli 3.11.0 → 3.12.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 (43) hide show
  1. package/dist/agent/infra/tools/implementations/curate-tool.js +18 -8
  2. package/dist/server/constants.d.ts +6 -0
  3. package/dist/server/constants.js +11 -0
  4. package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
  5. package/dist/server/core/domain/entities/task-history-entry.js +88 -0
  6. package/dist/server/core/domain/transport/schemas.d.ts +1403 -11
  7. package/dist/server/core/domain/transport/schemas.js +157 -6
  8. package/dist/server/core/domain/transport/task-info.d.ts +18 -0
  9. package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -0
  10. package/dist/server/core/interfaces/storage/i-task-history-store.d.ts +62 -0
  11. package/dist/server/core/interfaces/storage/i-task-history-store.js +1 -0
  12. package/dist/server/infra/daemon/brv-server.js +43 -18
  13. package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
  14. package/dist/server/infra/dream/dream-response-schemas.js +7 -0
  15. package/dist/server/infra/dream/operations/consolidate.js +21 -8
  16. package/dist/server/infra/dream/operations/synthesize.js +35 -8
  17. package/dist/server/infra/process/task-history-entry-builder.d.ts +36 -0
  18. package/dist/server/infra/process/task-history-entry-builder.js +101 -0
  19. package/dist/server/infra/process/task-history-hook.d.ts +37 -0
  20. package/dist/server/infra/process/task-history-hook.js +70 -0
  21. package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
  22. package/dist/server/infra/process/task-history-store-cache.js +106 -0
  23. package/dist/server/infra/process/task-router.d.ts +72 -0
  24. package/dist/server/infra/process/task-router.js +690 -15
  25. package/dist/server/infra/process/transport-handlers.d.ts +8 -0
  26. package/dist/server/infra/process/transport-handlers.js +2 -0
  27. package/dist/server/infra/storage/file-task-history-store.d.ts +294 -0
  28. package/dist/server/infra/storage/file-task-history-store.js +912 -0
  29. package/dist/shared/transport/events/index.d.ts +5 -0
  30. package/dist/shared/transport/events/task-events.d.ts +204 -1
  31. package/dist/shared/transport/events/task-events.js +11 -0
  32. package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
  33. package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
  34. package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
  35. package/dist/tui/types/messages.d.ts +2 -9
  36. package/dist/webui/assets/index-DyVvFoM6.css +1 -0
  37. package/dist/webui/assets/index-lr0byHh9.js +130 -0
  38. package/dist/webui/index.html +2 -2
  39. package/dist/webui/sw.js +1 -1
  40. package/oclif.manifest.json +665 -665
  41. package/package.json +1 -1
  42. package/dist/webui/assets/index--sXE__bc.css +0 -1
  43. package/dist/webui/assets/index-Bkkx961b.js +0 -130
@@ -31,6 +31,7 @@ import type { IClientManager } from '../../core/interfaces/client/i-client-manag
31
31
  import type { ITaskLifecycleHook } from '../../core/interfaces/process/i-task-lifecycle-hook.js';
32
32
  import type { IProjectRegistry } from '../../core/interfaces/project/i-project-registry.js';
33
33
  import type { IProjectRouter } from '../../core/interfaces/routing/i-project-router.js';
34
+ import type { ITaskHistoryStore } from '../../core/interfaces/storage/i-task-history-store.js';
34
35
  import type { ITransportServer } from '../../core/interfaces/transport/i-transport-server.js';
35
36
  import type { IsReviewDisabledResolver, PreDispatchCheck } from './task-router.js';
36
37
  export type { IsReviewDisabledResolver, PreDispatchCheck, PreDispatchCheckResult } from './task-router.js';
@@ -43,6 +44,8 @@ type TransportHandlersOptions = {
43
44
  * `client:register` ack so clients can render version-drift indicators.
44
45
  */
45
46
  daemonVersion?: string;
47
+ /** Per-project `ITaskHistoryStore` factory used by the M2.09 persistent-history handlers. */
48
+ getTaskHistoryStore?: (projectPath: string) => ITaskHistoryStore;
46
49
  /** Resolves project's review-disabled flag at task-create. Snapshotted once into TaskInfo + TaskExecute. */
47
50
  isReviewDisabled?: IsReviewDisabledResolver;
48
51
  /** Lifecycle hooks for task events (e.g. CurateLogHandler). */
@@ -51,6 +54,11 @@ type TransportHandlersOptions = {
51
54
  preDispatchCheck?: PreDispatchCheck;
52
55
  projectRegistry?: IProjectRegistry;
53
56
  projectRouter?: IProjectRouter;
57
+ /** Resolves the active provider/model snapshot stamped onto created tasks. */
58
+ resolveActiveProvider?: () => Promise<{
59
+ model?: string;
60
+ provider?: string;
61
+ }>;
54
62
  transport: ITransportServer;
55
63
  };
56
64
  /**
@@ -41,11 +41,13 @@ export class TransportHandlers {
41
41
  this.taskRouter = new TaskRouter({
42
42
  agentPool: options.agentPool,
43
43
  getAgentForProject: (projectPath) => this.connectionCoordinator.getAgentForProject(projectPath),
44
+ getTaskHistoryStore: options.getTaskHistoryStore,
44
45
  isReviewDisabled: options.isReviewDisabled,
45
46
  lifecycleHooks: options.lifecycleHooks,
46
47
  preDispatchCheck: options.preDispatchCheck,
47
48
  projectRegistry: options.projectRegistry,
48
49
  projectRouter: options.projectRouter,
50
+ resolveActiveProvider: options.resolveActiveProvider,
49
51
  resolveClientProjectPath: (clientId) => options.clientManager?.getClient(clientId)?.projectPath,
50
52
  transport: options.transport,
51
53
  });
@@ -0,0 +1,294 @@
1
+ /**
2
+ * File-based implementation of `ITaskHistoryStore`.
3
+ *
4
+ * Two-tier on-disk format:
5
+ * - `_index.jsonl` — append-only summary file. One JSON-line per save.
6
+ * Cheap to scan for list views.
7
+ * - `data/tsk-${taskId}.json` — full Level 2 detail per task (response,
8
+ * tool calls, reasoning) for the detail panel.
9
+ *
10
+ * Save ordering (data first, index second) bounds the failure mode to
11
+ * orphan data files (invisible to `list()`); compaction in M2.03 will
12
+ * sweep them. The reverse order would create dangling index entries
13
+ * pointing at non-existent data files.
14
+ *
15
+ * M2.02 scope: save / getById / list. Prune (M2.03), stale recovery
16
+ * (M2.04), and delete/clear (M2.05) land in subsequent tickets.
17
+ */
18
+ import type { TaskListItem } from '../../../shared/transport/events/task-events.js';
19
+ import type { TaskHistoryEntry } from '../../core/domain/entities/task-history-entry.js';
20
+ import type { ITaskHistoryStore, TaskHistoryStatus } from '../../core/interfaces/storage/i-task-history-store.js';
21
+ /**
22
+ * Group a list of pre-serialized index lines into batches whose total UTF-8
23
+ * byte length stays under `maxBytes`. A single line larger than `maxBytes` is
24
+ * emitted as its own batch (we never split a line — the caller relies on
25
+ * line-level atomicity).
26
+ *
27
+ * Exported for direct unit-testing of the chunk boundaries.
28
+ */
29
+ export declare function chunkLinesByBytes(lines: readonly string[], maxBytes: number): readonly (readonly string[])[];
30
+ type FileTaskHistoryStoreOptions = {
31
+ baseDir: string;
32
+ /**
33
+ * Wall-clock time at which this daemon process started. Used to gate
34
+ * stale-recovery: only entries whose `lastSavedAt < daemonStartedAt`
35
+ * are considered orphans of a previous daemon boot. Entries saved
36
+ * post-boot belong to an in-memory active task and must NEVER be
37
+ * recovered to `error: INTERRUPTED` (the read path would otherwise
38
+ * ping-pong with the live throttled saves at every `list()` call).
39
+ *
40
+ * Defaults to `Date.now()` at construction time, which is correct for
41
+ * production (one store per project, constructed at first use).
42
+ * Tests that wish to simulate "entries from a previous daemon" should
43
+ * pass a value FAR IN THE FUTURE so their saves register as pre-boot.
44
+ */
45
+ daemonStartedAt?: number;
46
+ /**
47
+ * Age-based prune threshold. Entries older than this many days are
48
+ * tombstoned + their data files unlinked. Default `TASK_HISTORY_DEFAULT_MAX_AGE_DAYS`
49
+ * (0 — disabled by default; count cap is the sole retention). Pass a
50
+ * positive integer to opt in to time-based eviction for a specific store.
51
+ */
52
+ maxAgeDays?: number;
53
+ /**
54
+ * Count-based prune cap. When live entry count exceeds this, oldest excess
55
+ * entries are tombstoned. Default `TASK_HISTORY_DEFAULT_MAX_ENTRIES` (1000).
56
+ * Pass `Number.POSITIVE_INFINITY` to disable.
57
+ */
58
+ maxEntries?: number;
59
+ /**
60
+ * Index-compaction trigger. When `total_lines / live_count` exceeds this
61
+ * ratio, `_index.jsonl` is rewritten with one line per live entry. Default
62
+ * `TASK_HISTORY_DEFAULT_MAX_INDEX_BLOAT_RATIO` (2). Pass
63
+ * `Number.POSITIVE_INFINITY` to disable compaction.
64
+ */
65
+ maxIndexBloatRatio?: number;
66
+ /**
67
+ * Staleness threshold for read-path recovery: entries with status `'created'`
68
+ * or `'started'` whose `createdAt` is older than this are rewritten to
69
+ * `status: 'error'` with `code: 'INTERRUPTED'`. Defaults to
70
+ * `TASK_HISTORY_STALE_THRESHOLD_MS` (10 minutes). Pass
71
+ * `Number.POSITIVE_INFINITY` to disable recovery.
72
+ */
73
+ staleThresholdMs?: number;
74
+ };
75
+ export declare class FileTaskHistoryStore implements ITaskHistoryStore {
76
+ private readonly daemonStartedAt;
77
+ private readonly dataDir;
78
+ /** Dedup-by-taskId result of the last index read. Invalidated on save/delete/recovery/prune. */
79
+ private indexCache;
80
+ /**
81
+ * In-flight `readIndexDedup` promise — concurrent callers (e.g. `getById`
82
+ * and a parallel `prune` timer) share the same pass so the embedded
83
+ * stale-recovery side effect cannot double-append index lines for the
84
+ * same taskId.
85
+ */
86
+ private indexDedupInFlight;
87
+ /**
88
+ * Monotonic counter bumped on every write to the index (save / tombstone /
89
+ * recovery / compaction). `doReadIndexDedup` samples it at start and at
90
+ * end of its pass; if the epoch advanced during the pass, the snapshot
91
+ * the pass built is stale relative to disk and MUST NOT replace the
92
+ * `indexCache`. Without this guard, a `firePrune` pass that began before
93
+ * a concurrent `save()` can finish AFTER that save's cache invalidation
94
+ * and overwrite the invalidation with a snapshot missing the just-saved
95
+ * row — visible to callers as "list() returns N-1 entries after N awaited
96
+ * saves" (the Category B race).
97
+ */
98
+ private indexEpoch;
99
+ private readonly indexPath;
100
+ private readonly maxAgeDays;
101
+ private readonly maxEntries;
102
+ private readonly maxIndexBloatRatio;
103
+ /**
104
+ * Promise-chain lock serializing operations that mutate the index file
105
+ * AND the data dir together — compaction (snapshot → rewrite → post-rewrite
106
+ * recovery → orphan sweep) and tombstoneAndUnlink (append + unlink). These
107
+ * two operations cannot interleave: a tombstone landing mid-rewrite would
108
+ * be wiped by `rename`, and `recoverPreRenameSaves` cannot recover the
109
+ * tombstone (the data file has been unlinked) — leaving a phantom row
110
+ * (B2). `save()` is intentionally NOT locked: its data file persists, so
111
+ * the C1 fix recovers from the data dir if the index line is wiped.
112
+ */
113
+ private operationLock;
114
+ /**
115
+ * Promise of the most recently scheduled prune+compaction pass. Each
116
+ * `firePrune()` call wraps its `setTimeout` in a promise and assigns it
117
+ * here; re-entrance via `pruneRequested` extends the chain through the
118
+ * tail-recursive `firePrune()` in the `.finally`. Tests await on this via
119
+ * `flushPendingOperations()` to drain pending work deterministically
120
+ * instead of polling.
121
+ */
122
+ private pruneChain;
123
+ /** Dedupes concurrent prune passes — only one runs at a time. */
124
+ private pruneInFlight;
125
+ /** Set when a save fires while a prune is in flight; triggers a re-run after current pass. */
126
+ private pruneRequested;
127
+ private readonly staleThresholdMs;
128
+ private readonly storeDir;
129
+ constructor(opts: FileTaskHistoryStoreOptions);
130
+ clear(options?: {
131
+ projectPath?: string;
132
+ statuses?: TaskHistoryStatus[];
133
+ }): Promise<{
134
+ deletedCount: number;
135
+ taskIds: string[];
136
+ }>;
137
+ delete(taskId: string): Promise<boolean>;
138
+ deleteMany(taskIds: string[]): Promise<string[]>;
139
+ /**
140
+ * Deterministically wait for all in-flight prune/compaction work to drain.
141
+ * Loops because each prune pass may schedule the next via `pruneRequested`,
142
+ * which `firePrune()` reschedules at the tail of `pruneChain`. Also drains
143
+ * `operationLock` so any compaction/tombstone serializer queued behind a
144
+ * prune has finished its writes before the caller proceeds.
145
+ *
146
+ * Test-only contract: production daemon never calls this — saves are
147
+ * fire-and-forget by design.
148
+ */
149
+ flushPendingOperations(): Promise<void>;
150
+ getById(taskId: string): Promise<TaskHistoryEntry | undefined>;
151
+ list(options?: {
152
+ createdAfter?: number;
153
+ createdBefore?: number;
154
+ model?: string[];
155
+ projectPath?: string;
156
+ provider?: string[];
157
+ status?: TaskHistoryStatus[];
158
+ type?: string[];
159
+ }): Promise<TaskListItem[]>;
160
+ save(entry: TaskHistoryEntry): Promise<void>;
161
+ /**
162
+ * Construct the recovered (status='error') variant of a stale entry.
163
+ * Uses Zod parse to narrow to the discriminated 'error' branch without an `as` cast.
164
+ */
165
+ private buildRecovered;
166
+ private dataPath;
167
+ private doReadIndexDedup;
168
+ /**
169
+ * Schedule an asynchronous prune+compaction pass without blocking the caller.
170
+ * Deduplicates concurrent calls — only one pass runs at a time. If a save
171
+ * fires while a pass is in-flight, `pruneRequested` is set so a follow-up
172
+ * pass runs once the current one finishes (catches saves that landed mid-pass).
173
+ *
174
+ * Uses `setTimeout(fn, 0)` to defer the pass to the next macrotask, ensuring
175
+ * all pending microtasks (e.g. a follow-up `getById` that triggers M2.04
176
+ * recovery on the same task) drain before prune runs. Without this, the
177
+ * prune's own `readIndexDedup` could trigger a parallel recovery race.
178
+ */
179
+ private firePrune;
180
+ /**
181
+ * Drop all cached/in-flight reads of the index AND bump `indexEpoch` so any
182
+ * concurrent `doReadIndexDedup` pass that sampled the old epoch will skip
183
+ * its setCache step. Called by every write path (save / tombstone /
184
+ * recovery / compaction). Both layers must clear together — leaving
185
+ * `indexDedupInFlight` set after a write would let a list() call hit the
186
+ * pre-write promise and silently return the stale snapshot.
187
+ */
188
+ private invalidateIndexCaches;
189
+ private isStale;
190
+ /**
191
+ * Daemon-startup gate (C0): only entries whose `lastSavedAt` predates this
192
+ * daemon's boot are eligible for stale recovery. An entry written by the
193
+ * CURRENT daemon (post-boot) belongs to an in-memory active task whose
194
+ * lifecycle hook is still firing throttled saves — recovering it would
195
+ * ping-pong the on-disk state against the next save.
196
+ *
197
+ * Legacy lines (no `lastSavedAt`) fall back to the age-only check so
198
+ * existing index files from before this field was introduced behave as
199
+ * they did pre-C0.
200
+ */
201
+ private isStaleAndRecoverable;
202
+ /**
203
+ * Rewrite `_index.jsonl` keeping one line per live entry when bloat exceeds
204
+ * the configured ratio. Sweeps orphan data files (taskId not in live map)
205
+ * after the rewrite so the data dir stays in sync.
206
+ *
207
+ * Locked against `tombstoneAndUnlink` via `operationLock` — see B2 comment
208
+ * on the field declaration.
209
+ */
210
+ private maybeCompact;
211
+ /**
212
+ * Best-effort: write the recovered shape to the data file FIRST, then
213
+ * append the recovery line to the index. Sequential ordering matches
214
+ * `save()` and bounds the failure mode: if the data-file write fails,
215
+ * we return BEFORE the index append so the index never gains an orphan
216
+ * recovery line pointing to an unmutated data file (which would split
217
+ * `list()` from `getById()` — N1). Each step swallows its own error
218
+ * and logs via `transportLog`; never throws to caller.
219
+ */
220
+ private persistRecovery;
221
+ /**
222
+ * Phase 1 (age) + Phase 2 (count) prune. Builds the dead-taskId set from
223
+ * the dedup'd live map and delegates to `tombstoneAndUnlink`.
224
+ */
225
+ private prune;
226
+ private pruneAndCompact;
227
+ private readIndexDedup;
228
+ /**
229
+ * Detect data files whose index line was overwritten by the compaction
230
+ * rename (race window: save's `appendFile` landed BEFORE compaction's
231
+ * `rename`). For each, parse the data file and either:
232
+ * - C1 path (current-boot save): re-append as live with `lastSavedAt = now`.
233
+ * - N2 path (prior-boot orphan): delegate to `recoverViaTaskId`, which
234
+ * mutates the data file to `status: 'error'` and persists the recovery
235
+ * line. Without this gate, an old `'started'` orphan would be re-stamped
236
+ * `lastSavedAt = Date.now()` and the C0 daemon-startup check would then
237
+ * forever protect it as a live current-boot task.
238
+ *
239
+ * Distinguishing C1 vs N2: synthesize a probe `IndexDataLine` from the data
240
+ * file with no `lastSavedAt` and feed it through `isStaleAndRecoverable`.
241
+ * Recent saves (createdAt within `staleThresholdMs`) fall through to the
242
+ * C1 branch; old `'created'`/`'started'` orphans take the N2 branch.
243
+ */
244
+ private recoverPreRenameSaves;
245
+ /**
246
+ * Read the data file for a stale candidate, mutate to error, persist.
247
+ * Returns the summary projection of the recovered entry. Returns undefined
248
+ * if the data file is missing/corrupt. If the data file already shows a
249
+ * terminal status (a prior partial-success recovery), returns its projection
250
+ * without re-recovering — defensive idempotency restoration.
251
+ */
252
+ private recoverViaTaskId;
253
+ /**
254
+ * Atomically replace `_index.jsonl` with a fresh file containing exactly
255
+ * one line per live entry. Preserves the previous main as `_index.jsonl.bak`
256
+ * for one cycle. Sequence (best-effort .bak; atomic main swap):
257
+ * 1. Write `_index.jsonl.tmp` with the new content
258
+ * 2. copyFile `_index.jsonl` → `_index.jsonl.bak` (best-effort)
259
+ * 3. rename `_index.jsonl.tmp` → `_index.jsonl` (single atomic syscall)
260
+ */
261
+ private rewriteIndex;
262
+ /**
263
+ * After compaction, unlink any `data/tsk-${taskId}.json` whose taskId is
264
+ * not in the live map. Best-effort per file — ENOENT etc. is swallowed.
265
+ */
266
+ private sweepOrphanData;
267
+ /**
268
+ * Tombstone the given taskIds, then unlink each data file in parallel. Order
269
+ * matters: tombstone first so list/getById skip the entry even if the unlink
270
+ * fails (orphan data files are swept by M2.03 compaction). Reverse order
271
+ * would leave the row visible to list while getById returns undefined.
272
+ *
273
+ * Tombstones are appended in size-bounded chunks (each <`MAX_APPEND_CHUNK_BYTES`).
274
+ * POSIX guarantees nothing about regular-file write atomicity beyond pipes
275
+ * (PIPE_BUF applies to FIFOs/sockets only). Linux ext4 / macOS APFS happen
276
+ * to serialize appends per inode, but other filesystems may interleave a
277
+ * concurrent unlocked `save()` into the middle of a large multi-line write,
278
+ * corrupting a tombstone JSON line. Keeping every individual `appendFile`
279
+ * call comfortably under 4 KB lets the kernel page-cache write path treat
280
+ * each call as a single sector-level write, which most filesystems handle
281
+ * atomically.
282
+ *
283
+ * Locked against `maybeCompact` via `operationLock` (B2): if our appendFile
284
+ * landed mid-rewrite the tombstone would be wiped by `rename`, and our
285
+ * subsequent unlink would orphan the index entry `recoverPreRenameSaves`
286
+ * cannot detect (data file gone). The lock spans the entire chunked append
287
+ * + unlink sequence so all tombstones and unlinks are durable before any
288
+ * compaction consumes the snapshot.
289
+ */
290
+ private tombstoneAndUnlink;
291
+ private withOperationLock;
292
+ private writeAtomic;
293
+ }
294
+ export {};