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.
- package/dist/agent/infra/tools/implementations/curate-tool.js +18 -8
- package/dist/server/constants.d.ts +6 -0
- package/dist/server/constants.js +11 -0
- package/dist/server/core/domain/entities/task-history-entry.d.ts +775 -0
- package/dist/server/core/domain/entities/task-history-entry.js +88 -0
- package/dist/server/core/domain/transport/schemas.d.ts +1403 -11
- package/dist/server/core/domain/transport/schemas.js +157 -6
- package/dist/server/core/domain/transport/task-info.d.ts +18 -0
- package/dist/server/core/interfaces/process/i-task-lifecycle-hook.d.ts +7 -0
- package/dist/server/core/interfaces/storage/i-task-history-store.d.ts +62 -0
- package/dist/server/core/interfaces/storage/i-task-history-store.js +1 -0
- package/dist/server/infra/daemon/brv-server.js +43 -18
- package/dist/server/infra/dream/dream-response-schemas.d.ts +24 -0
- package/dist/server/infra/dream/dream-response-schemas.js +7 -0
- package/dist/server/infra/dream/operations/consolidate.js +21 -8
- package/dist/server/infra/dream/operations/synthesize.js +35 -8
- package/dist/server/infra/process/task-history-entry-builder.d.ts +36 -0
- package/dist/server/infra/process/task-history-entry-builder.js +101 -0
- package/dist/server/infra/process/task-history-hook.d.ts +37 -0
- package/dist/server/infra/process/task-history-hook.js +70 -0
- package/dist/server/infra/process/task-history-store-cache.d.ts +25 -0
- package/dist/server/infra/process/task-history-store-cache.js +106 -0
- package/dist/server/infra/process/task-router.d.ts +72 -0
- package/dist/server/infra/process/task-router.js +690 -15
- package/dist/server/infra/process/transport-handlers.d.ts +8 -0
- package/dist/server/infra/process/transport-handlers.js +2 -0
- package/dist/server/infra/storage/file-task-history-store.d.ts +294 -0
- package/dist/server/infra/storage/file-task-history-store.js +912 -0
- package/dist/shared/transport/events/index.d.ts +5 -0
- package/dist/shared/transport/events/task-events.d.ts +204 -1
- package/dist/shared/transport/events/task-events.js +11 -0
- package/dist/tui/features/tasks/hooks/use-task-subscriptions.js +7 -0
- package/dist/tui/features/tasks/stores/tasks-store.d.ts +4 -16
- package/dist/tui/features/tasks/stores/tasks-store.js +7 -0
- package/dist/tui/types/messages.d.ts +2 -9
- package/dist/webui/assets/index-DyVvFoM6.css +1 -0
- package/dist/webui/assets/index-lr0byHh9.js +130 -0
- package/dist/webui/index.html +2 -2
- package/dist/webui/sw.js +1 -1
- package/oclif.manifest.json +665 -665
- package/package.json +1 -1
- package/dist/webui/assets/index--sXE__bc.css +0 -1
- 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 {};
|