@vibe-commit/capture 0.9.0 → 0.11.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/README.md +49 -1
- package/dist/agent_hooks.js +2 -2
- package/dist/cli.d.ts +35 -0
- package/dist/cli.js +412 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/backfill.js +19 -2
- package/dist/commands/backfill.js.map +1 -1
- package/dist/commands/finalize.d.ts +37 -1
- package/dist/commands/finalize.js +131 -29
- package/dist/commands/finalize.js.map +1 -1
- package/dist/commands/status.js +7 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/status_machine.js +5 -0
- package/dist/commands/status_machine.js.map +1 -1
- package/dist/commands/upload_worker.d.ts +63 -0
- package/dist/commands/upload_worker.js +336 -0
- package/dist/commands/upload_worker.js.map +1 -0
- package/dist/commands/why.d.ts +19 -1
- package/dist/commands/why.js +35 -3
- package/dist/commands/why.js.map +1 -1
- package/dist/commit_files.d.ts +64 -0
- package/dist/commit_files.js +83 -0
- package/dist/commit_files.js.map +1 -0
- package/dist/copy/strings.d.ts +34 -14
- package/dist/copy/strings.js +51 -14
- package/dist/copy/strings.js.map +1 -1
- package/dist/drain.d.ts +68 -0
- package/dist/drain.js +234 -0
- package/dist/drain.js.map +1 -0
- package/dist/firing.d.ts +169 -0
- package/dist/firing.js +334 -0
- package/dist/firing.js.map +1 -0
- package/dist/hooks/entry.d.ts +171 -26
- package/dist/hooks/entry.js +335 -507
- package/dist/hooks/entry.js.map +1 -1
- package/dist/hooks/fire_lock.d.ts +36 -87
- package/dist/hooks/fire_lock.js +40 -99
- package/dist/hooks/fire_lock.js.map +1 -1
- package/dist/hooks/post_commit.d.ts +11 -0
- package/dist/hooks/post_commit.js +16 -8
- package/dist/hooks/post_commit.js.map +1 -1
- package/dist/identity.d.ts +32 -0
- package/dist/identity.js +38 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +3 -17
- package/dist/index.js +59 -450
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +18 -0
- package/dist/paths.js +31 -0
- package/dist/paths.js.map +1 -1
- package/dist/pending_prune.d.ts +2 -2
- package/dist/pending_prune.js +2 -2
- package/dist/policy.d.ts +17 -0
- package/dist/policy.js +7 -0
- package/dist/policy.js.map +1 -1
- package/dist/post.d.ts +63 -4
- package/dist/post.js +76 -24
- package/dist/post.js.map +1 -1
- package/dist/replay.d.ts +167 -0
- package/dist/replay.js +301 -0
- package/dist/replay.js.map +1 -0
- package/dist/session_targets.d.ts +12 -0
- package/dist/session_targets.js +6 -1
- package/dist/session_targets.js.map +1 -1
- package/dist/spool.d.ts +55 -21
- package/dist/spool.js +182 -38
- package/dist/spool.js.map +1 -1
- package/dist/spool_union.d.ts +18 -2
- package/dist/spool_union.js +23 -2
- package/dist/spool_union.js.map +1 -1
- package/dist/state.js +21 -14
- package/dist/state.js.map +1 -1
- package/dist/stdin.d.ts +2 -0
- package/dist/stdin.js +13 -0
- package/dist/stdin.js.map +1 -0
- package/dist/uploader_status.d.ts +34 -0
- package/dist/uploader_status.js +25 -0
- package/dist/uploader_status.js.map +1 -0
- package/dist/uploader_view.d.ts +39 -0
- package/dist/uploader_view.js +106 -0
- package/dist/uploader_view.js.map +1 -0
- package/dist/verbs.d.ts +11 -0
- package/dist/verbs.js +13 -0
- package/dist/verbs.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +2 -2
package/dist/drain.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE IDLE-SESSION DRAIN, WORKER SIDE — U5c of the detached uploader (plan §3.9 "Idle drain cap", §3.3 fairness).
|
|
3
|
+
*
|
|
4
|
+
* A session whose last firing ran with bytes still behind (a multi-repo session at `SessionEnd`, a repo found late in a long one) has no
|
|
5
|
+
* next hook of its own. The hook used to finish such sessions with the leftover of its one deadline (`drainIdleSessions`, `entry.ts`); U6
|
|
6
|
+
* removes that from the hook, and this is where it lives. The worker calls `drainIdleStep` when it has no live firing to serve, so
|
|
7
|
+
* **live firings always outrank the drain** and the queue is looked at again between deltas.
|
|
8
|
+
*
|
|
9
|
+
* ## What a step is
|
|
10
|
+
* ONE delta for the first idle session that has something to send, under THAT session's writer lock (never waited for: a busy session is
|
|
11
|
+
* skipped), consent re-checked by the resolver, discovery continued (`resolveTargets` with `drain: true`: ≤8 MiB and ≤8 roots per call).
|
|
12
|
+
*
|
|
13
|
+
* ## What bounds it
|
|
14
|
+
* - **64 MiB per worker life** (`DRAIN_CAP_BYTES`, plan §3.9, a starting value, unmeasured). Without a deadline the drain would otherwise
|
|
15
|
+
* send every idle session's backlog on the worker's first run and flood the shared-cpu VM. U8 measures the Stop-to-`capture_turns` lag.
|
|
16
|
+
* - Sessions are served OLDEST first and a session that cannot be acted on moves to the back of the line (`rotate`), so forty unactionable
|
|
17
|
+
* records at the front cannot starve a live owed session behind them (C18 #7).
|
|
18
|
+
* - Only sessions idle for 10 minutes (`DRAIN_IDLE_MS`) and with NO unconsumed firing: a session with a firing waiting is a live session.
|
|
19
|
+
*
|
|
20
|
+
* ## ⛔ Identity (plan §3.13): "a worker serves only firings of its own identity, in the idle drain as well"
|
|
21
|
+
* The target record names no credential, so the identity is read from the session's firing log: the last firing's `id` must be ours. A
|
|
22
|
+
* session with NO firing log is a LEGACY one (recorded by a hook before U6) and is served only by a worker using the saved credential
|
|
23
|
+
* (`VIBECOMMIT_TOKEN` unset), which is the credential those hooks used unless the user said otherwise. A worker running on an environment
|
|
24
|
+
* token never drains a session it cannot prove is its own.
|
|
25
|
+
*
|
|
26
|
+
* ## ⛔ A drain delivery is not a hook's: it stamps NO liveness (`byHook: false`, no `liveAtMs`). The old hook drain stamped "now" on
|
|
27
|
+
* every idle session it touched, which kept a finished session live for a window it did not earn; that was a defect and is not carried.
|
|
28
|
+
*/
|
|
29
|
+
import { type ReplayConfig } from "./replay.js";
|
|
30
|
+
/** Bytes the drain may send in one worker's life (plan §3.9). */
|
|
31
|
+
export declare const DRAIN_CAP_BYTES: number;
|
|
32
|
+
export interface DrainState {
|
|
33
|
+
/** Bytes sent by the drain so far in this worker's life. */
|
|
34
|
+
bytes: number;
|
|
35
|
+
}
|
|
36
|
+
export type DrainStep =
|
|
37
|
+
/** Nothing idle has anything to send (or the cap is spent): the worker may go idle. */
|
|
38
|
+
{
|
|
39
|
+
readonly kind: "idle";
|
|
40
|
+
}
|
|
41
|
+
/** One delta went. `bytes` is how much it moved the ledgers by. */
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "delivered";
|
|
44
|
+
readonly bytes: number;
|
|
45
|
+
readonly elapsedMs: number;
|
|
46
|
+
}
|
|
47
|
+
/** A timeout or 5xx with no recorded state: the session is retried later. */
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: "later";
|
|
50
|
+
readonly session: string;
|
|
51
|
+
readonly elapsedMs: number;
|
|
52
|
+
readonly retryAfterMs?: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* U6b (F2a): a request WAS made and nothing moved and nothing failed: the server HELD the session (a 403 `capture_not_approved`). Returned like `delivered`
|
|
56
|
+
* and `later`: ONE request per step, however many held sessions are in line. The worker backs the whole drain off after it.
|
|
57
|
+
*/
|
|
58
|
+
| {
|
|
59
|
+
readonly kind: "held";
|
|
60
|
+
readonly session: string;
|
|
61
|
+
readonly elapsedMs: number;
|
|
62
|
+
}
|
|
63
|
+
/** The server rejected the credential. The worker stops. */
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: "fatal";
|
|
66
|
+
readonly elapsedMs: number;
|
|
67
|
+
};
|
|
68
|
+
export declare function drainIdleStep(cfg: ReplayConfig, state: DrainState, nowMs?: () => number): Promise<DrainStep>;
|
package/dist/drain.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE IDLE-SESSION DRAIN, WORKER SIDE — U5c of the detached uploader (plan §3.9 "Idle drain cap", §3.3 fairness).
|
|
3
|
+
*
|
|
4
|
+
* A session whose last firing ran with bytes still behind (a multi-repo session at `SessionEnd`, a repo found late in a long one) has no
|
|
5
|
+
* next hook of its own. The hook used to finish such sessions with the leftover of its one deadline (`drainIdleSessions`, `entry.ts`); U6
|
|
6
|
+
* removes that from the hook, and this is where it lives. The worker calls `drainIdleStep` when it has no live firing to serve, so
|
|
7
|
+
* **live firings always outrank the drain** and the queue is looked at again between deltas.
|
|
8
|
+
*
|
|
9
|
+
* ## What a step is
|
|
10
|
+
* ONE delta for the first idle session that has something to send, under THAT session's writer lock (never waited for: a busy session is
|
|
11
|
+
* skipped), consent re-checked by the resolver, discovery continued (`resolveTargets` with `drain: true`: ≤8 MiB and ≤8 roots per call).
|
|
12
|
+
*
|
|
13
|
+
* ## What bounds it
|
|
14
|
+
* - **64 MiB per worker life** (`DRAIN_CAP_BYTES`, plan §3.9, a starting value, unmeasured). Without a deadline the drain would otherwise
|
|
15
|
+
* send every idle session's backlog on the worker's first run and flood the shared-cpu VM. U8 measures the Stop-to-`capture_turns` lag.
|
|
16
|
+
* - Sessions are served OLDEST first and a session that cannot be acted on moves to the back of the line (`rotate`), so forty unactionable
|
|
17
|
+
* records at the front cannot starve a live owed session behind them (C18 #7).
|
|
18
|
+
* - Only sessions idle for 10 minutes (`DRAIN_IDLE_MS`) and with NO unconsumed firing: a session with a firing waiting is a live session.
|
|
19
|
+
*
|
|
20
|
+
* ## ⛔ Identity (plan §3.13): "a worker serves only firings of its own identity, in the idle drain as well"
|
|
21
|
+
* The target record names no credential, so the identity is read from the session's firing log: the last firing's `id` must be ours. A
|
|
22
|
+
* session with NO firing log is a LEGACY one (recorded by a hook before U6) and is served only by a worker using the saved credential
|
|
23
|
+
* (`VIBECOMMIT_TOKEN` unset), which is the credential those hooks used unless the user said otherwise. A worker running on an environment
|
|
24
|
+
* token never drains a session it cannot prove is its own.
|
|
25
|
+
*
|
|
26
|
+
* ## ⛔ A drain delivery is not a hook's: it stamps NO liveness (`byHook: false`, no `liveAtMs`). The old hook drain stamped "now" on
|
|
27
|
+
* every idle session it touched, which kept a finished session live for a window it did not earn; that was a defect and is not carried.
|
|
28
|
+
*/
|
|
29
|
+
import { existsSync, rmSync, utimesSync } from "node:fs";
|
|
30
|
+
import { admitsTranscript } from "./agents/registry.js";
|
|
31
|
+
import { loadCredential } from "./credential.js";
|
|
32
|
+
import { forgetFirings, readCursor, readFirings } from "./firing.js";
|
|
33
|
+
import { CAPTURE_NOT_APPROVED } from "./post.js";
|
|
34
|
+
import { DRAIN_EXAMINE_MAX, DRAIN_FORGET_MS, DRAIN_IDLE_MS, deliverFiring, fileSize, findSubagentFiles, wait, writeOffOwed, } from "./hooks/entry.js";
|
|
35
|
+
import { claimSessionWriter } from "./hooks/fire_lock.js";
|
|
36
|
+
import { sessionFileStem } from "./paths.js";
|
|
37
|
+
import { REQUEST_TIMEOUT_MS, SESSION_BEAT_MS, SESSION_LOCK_STALE_MS } from "./replay.js";
|
|
38
|
+
import { dialectIsolates, listTargetRecords, resolveTargets, stopKeyOf, LockLostError } from "./session_targets.js";
|
|
39
|
+
import { fileState, loadSessionState, pruneDeliveredEndHold } from "./state.js";
|
|
40
|
+
/** Bytes the drain may send in one worker's life (plan §3.9). */
|
|
41
|
+
export const DRAIN_CAP_BYTES = 64 * 1024 * 1024;
|
|
42
|
+
/** Total bytes acknowledged across a session's ledgers: the measure of what a step moved. */
|
|
43
|
+
function sentBytes(cfg, targets, sessionId, subFiles) {
|
|
44
|
+
let total = 0;
|
|
45
|
+
for (const target of targets) {
|
|
46
|
+
const state = loadSessionState(cfg.home, { repoKey: target.ledgerKey, sessionId });
|
|
47
|
+
total += fileState(state, "main").sentOffset;
|
|
48
|
+
for (const file of subFiles)
|
|
49
|
+
total += fileState(state, file.fileKey).sentOffset;
|
|
50
|
+
}
|
|
51
|
+
return total;
|
|
52
|
+
}
|
|
53
|
+
/** Does this session owe a firing, of any identity? Then it is live work, not idle work. */
|
|
54
|
+
function hasFiring(cfg, sessionId) {
|
|
55
|
+
const stem = sessionFileStem(sessionId);
|
|
56
|
+
return readFirings(cfg.home, stem, readCursor(cfg.home, stem).offset).firings.length > 0;
|
|
57
|
+
}
|
|
58
|
+
/** May THIS worker's credential serve this session? See the module doc. */
|
|
59
|
+
function ours(cfg, sessionId) {
|
|
60
|
+
const stem = sessionFileStem(sessionId);
|
|
61
|
+
const last = readFirings(cfg.home, stem, 0).firings.at(-1)?.firing;
|
|
62
|
+
if (last !== undefined)
|
|
63
|
+
return last.id === cfg.identity;
|
|
64
|
+
const token = cfg.env.VIBECOMMIT_TOKEN?.trim() ?? "";
|
|
65
|
+
return token === "";
|
|
66
|
+
}
|
|
67
|
+
export async function drainIdleStep(cfg, state, nowMs = Date.now) {
|
|
68
|
+
if (state.bytes >= DRAIN_CAP_BYTES)
|
|
69
|
+
return { kind: "idle" };
|
|
70
|
+
const candidates = listTargetRecords(cfg.home)
|
|
71
|
+
.sort((a, b) => a.mtimeMs - b.mtimeMs)
|
|
72
|
+
.slice(0, DRAIN_EXAMINE_MAX);
|
|
73
|
+
for (const { path, record } of candidates) {
|
|
74
|
+
const now = nowMs();
|
|
75
|
+
if (now - record.firedAt < DRAIN_IDLE_MS)
|
|
76
|
+
continue;
|
|
77
|
+
if (hasFiring(cfg, record.sessionId))
|
|
78
|
+
continue;
|
|
79
|
+
const rotate = () => {
|
|
80
|
+
try {
|
|
81
|
+
utimesSync(path, new Date(), new Date());
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// Ordering only.
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const forgetIfOld = () => {
|
|
88
|
+
if (now - record.firedAt > DRAIN_FORGET_MS) {
|
|
89
|
+
try {
|
|
90
|
+
rmSync(path);
|
|
91
|
+
forgetFirings(cfg.home, record.sessionId);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// A record we could not delete is just examined again.
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
rotate();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
if (record.transcriptPath === null) {
|
|
102
|
+
rotate();
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const transcript = record.transcriptPath;
|
|
106
|
+
if (!ours(cfg, record.sessionId)) {
|
|
107
|
+
rotate();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// C28: a dialect that cannot isolate is never owed; it delivers only the repo each turn ends in, on that turn's own firing.
|
|
111
|
+
if (!dialectIsolates(cfg.home, cfg.env, transcript)) {
|
|
112
|
+
forgetIfOld();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Confinement again (`/cso` finding 1): this path was read from a record, and it reaches a read whose result is uploaded.
|
|
116
|
+
if (existsSync(transcript) && !admitsTranscript(cfg.home, cfg.env, transcript)) {
|
|
117
|
+
rotate();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// `agentId` is a placeholder: the drain also serves Codex and Cursor sessions, and delivery takes the dialect and the scrubber from the
|
|
121
|
+
// transcript path, not from this field (the hook drain this replaces used whichever hook happened to be running).
|
|
122
|
+
const ctx = { env: cfg.env, home: cfg.home, nodeVersion: cfg.nodeVersion, agentId: "claude-code", readStdin: () => Promise.resolve("") };
|
|
123
|
+
const size = fileSize(transcript);
|
|
124
|
+
if (size === null) {
|
|
125
|
+
// The transcript is gone: what is owed becomes a gap now (`writeOffOwed`), a write, so under the session's lock, never waited for.
|
|
126
|
+
const writeOff = await claimSessionWriter(cfg.home, record.sessionId, { staleAfterMs: SESSION_LOCK_STALE_MS, deadlineAtMs: 0, wait });
|
|
127
|
+
if (writeOff === null) {
|
|
128
|
+
rotate();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
writeOffOwed(cfg.home, record, now);
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
writeOff.release();
|
|
136
|
+
}
|
|
137
|
+
forgetIfOld();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const live = record.targets.filter((target) => record.stopped[stopKeyOf(target)] === undefined);
|
|
141
|
+
const subFiles = findSubagentFiles(ctx, transcript);
|
|
142
|
+
const behind = live.some((target) => {
|
|
143
|
+
const ledger = loadSessionState(cfg.home, { repoKey: target.ledgerKey, sessionId: record.sessionId });
|
|
144
|
+
return (ledger.endHold !== null ||
|
|
145
|
+
fileState(ledger, "main").sentOffset < size ||
|
|
146
|
+
subFiles.some((file) => fileState(ledger, file.fileKey).sentOffset < file.size));
|
|
147
|
+
});
|
|
148
|
+
if (!behind && record.settled) {
|
|
149
|
+
rotate();
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const claim = await claimSessionWriter(cfg.home, record.sessionId, { staleAfterMs: SESSION_LOCK_STALE_MS, deadlineAtMs: 0, wait });
|
|
153
|
+
if (claim === null) {
|
|
154
|
+
rotate();
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const startedAt = nowMs();
|
|
158
|
+
// The lock goes stale in four beats, not in a request timeout: a timer keeps it alive through a long request (as `replayStep`'s does).
|
|
159
|
+
const beat = setInterval(() => claim.refresh(), SESSION_BEAT_MS);
|
|
160
|
+
beat.unref();
|
|
161
|
+
try {
|
|
162
|
+
const load = loadCredential({ env: cfg.env, home: cfg.home });
|
|
163
|
+
if (load.kind !== "ok")
|
|
164
|
+
return { kind: "fatal", elapsedMs: 0 };
|
|
165
|
+
const heartbeat = () => claim.refresh();
|
|
166
|
+
let resolution;
|
|
167
|
+
try {
|
|
168
|
+
resolution = resolveTargets({
|
|
169
|
+
home: cfg.home,
|
|
170
|
+
env: cfg.env,
|
|
171
|
+
sessionId: record.sessionId,
|
|
172
|
+
transcriptPath: transcript,
|
|
173
|
+
turnEndCwd: null,
|
|
174
|
+
mode: "turn",
|
|
175
|
+
nowMs: nowMs(),
|
|
176
|
+
drain: true,
|
|
177
|
+
heartbeat,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error instanceof LockLostError)
|
|
182
|
+
continue;
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
if (resolution.targets.length === 0) {
|
|
186
|
+
rotate();
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const input = { sessionId: record.sessionId, transcriptPath: transcript, cwd: "", eventName: "Stop" };
|
|
190
|
+
const subs = findSubagentFiles(ctx, transcript);
|
|
191
|
+
const before = sentBytes(cfg, resolution.targets, record.sessionId, subs);
|
|
192
|
+
const results = await deliverFiring({
|
|
193
|
+
ctx,
|
|
194
|
+
input,
|
|
195
|
+
url: cfg.url,
|
|
196
|
+
credential: load.credential,
|
|
197
|
+
resolution,
|
|
198
|
+
eof: size,
|
|
199
|
+
event: null,
|
|
200
|
+
isSessionEnd: false,
|
|
201
|
+
pace: { kind: "unbounded", timeoutMs: cfg.requestTimeoutMs ?? REQUEST_TIMEOUT_MS },
|
|
202
|
+
heartbeat,
|
|
203
|
+
});
|
|
204
|
+
for (const target of resolution.targets)
|
|
205
|
+
pruneDeliveredEndHold(cfg.home, { repoKey: target.ledgerKey, sessionId: record.sessionId });
|
|
206
|
+
const moved = Math.max(0, sentBytes(cfg, resolution.targets, record.sessionId, subs) - before);
|
|
207
|
+
state.bytes += moved;
|
|
208
|
+
const deliveries = results.flatMap((r) => [r.delivery, ...r.subagents.deliveries]);
|
|
209
|
+
if (results.some((r) => r.reason.kind === "fatal") || deliveries.some((d) => d.kind === "attempted" && d.disposition === "fatal")) {
|
|
210
|
+
return { kind: "fatal", elapsedMs: nowMs() - startedAt };
|
|
211
|
+
}
|
|
212
|
+
if (results.some((r) => r.reason.kind === "later") || deliveries.some((d) => d.kind === "attempted" && d.disposition === "later" && d.detail?.code !== CAPTURE_NOT_APPROVED)) {
|
|
213
|
+
// To the back of the line: a session that keeps failing must not stand in front of the ones behind it.
|
|
214
|
+
rotate();
|
|
215
|
+
const waits = deliveries.flatMap((d) => (d.kind === "attempted" && d.disposition === "later" && d.retryAfterMs !== undefined ? [d.retryAfterMs] : []));
|
|
216
|
+
return { kind: "later", session: record.sessionId, elapsedMs: nowMs() - startedAt, ...(waits.length === 0 ? {} : { retryAfterMs: Math.max(...waits) }) };
|
|
217
|
+
}
|
|
218
|
+
if (moved > 0)
|
|
219
|
+
return { kind: "delivered", bytes: moved, elapsedMs: nowMs() - startedAt };
|
|
220
|
+
// Nothing moved and nothing failed. If a request WAS made, the server held this session (403 capture_not_approved): rotate it to the back
|
|
221
|
+
// and RETURN, so one step is one request (F2a: the old loop went on to the next held session inside the same step, up to
|
|
222
|
+
// DRAIN_EXAMINE_MAX back-to-back requests, re-sent on every worker life). If nothing was sent at all, move on without cost.
|
|
223
|
+
rotate();
|
|
224
|
+
if (deliveries.some((d) => d.kind === "attempted"))
|
|
225
|
+
return { kind: "held", session: record.sessionId, elapsedMs: nowMs() - startedAt };
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
clearInterval(beat);
|
|
229
|
+
claim.release();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return { kind: "idle" };
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=drain.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drain.js","sourceRoot":"","sources":["../src/drain.ts"],"names":[],"mappings":"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"}
|
package/dist/firing.d.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE FIRING LOG AND THE HOOK'S LIVENESS STAMP — U3 of the detached uploader.
|
|
3
|
+
*
|
|
4
|
+
* A *firing* is one hook that ended a turn: today's hook body, deferred. The hook appends one line saying "this turn ended here, at this
|
|
5
|
+
* size, in this folder, with these repos" and returns; the upload worker (U5) replays the lines in order, each ALONE, with the values
|
|
6
|
+
* recorded at the moment the hook fired (plan §3.2-§3.4, D15). This module is the storage half and nothing in the hook calls it yet:
|
|
7
|
+
* it is inert until the hook is flipped (U6).
|
|
8
|
+
*
|
|
9
|
+
* ## What it owns
|
|
10
|
+
* - `<sid>.firings.jsonl` — append-only, one `O_APPEND` write per line, no lock (hooks never wait).
|
|
11
|
+
* - `<sid>.firings.cursor` — the worker's progress: a byte offset, and how many times the firing at that offset made the worker THROW.
|
|
12
|
+
* Written atomically, and only by a holder of the session's writer lock.
|
|
13
|
+
* - `<repo digest>/<sid>.live` — the hook-only liveness stamp, and the ONE function that reads liveness (`liveAtOf`).
|
|
14
|
+
*
|
|
15
|
+
* ## What a firing holds: paths and numbers only
|
|
16
|
+
* No credential, no transcript content. The worker re-confines `transcript` and `announced` with `admitsTranscript` before every read.
|
|
17
|
+
*
|
|
18
|
+
* ## ⛔ A LEAF MODULE. It imports `paths` and `atomic_write` and nothing else of this package's, because `spool.ts` and `state.ts` both
|
|
19
|
+
* read liveness through it and neither may import the other.
|
|
20
|
+
*/
|
|
21
|
+
import { type SessionKey } from "./paths.js";
|
|
22
|
+
/** The line format this build writes and reads. A reader sees a NEWER `v` as a stop sign, never as garbage (§3.14). */
|
|
23
|
+
export declare const FIRING_VERSION = 1;
|
|
24
|
+
/** A line longer than this is not written: a firing is ~300 bytes, and one `O_APPEND` write of a small buffer is what keeps hooks lock-free. */
|
|
25
|
+
export declare const MAX_FIRING_BYTES: number;
|
|
26
|
+
/** After this many uncaught throws on one firing, the worker skips it. A firing only sets an EOF, so the next one still covers its bytes. */
|
|
27
|
+
export declare const MAX_FIRING_THROWS = 3;
|
|
28
|
+
/**
|
|
29
|
+
* Per repo key: the liveness the hook read BEFORE it stamped (plan §3.4, P2 point 4). Stamping first would flip `stale-own` to `first-turn`.
|
|
30
|
+
* - a number: the stamp (`SessionState.liveAt`, or the `.live` file, whichever is later).
|
|
31
|
+
* - `null`: a state file exists with NO `liveAt` (what `finalize` and `backfill` leave). Reads as `stale-own`.
|
|
32
|
+
* - key ABSENT: no state file at all. Reads as `first-turn`.
|
|
33
|
+
* A plain number-or-omitted map would read a resumed session as `first-turn` and keep a lingering-id line.
|
|
34
|
+
*/
|
|
35
|
+
export type PriorLiveness = Readonly<Record<string, number | null>>;
|
|
36
|
+
export interface Firing {
|
|
37
|
+
readonly v: number;
|
|
38
|
+
/** The hook's own clock, epoch ms: "the moment its hook runs". Promotion, pending mode and the commit pin are all judged as of it. */
|
|
39
|
+
readonly at: number;
|
|
40
|
+
/**
|
|
41
|
+
* The session id as the agent named it. The log's FILE NAME is `sanitise(sid)` (capped at 128 characters), which loses characters the
|
|
42
|
+
* wire id keeps, so the worker takes the id to send from here and never from a file name.
|
|
43
|
+
*/
|
|
44
|
+
readonly sid: string;
|
|
45
|
+
/** `Stop`, `PreCompact`, `SessionEnd`, ... as the dialect named it. */
|
|
46
|
+
readonly event: string;
|
|
47
|
+
/** The main transcript's size when the hook ran. `SessionEnd` is settled by the worker, not here. */
|
|
48
|
+
readonly eof: number;
|
|
49
|
+
readonly transcript: string;
|
|
50
|
+
/** The turn-end folder (stdin's `cwd`). */
|
|
51
|
+
readonly cwd: string;
|
|
52
|
+
/** The turn-end repo's keys as step 3 resolved them. Re-deriving from `cwd` fails once a work tree is gone (§3.2). Absent: not in a repo. */
|
|
53
|
+
readonly keys?: {
|
|
54
|
+
readonly worktree: string;
|
|
55
|
+
readonly consent: string;
|
|
56
|
+
};
|
|
57
|
+
/** Present only when step 3's consent check got a REAL slug, never the `local:` fallback. */
|
|
58
|
+
readonly slug?: string;
|
|
59
|
+
readonly agent: string;
|
|
60
|
+
/** The identity digest (§3.13): a worker serves only firings of its own. */
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly prior: PriorLiveness;
|
|
63
|
+
/** A sub-agent transcript the dialect announced for this firing. */
|
|
64
|
+
readonly announced?: string;
|
|
65
|
+
/**
|
|
66
|
+
* U6b (F5, C33): each sub-agent file's size when the hook ran, by file key. A replay sends no sub-agent byte past these (a file absent here
|
|
67
|
+
* sends nothing in this step), so a late replay cannot read sub-agent bytes that did not exist at the firing and a delayed worker produces what
|
|
68
|
+
* a prompt one does. ABSENT: an older firing, or one whose map would not fit the line; its sub-agents are sent to their real size, as before.
|
|
69
|
+
*/
|
|
70
|
+
readonly sub?: Readonly<Record<string, number>>;
|
|
71
|
+
}
|
|
72
|
+
/** More sub-agent files than this in one firing: the map is left out (uncapped) rather than growing the line past `MAX_FIRING_BYTES`. */
|
|
73
|
+
export declare const MAX_FIRING_SUB_FILES = 200;
|
|
74
|
+
/** One firing as read back: where it starts and ends in the log, so a cursor can step over exactly it. */
|
|
75
|
+
export interface ReadFiring {
|
|
76
|
+
readonly firing: Firing;
|
|
77
|
+
readonly start: number;
|
|
78
|
+
readonly end: number;
|
|
79
|
+
}
|
|
80
|
+
export interface FiringRead {
|
|
81
|
+
/** Valid firings at or after the cursor, oldest first, up to the first line of a NEWER version. */
|
|
82
|
+
readonly firings: readonly ReadFiring[];
|
|
83
|
+
/**
|
|
84
|
+
* The offset after the last COMPLETE line this read judged (a malformed one included, a partial trailing one not, a newer-version one
|
|
85
|
+
* not). Advancing the cursor here steps over torn lines that no valid firing follows.
|
|
86
|
+
*/
|
|
87
|
+
readonly through: number;
|
|
88
|
+
/** True when a line of a newer `v` stopped the read. The worker leaves it unconsumed and exits (a newer build owns it). */
|
|
89
|
+
readonly blockedByNewer: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Append one firing. Returns false on any failure — a hook must never throw, and a missed firing self-heals: the NEXT firing's `eof`
|
|
93
|
+
* covers the same bytes.
|
|
94
|
+
*
|
|
95
|
+
* ⛔ ONE `O_APPEND` WRITE. Two hooks (Cursor double-fires) interleave whole lines, never halves, so no lock is needed. A line over
|
|
96
|
+
* `MAX_FIRING_BYTES` is refused rather than split.
|
|
97
|
+
*/
|
|
98
|
+
export declare function appendFiring(home: string, sessionId: string, firing: Firing): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* What the hook calls: `appendFiring`, and if the line is refused while it carries a `sub` map, once more WITHOUT it (uncapped, the behaviour before
|
|
101
|
+
* the cap existed). The map is the one part of a line that grows with the session; the cap must never cost a firing. Never throws.
|
|
102
|
+
*/
|
|
103
|
+
export declare function recordFiring(home: string, sessionId: string, firing: Firing): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* The firings from `from` (a cursor offset) on. Never throws: an unreadable log reads as empty.
|
|
106
|
+
*
|
|
107
|
+
* A malformed COMPLETE line is skipped and counted in `through`. A trailing line with no newline is a hook mid-append and is left alone.
|
|
108
|
+
* A line of a newer `v` stops the read and is not consumed (`blockedByNewer`).
|
|
109
|
+
*/
|
|
110
|
+
export declare function readFirings(home: string, sessionId: string, from?: number): FiringRead;
|
|
111
|
+
/**
|
|
112
|
+
* Every session that has a firing log: the file stems, oldest-modified first (so a session that has waited longest is served first). A
|
|
113
|
+
* stem is `sanitise(sid)`; it names the log and cursor files and nothing else, because the id to SEND is `Firing.sid`. Never throws.
|
|
114
|
+
*/
|
|
115
|
+
export declare function firingSessions(home: string): string[];
|
|
116
|
+
/** Forget a session's firing log and cursor (with its target record, on the existing 30-day schedule). Never throws. */
|
|
117
|
+
export declare function forgetFirings(home: string, sessionId: string): void;
|
|
118
|
+
export interface FiringCursor {
|
|
119
|
+
/** Byte offset of the first unconsumed firing. */
|
|
120
|
+
readonly offset: number;
|
|
121
|
+
/** Uncaught throws on the firing at `offset`. Reset whenever the cursor moves. */
|
|
122
|
+
readonly throws: number;
|
|
123
|
+
}
|
|
124
|
+
export declare const START_OF_LOG: FiringCursor;
|
|
125
|
+
/**
|
|
126
|
+
* The cursor, or the start of the log when it is missing or unreadable. Starting over is SAFE: replaying a consumed firing is a no-op
|
|
127
|
+
* (`firing.eof <= sentOffset`, trap 1), just slower.
|
|
128
|
+
*/
|
|
129
|
+
export declare function readCursor(home: string, sessionId: string): FiringCursor;
|
|
130
|
+
/**
|
|
131
|
+
* Persist the cursor atomically. ⛔ ONLY A HOLDER OF THE SESSION'S WRITER LOCK may call this, and only after a heartbeat that said the
|
|
132
|
+
* lock is still its own (§3.3: "every cursor write beats first"); a worker that lost its lock writes nothing. Returns false on failure.
|
|
133
|
+
*/
|
|
134
|
+
export declare function writeCursor(home: string, sessionId: string, cursor: FiringCursor): boolean;
|
|
135
|
+
/** Move the cursor to `offset`; the throw count belongs to the firing it left, so it resets. */
|
|
136
|
+
export declare const advanced: (cursor: FiringCursor, offset: number) => FiringCursor;
|
|
137
|
+
/** The firing at the cursor made the worker throw once more. */
|
|
138
|
+
export declare const threw: (cursor: FiringCursor) => FiringCursor;
|
|
139
|
+
/** Skip the firing at the cursor: it has thrown `MAX_FIRING_THROWS` times. */
|
|
140
|
+
export declare const shouldSkip: (cursor: FiringCursor) => boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Stamp "this session's hook ran at `atMs`" for one repo key. HOOK-ONLY: the worker runs `deliver` with `byHook: false` and never calls
|
|
143
|
+
* this. A JSON object (`{at}`) rather than a bare number so P2's T7 can add `transcript` without a format change; the reader ignores
|
|
144
|
+
* unknown fields. Atomic replace. Never throws; false on failure (the commit then falls to the cold rung or goes unrecorded, as today
|
|
145
|
+
* before a first hook).
|
|
146
|
+
*/
|
|
147
|
+
export declare function stampLive(home: string, key: SessionKey, atMs: number): boolean;
|
|
148
|
+
/** The `.live` stamp at `path`, or null when absent, unreadable or not a positive number. Unknown fields are ignored. */
|
|
149
|
+
export declare function readLiveFile(path: string): number | null;
|
|
150
|
+
/**
|
|
151
|
+
* ⛔ THE ONE LIVENESS READER (§3.5). A session is live as of the LATER of its state file's `liveAt` (stamped by `deliver()` for a hook
|
|
152
|
+
* delivery) and its `.live` file (stamped by the hook itself, before the worker has delivered anything). All three readers go through
|
|
153
|
+
* this: `liveSessions` and `pendingModeFor` (`spool.ts`) and the two `status` classifiers (`state.ts`). Read straight from the files,
|
|
154
|
+
* never from the mtime: a `finalize` that rewrites a dead session's state must not make it live (VG review 2, D2).
|
|
155
|
+
*/
|
|
156
|
+
export declare function liveAtOf(stateLiveAt: number | null, livePath: string | null): number | null;
|
|
157
|
+
/**
|
|
158
|
+
* Which repo keys a hook stamps `.live` for (§3.5).
|
|
159
|
+
* - the turn-end repo, when allowed;
|
|
160
|
+
* - for a dialect that ISOLATES its content records (Claude Code, Codex): also every ledger key in the session's target record;
|
|
161
|
+
* - for one that does not (Cursor): the turn-end repo ONLY. Sticky targets stay in Cursor's record but are not delivered, and only
|
|
162
|
+
* delivered targets stamp today. Stamping them would keep a repo Cursor has left live for 30 minutes past every turn, a human's commit
|
|
163
|
+
* there would become `sole_live_session`, and the row is permanent.
|
|
164
|
+
*/
|
|
165
|
+
export declare function liveStampKeys(input: {
|
|
166
|
+
readonly isolates: boolean;
|
|
167
|
+
readonly turnEnd: string | null;
|
|
168
|
+
readonly recordLedgerKeys: readonly string[];
|
|
169
|
+
}): string[];
|