@vibe-commit/capture 0.6.1 → 0.7.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 (143) hide show
  1. package/README.md +80 -5
  2. package/dist/agent_hooks.d.ts +8 -0
  3. package/dist/agent_hooks.js +31 -1
  4. package/dist/agent_hooks.js.map +1 -1
  5. package/dist/agents/claude_code.d.ts +34 -0
  6. package/dist/agents/claude_code.js +141 -8
  7. package/dist/agents/claude_code.js.map +1 -1
  8. package/dist/agents/codex_cli.d.ts +13 -0
  9. package/dist/agents/codex_cli.js +241 -2
  10. package/dist/agents/codex_cli.js.map +1 -1
  11. package/dist/agents/cursor.d.ts +34 -0
  12. package/dist/agents/cursor.js +115 -1
  13. package/dist/agents/cursor.js.map +1 -1
  14. package/dist/agents/session_listing.d.ts +29 -0
  15. package/dist/agents/session_listing.js +49 -0
  16. package/dist/agents/session_listing.js.map +1 -0
  17. package/dist/agents/types.d.ts +118 -8
  18. package/dist/agents/types.js +17 -3
  19. package/dist/agents/types.js.map +1 -1
  20. package/dist/commands/approval_pending.d.ts +9 -0
  21. package/dist/commands/approval_pending.js +23 -0
  22. package/dist/commands/approval_pending.js.map +1 -0
  23. package/dist/commands/auth.js +31 -11
  24. package/dist/commands/auth.js.map +1 -1
  25. package/dist/commands/backfill.d.ts +160 -0
  26. package/dist/commands/backfill.js +748 -0
  27. package/dist/commands/backfill.js.map +1 -0
  28. package/dist/commands/browser_credential.d.ts +2 -0
  29. package/dist/commands/browser_credential.js +31 -14
  30. package/dist/commands/browser_credential.js.map +1 -1
  31. package/dist/commands/connect.d.ts +137 -0
  32. package/dist/commands/connect.js +203 -109
  33. package/dist/commands/connect.js.map +1 -1
  34. package/dist/commands/connect_picker.d.ts +27 -0
  35. package/dist/commands/connect_picker.js +415 -0
  36. package/dist/commands/connect_picker.js.map +1 -0
  37. package/dist/commands/context.d.ts +16 -0
  38. package/dist/commands/context.js +27 -0
  39. package/dist/commands/context.js.map +1 -1
  40. package/dist/commands/credential_problem.d.ts +15 -4
  41. package/dist/commands/credential_problem.js +29 -22
  42. package/dist/commands/credential_problem.js.map +1 -1
  43. package/dist/commands/device_credential.d.ts +2 -0
  44. package/dist/commands/device_credential.js +16 -14
  45. package/dist/commands/device_credential.js.map +1 -1
  46. package/dist/commands/finalize.d.ts +95 -0
  47. package/dist/commands/finalize.js +249 -74
  48. package/dist/commands/finalize.js.map +1 -1
  49. package/dist/commands/finish_mint.d.ts +2 -6
  50. package/dist/commands/finish_mint.js +10 -2
  51. package/dist/commands/finish_mint.js.map +1 -1
  52. package/dist/commands/report.js +38 -30
  53. package/dist/commands/report.js.map +1 -1
  54. package/dist/commands/scan.d.ts +162 -0
  55. package/dist/commands/scan.js +507 -0
  56. package/dist/commands/scan.js.map +1 -0
  57. package/dist/commands/status.js +119 -22
  58. package/dist/commands/status.js.map +1 -1
  59. package/dist/commands/status_hooks.d.ts +14 -0
  60. package/dist/commands/status_hooks.js +81 -0
  61. package/dist/commands/status_hooks.js.map +1 -0
  62. package/dist/commands/status_machine.d.ts +18 -0
  63. package/dist/commands/status_machine.js +149 -0
  64. package/dist/commands/status_machine.js.map +1 -0
  65. package/dist/commands/why.js +44 -31
  66. package/dist/commands/why.js.map +1 -1
  67. package/dist/consent.d.ts +111 -0
  68. package/dist/consent.js +203 -1
  69. package/dist/consent.js.map +1 -1
  70. package/dist/copy/error_codes.d.ts +222 -0
  71. package/dist/copy/error_codes.js +105 -0
  72. package/dist/copy/error_codes.js.map +1 -0
  73. package/dist/copy/index.d.ts +2 -2
  74. package/dist/copy/index.js +2 -2
  75. package/dist/copy/index.js.map +1 -1
  76. package/dist/copy/strings.d.ts +497 -29
  77. package/dist/copy/strings.js +601 -35
  78. package/dist/copy/strings.js.map +1 -1
  79. package/dist/copy/time.d.ts +11 -0
  80. package/dist/copy/time.js +18 -0
  81. package/dist/copy/time.js.map +1 -1
  82. package/dist/credential.d.ts +5 -1
  83. package/dist/credential.js +2 -2
  84. package/dist/credential.js.map +1 -1
  85. package/dist/git.d.ts +43 -0
  86. package/dist/git.js +95 -0
  87. package/dist/git.js.map +1 -1
  88. package/dist/hooks/entry.d.ts +18 -1
  89. package/dist/hooks/entry.js +110 -13
  90. package/dist/hooks/entry.js.map +1 -1
  91. package/dist/hooks/post_commit.js +3 -3
  92. package/dist/hooks/post_commit.js.map +1 -1
  93. package/dist/hooks/post_rewrite.js +6 -4
  94. package/dist/hooks/post_rewrite.js.map +1 -1
  95. package/dist/hooks/session_start.d.ts +93 -0
  96. package/dist/hooks/session_start.js +199 -0
  97. package/dist/hooks/session_start.js.map +1 -0
  98. package/dist/index.d.ts +1 -1
  99. package/dist/index.js +56 -8
  100. package/dist/index.js.map +1 -1
  101. package/dist/install.d.ts +57 -0
  102. package/dist/install.js +54 -20
  103. package/dist/install.js.map +1 -1
  104. package/dist/json.d.ts +32 -9
  105. package/dist/json.js +35 -8
  106. package/dist/json.js.map +1 -1
  107. package/dist/machine_state.d.ts +52 -0
  108. package/dist/machine_state.js +63 -0
  109. package/dist/machine_state.js.map +1 -0
  110. package/dist/oauth/session.d.ts +1 -0
  111. package/dist/org_pending.d.ts +52 -0
  112. package/dist/org_pending.js +109 -0
  113. package/dist/org_pending.js.map +1 -0
  114. package/dist/paths.d.ts +50 -0
  115. package/dist/paths.js +64 -0
  116. package/dist/paths.js.map +1 -1
  117. package/dist/picker.d.ts +180 -0
  118. package/dist/picker.js +477 -0
  119. package/dist/picker.js.map +1 -0
  120. package/dist/picker_checklist.d.ts +125 -0
  121. package/dist/picker_checklist.js +375 -0
  122. package/dist/picker_checklist.js.map +1 -0
  123. package/dist/policy.d.ts +54 -0
  124. package/dist/policy.js +47 -0
  125. package/dist/policy.js.map +1 -1
  126. package/dist/post.d.ts +50 -16
  127. package/dist/post.js +220 -16
  128. package/dist/post.js.map +1 -1
  129. package/dist/state.d.ts +6 -0
  130. package/dist/state.js +38 -1
  131. package/dist/state.js.map +1 -1
  132. package/dist/system_message.d.ts +8 -1
  133. package/dist/system_message.js +19 -1
  134. package/dist/system_message.js.map +1 -1
  135. package/dist/term.d.ts +28 -2
  136. package/dist/term.js +23 -2
  137. package/dist/term.js.map +1 -1
  138. package/dist/verb_help.d.ts +13 -0
  139. package/dist/verb_help.js +40 -0
  140. package/dist/verb_help.js.map +1 -0
  141. package/dist/version.d.ts +1 -1
  142. package/dist/version.js +1 -1
  143. package/package.json +5 -9
package/dist/policy.js CHANGED
@@ -55,6 +55,11 @@ export const STATUS_CLASSES = new Map([
55
55
  // Org approval pending (D60 §D12). Capture has not stopped; it is waiting.
56
56
  [403, "later"],
57
57
  // Payload too large. Retrying the same bytes cannot work.
58
+ //
59
+ // ⚠ PR 4a: this row is now the verdict on a span that CANNOT BE SPLIT — a single record that alone exceeds a limit.
60
+ // `deliver()` halves a splittable span at a record boundary and sends the two halves, which are different bytes, so the
61
+ // reason above still holds for what reaches this table. Only a `deliver()` given no `RecordCutter` (a caller that
62
+ // cannot align) classifies a 413 here directly, and it is `never` there exactly as before.
58
63
  [413, "never"],
59
64
  // Rate limited. The definition of transient.
60
65
  [429, "later"],
@@ -150,6 +155,48 @@ function positiveOr(raw, fallback) {
150
155
  const parsed = Number(raw);
151
156
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
152
157
  }
158
+ // ---------------------------------------------------------------------------
159
+ // Delta sizing — PR 4a. One request carries one DELTA, never the whole span to EOF.
160
+ // ---------------------------------------------------------------------------
161
+ /**
162
+ * The most RAW transcript bytes one delta carries. The server's own ceiling is 16 MiB decompressed
163
+ * (`MAX_DELTA_BYTES`, `ingest_session.ts:146`); half of it leaves room for redaction markers and for a record that
164
+ * straddles the cut.
165
+ */
166
+ export const MAX_DELTA_RAW_BYTES = 8 * 1024 * 1024;
167
+ /**
168
+ * The most COMPRESSED bytes one delta's frame may be. The server refuses a request body over 1 MiB (`MAX_BODY_BYTES`,
169
+ * `src/transport/server.ts:115`, a 413); 960 KiB leaves the rest for the framing the body does not include.
170
+ */
171
+ export const MAX_DELTA_FRAME_BYTES = 960 * 1024;
172
+ /**
173
+ * Cut the next delta from `from`: everything to `eof` when it fits, else up to the last record boundary inside `maxRaw`.
174
+ *
175
+ * Pure; the cutter is the only thing that touches a file.
176
+ */
177
+ export function planChunk(cutter, from, eof, maxRaw = MAX_DELTA_RAW_BYTES) {
178
+ if (eof - from <= maxRaw)
179
+ return { kind: "span", to: eof };
180
+ const boundary = cutter.lastBoundary(from, from + maxRaw);
181
+ if (boundary !== null)
182
+ return { kind: "span", to: boundary };
183
+ const end = cutter.firstBoundary(from);
184
+ return end === null ? { kind: "wait" } : { kind: "oversize", to: end };
185
+ }
186
+ /**
187
+ * Where to cut `[from, to)` in two, at a record boundary, or `null` when the span is a single record and cannot be.
188
+ *
189
+ * Halves by bytes, snapping back to the last boundary at or before the middle. When the first record is longer than
190
+ * half, the cut is that record's end, so the oversize record goes out alone and a second 413 on it is final.
191
+ */
192
+ export function splitPoint(cutter, from, to) {
193
+ const middle = from + Math.floor((to - from) / 2);
194
+ const boundary = cutter.lastBoundary(from, middle);
195
+ if (boundary !== null)
196
+ return boundary;
197
+ const end = cutter.firstBoundary(from);
198
+ return end !== null && end < to ? end : null;
199
+ }
153
200
  /** Undelivered bytes currently held. */
154
201
  export function pendingBytes(state) {
155
202
  if (state.backlog.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
package/dist/post.d.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import { type SelectedAgentId } from "./agents/types.js";
21
21
  import type { IngestCredential } from "./credential.js";
22
- import { type Disposition } from "./policy.js";
22
+ import { type Disposition, type RecordCutter } from "./policy.js";
23
23
  import { type InferredGroupId, type Observed } from "./spool.js";
24
24
  /** Production data plane. Overridable for tests and for a self-hosted server. */
25
25
  export declare const DEFAULT_INGEST_URL = "https://api.vibecommit.ai/ingest/v1/session";
@@ -360,8 +360,29 @@ export type Delivery =
360
360
  readonly inferredSetAsidePairs: number;
361
361
  /** `X-Credential-Expires` from the 2xx that delivered this, or `null`/absent. See `SendOutcome`. */
362
362
  readonly credentialExpires?: string | null;
363
- /** `conversation_id` from that 2xx, ONLY when the caller opted in and `disposition === "ok"`. See `SendOutcome`. */
363
+ /**
364
+ * `conversation_id` from a 2xx, ONLY when the caller opted in. See `SendOutcome`.
365
+ *
366
+ * PR 4a (DX D8): a delivery of several deltas carries the id from whichever delta was acknowledged with it, even when
367
+ * a LATER delta came back `later`, so `connect`'s session link does not fall back to `/app/commits` on a partial run.
368
+ */
364
369
  readonly conversationId?: string;
370
+ /**
371
+ * PR 4a — records skipped because one record ALONE exceeded a limit (`record too large`): over `limits.rawBytes`
372
+ * raw, or over `limits.frameBytes` compressed, or refused with a 413 that no split can shrink. Each was written off
373
+ * with `markSkipped`, so it is in the gap counters `status` reports as "skipped … will not be sent later".
374
+ */
375
+ readonly recordsTooLarge?: number;
376
+ /**
377
+ * PR 5 — set when the last attempt got NO response at all (a refused connection, a timeout), so a caller can tell a server
378
+ * that could not be reached apart from one that answered and asked to wait. Absent whenever a response arrived. It
379
+ * changes no decision here: both are `later`.
380
+ */
381
+ readonly unreachable?: true;
382
+ /** PR 4a — deltas this call acknowledged or skipped past, when more than one. */
383
+ readonly deltas?: number;
384
+ /** PR 4a — `true` when the call stopped on the hook's budget with bytes still unsent. The offset is where it left off. */
385
+ readonly more?: boolean;
365
386
  };
366
387
  export interface DeliveryContext {
367
388
  readonly home: string;
@@ -444,23 +465,36 @@ export interface DeliveryContext {
444
465
  * reads, awaits or parses a 2xx body.
445
466
  */
446
467
  readonly readConversationId?: boolean;
468
+ /**
469
+ * PR 4a — WHEN THE HOOK'S SEND BUDGET ENDS, epoch ms. Set by the hook path only. `deliver()` starts a further delta (or a
470
+ * 413 split's retry) only while at least `MIN_NEXT_DELTA_BUDGET_MS` of it remains, and leaves the rest of the span for the
471
+ * next event or `vibecommit finalize`. Unset (`finalize`, `connect`) means no deadline.
472
+ */
473
+ readonly deadlineAtMs?: number;
474
+ /** PR 4a — the clock `deadlineAtMs` is measured on. A test seam; defaults to `Date.now`. */
475
+ readonly clock?: () => number;
476
+ /** PR 4a — the delta size limits. A test seam; defaults to the server-derived constants in `policy.ts`. */
477
+ readonly limits?: {
478
+ readonly rawBytes: number;
479
+ readonly frameBytes: number;
480
+ };
447
481
  }
448
482
  /**
449
- * Send the undelivered span of one transcript file and apply the failure policy.
483
+ * Send the undelivered span of one transcript file, in deltas — PR 4a.
450
484
  *
451
- * **At most one NETWORK attempt per hook, still — exactly D56 §D8's rule.** A
452
- * `finalize` lean retry (below) is the one exception D56 §D8 itself does not
453
- * cover: it governs HOOKS, and `finalize` is a synchronous, user-invoked verb
454
- * that already makes 1 + up to `FINALIZE_DRAIN_MAX_ITERATIONS` requests by
455
- * design (VL's explicit ruling, `TODOS[182]`). On the hook path, `later` still
456
- * does not retry here; it leaves the offset where it is so the hook that fires
457
- * on the NEXT turn picks the same bytes up.
485
+ * Without a `cutter` this is the single request it always was: everything from the offset to EOF, at most one attempt.
486
+ * With one (every production caller), the span goes out as record-aligned deltas, each acknowledged before the offset
487
+ * moves past it:
458
488
  *
459
- * Never throws: the caller is a hook, and an exception on this path is the
460
- * contract breaking. Every failure resolves to a `Delivery`.
489
+ * - **A hook sends deltas only while its budget lasts** (`ctx.deadlineAtMs`) and leaves the rest for its next event or
490
+ * `vibecommit finalize`; `Delivery.more` says it stopped early. `finalize` and `connect` set no deadline and run on.
491
+ * - **It stops at the first delta that is not `ok`** and returns that delta's outcome: `later` and `fatal` hold exactly
492
+ * the bytes that failed, so a resume starts at the next unacknowledged delta, with no resend and no gap. A
493
+ * `capture_not_approved` 403 on any delta is such a stop, so its pending beat is printed once.
494
+ * - **A skipped oversize record does not stop it.** The records after it still go out.
495
+ * - **`connect`'s conversation id survives a partial run** (DX D8): it is read from the first 2xx that carries it, no later
496
+ * delta reads a body, and it is returned even when a later delta came back `later`.
461
497
  *
462
- * `readBody` is supplied by the caller rather than read here, because the caller
463
- * is the one that has confined `transcript_path` to the Claude Code root
464
- * (`/cso` finding 1). This module never opens a file.
498
+ * ⛔ A 2xx never changes a delta's outcome, and a hook reads no body (eng-2 D2): the only body read is `connect`'s opt-in.
465
499
  */
466
- export declare function deliver(ctx: DeliveryContext, eof: number, readBody: (from: number, to: number) => Uint8Array | null): Promise<Delivery>;
500
+ export declare function deliver(ctx: DeliveryContext, eof: number, readBody: (from: number, to: number) => Uint8Array | null, cutter?: RecordCutter): Promise<Delivery>;
package/dist/post.js CHANGED
@@ -22,7 +22,8 @@ import { agentForTranscript } from "./agents/registry.js";
22
22
  import { UNKNOWN_AGENT_ID } from "./agents/types.js";
23
23
  import { headerSafe, sessionIdSafe } from "./header_safety.js";
24
24
  import { recordInferredSkipped } from "./inferred_tally.js";
25
- import { classify, markDelivered, markHeld, markSkipped, nextSpan, resolveCaps, } from "./policy.js";
25
+ import { clearOrgPending, markOrgPending } from "./org_pending.js";
26
+ import { MAX_DELTA_FRAME_BYTES, MAX_DELTA_RAW_BYTES, classify, markDelivered, markHeld, markSkipped, nextSpan, planChunk, resolveCaps, splitPoint, } from "./policy.js";
26
27
  import { fileState, isStopped, loadSessionState, saveSessionState, withFileState, } from "./state.js";
27
28
  import { INFERRED_SET_ASIDE_AFTER, dropInferredGroups, dropRewrites, dropSpooled, isFullSha, recordInferredAttempt, setAsideInferredGroup, settleSpooled, } from "./spool.js";
28
29
  import { CLIENT_VERSION, CLIENT_VERSION_HEADER } from "./version.js";
@@ -498,6 +499,15 @@ function parseFinalAck(raw) {
498
499
  const v = raw?.trim().toLowerCase();
499
500
  return v === "sealed" || v === "already" || v === "refused" ? v : null;
500
501
  }
502
+ /**
503
+ * PR 4a — below this much remaining hook budget, do not START another delta. The same floor the sub-agent loop uses
504
+ * (`MIN_SUBAGENT_BUDGET_MS`, `hooks/entry.ts`), for the same reason: a request that small can only time out.
505
+ */
506
+ const MIN_NEXT_DELTA_BUDGET_MS = 400;
507
+ /** The floor under one attempt's timeout once the hook budget is nearly or fully spent — `MIN_SEND_TIMEOUT_MS` in `hooks/entry.ts`. */
508
+ const MIN_ATTEMPT_TIMEOUT_MS = 50;
509
+ /** The share of what is left one further attempt may take — `SEND_BUDGET_FRACTION`'s value in `hooks/entry.ts`. */
510
+ const NEXT_DELTA_BUDGET_FRACTION = 0.6;
501
511
  /**
502
512
  * Build the delta, reduce it to the deny-list set when `lean`, validate every
503
513
  * resulting header value, and only then call `send()` — `TODOS[182]`.
@@ -508,7 +518,12 @@ function parseFinalAck(raw) {
508
518
  * by elimination, always names a REQUIRED header — there is no second
509
519
  * optional one for the caller to drop.
510
520
  */
511
- async function attemptSend(ctx, agent, seq, span, body, lean) {
521
+ async function attemptSend(ctx, agent, seq, span, body, lean,
522
+ // PR 4a. ⛔ ONLY THE DELTA THAT ENDS AT EOF CARRIES THE SESSION-LEVEL HEADERS. `X-Session-Final` seals the open turn, and
523
+ // sealing before the rest of the span has arrived would seal a poorer turn (`finalize.ts`'s docblock). The commits,
524
+ // rewrites and head describe the END of the session, and `capture_commits`' PK admits one row per (capture, sha), so they
525
+ // ride on one delta and not on every one. A delta that is not the last sends none of them, and the spool keeps them.
526
+ isLast = true) {
512
527
  const full = buildIngestHeaders(ctx.credential, {
513
528
  sessionId: ctx.sessionId,
514
529
  seq,
@@ -516,11 +531,11 @@ async function attemptSend(ctx, agent, seq, span, body, lean) {
516
531
  fileKey: ctx.fileKey,
517
532
  repoSlug: ctx.repoSlug,
518
533
  agent,
519
- final: ctx.final === true,
520
- head: ctx.head,
521
- commits: ctx.commits,
522
- rewrites: ctx.rewrites,
523
- inferredRewrites: ctx.inferredRewrites,
534
+ final: ctx.final === true && isLast,
535
+ head: isLast ? ctx.head : undefined,
536
+ commits: isLast ? ctx.commits : undefined,
537
+ rewrites: isLast ? ctx.rewrites : undefined,
538
+ inferredRewrites: isLast ? ctx.inferredRewrites : undefined,
524
539
  });
525
540
  const headers = lean ? leanHeaders(full) : full;
526
541
  const badHeader = firstUnsafeHeader(headers);
@@ -593,8 +608,10 @@ function holdLean(ctx, key, session, current, span, caps) {
593
608
  /**
594
609
  * Send the undelivered span of one transcript file and apply the failure policy.
595
610
  *
596
- * **At most one NETWORK attempt per hook, still — exactly D56 §D8's rule.** A
597
- * `finalize` lean retry (below) is the one exception D56 §D8 itself does not
611
+ * **At most one NETWORK attempt per hook, still — exactly D56 §D8's rule — FOR ONE DELTA.** PR 4a:
612
+ * `deliver()` (below) calls this once per delta, and a hook calls it again only while its budget lasts; a 413 split's
613
+ * retry is budget-gated the same way and is a DIFFERENT, smaller body, so it is not the retry of the same bytes §D8
614
+ * forbids. A `finalize` lean retry (below) is the one exception D56 §D8 itself does not
598
615
  * cover: it governs HOOKS, and `finalize` is a synchronous, user-invoked verb
599
616
  * that already makes 1 + up to `FINALIZE_DRAIN_MAX_ITERATIONS` requests by
600
617
  * design (VL's explicit ruling, `TODOS[182]`). On the hook path, `later` still
@@ -608,7 +625,7 @@ function holdLean(ctx, key, session, current, span, caps) {
608
625
  * is the one that has confined `transcript_path` to the Claude Code root
609
626
  * (`/cso` finding 1). This module never opens a file.
610
627
  */
611
- export async function deliver(ctx, eof, readBody) {
628
+ async function deliverChunk(ctx, eof, readBody, cutter) {
612
629
  // `CR-017d`. The consent gate already refuses a `cwd` outside a work tree, so
613
630
  // the hook cannot reach this — but the ledger's isolation must not rest on a
614
631
  // check in another module, the same discipline `consent.ts` states about its
@@ -630,10 +647,41 @@ export async function deliver(ctx, eof, readBody) {
630
647
  }
631
648
  if (span === null)
632
649
  return { kind: "nothing-to-send" };
633
- const body = readBody(span.from, span.to);
650
+ const caps = resolveCaps(ctx.env);
651
+ const limits = ctx.limits ?? { rawBytes: MAX_DELTA_RAW_BYTES, frameBytes: MAX_DELTA_FRAME_BYTES };
652
+ // ⭐ PR 4a — ONE DELTA, NOT THE WHOLE SPAN TO EOF. Without a `cutter` this is the old single-span path, byte for byte.
653
+ //
654
+ // The delta is cut on a record boundary (the server drops a trailing partial line), at most `limits.rawBytes` raw, and
655
+ // measured AFTER compression: a frame over `limits.frameBytes` is halved at a boundary and measured again. A record that
656
+ // alone exceeds either limit cannot be split, so it is skipped and COUNTED (`markSkipped`: the gap counters `status`
657
+ // reports as "skipped … will not be sent later"), never retried — retrying the same bytes cannot work.
658
+ //
659
+ // A cut that is NOT at EOF is not the last delta (`isLast`), so it carries none of the session-level headers.
660
+ if (cutter !== undefined && span.to > span.from) {
661
+ const plan = planChunk(cutter, span.from, span.to, limits.rawBytes);
662
+ // An unterminated record longer than the limit, still at EOF: it may be growing. Leave it for the next event.
663
+ if (plan.kind === "wait")
664
+ return { kind: "nothing-to-send" };
665
+ if (plan.kind === "oversize")
666
+ return recordTooLarge(ctx, key, session, current, { from: span.from, to: plan.to }, caps);
667
+ span = { from: span.from, to: plan.to };
668
+ }
669
+ let body = readBody(span.from, span.to);
634
670
  if (body === null)
635
671
  return { kind: "nothing-to-send" };
636
- const caps = resolveCaps(ctx.env);
672
+ if (cutter !== undefined) {
673
+ while (body.length > limits.frameBytes) {
674
+ const at = splitPoint(cutter, span.from, span.to);
675
+ if (at === null)
676
+ return recordTooLarge(ctx, key, session, current, span, caps);
677
+ span = { from: span.from, to: at };
678
+ body = readBody(span.from, span.to);
679
+ if (body === null)
680
+ return { kind: "nothing-to-send" };
681
+ }
682
+ }
683
+ // An empty span (a `final` with nothing new) is the last delta by definition.
684
+ let isLast = cutter === undefined || span.to >= eof || span.to === span.from;
637
685
  // `seq` is claimed BEFORE the attempt and persisted by the SWITCH below (every ordinary disposition —
638
686
  // "ok"/"later"/"never"/"fatal" — builds `next` from `{ ...session, seq, … }`), so it is monotonic per
639
687
  // session across retries once a request was actually classified. A seq reused after a failure would read
@@ -673,7 +721,7 @@ export async function deliver(ctx, eof, readBody) {
673
721
  // full and lean requests depending on what the NEXT turn happens to contain,
674
722
  // which nobody can debug.
675
723
  let lean = current.lean;
676
- let attempt = await attemptSend(ctx, agent, seq, span, body, lean);
724
+ let attempt = await attemptSend(withinBudget(ctx), agent, seq, span, body, lean, isLast);
677
725
  if (attempt.kind === "unsafe") {
678
726
  return refuse(ctx, key, session, current, span, caps, attempt.header === "authorization" ? "fatal" : "never", lean);
679
727
  }
@@ -688,7 +736,7 @@ export async function deliver(ctx, eof, readBody) {
688
736
  if (outcome.kind === "response" && outcome.status === 431) {
689
737
  if (lean) {
690
738
  // Already lean and STILL 431: nothing client-side can shrink this
691
- // further. `never`, exactly as 413 — the one case `never` is correct
739
+ // further. `never`, exactly as an UNSPLITTABLE 413 (a single record) — the one case `never` is correct
692
740
  // here, because it really is per-body now.
693
741
  return refuse(ctx, key, session, current, span, caps, "never", true);
694
742
  }
@@ -700,7 +748,7 @@ export async function deliver(ctx, eof, readBody) {
700
748
  // in-process retry here does not reopen D56 §D8, which governs hooks. Go
701
749
  // lean and retry ONCE, within this same invocation.
702
750
  lean = true;
703
- attempt = await attemptSend(ctx, agent, seq, span, body, lean);
751
+ attempt = await attemptSend(withinBudget(ctx), agent, seq, span, body, lean, isLast);
704
752
  if (attempt.kind === "unsafe") {
705
753
  // Unreachable in practice — every lean header is already proven safe, or
706
754
  // it would never have reached the full build either — but stated rather
@@ -718,6 +766,35 @@ export async function deliver(ctx, eof, readBody) {
718
766
  // true — the `"ok"` case's accounting reads it to skip settling data this
719
767
  // attempt never put on the wire.
720
768
  }
769
+ // ⭐ PR 4a — A 413 IS NO LONGER A WRITE-OFF WHEN THE SPAN CAN BE SPLIT. Halve it at a record boundary and send the first half
770
+ // as a different, smaller request. A 413 is refused before the server applies anything (`server.ts`'s body limit, then the
771
+ // decompressed-size check), so the SAME `seq` is reused: the server never recorded it, and it is not a replay.
772
+ //
773
+ // ⛔ Only a span that is a single record falls through to `classify`, where 413 is `never`: the bytes cannot be made
774
+ // smaller, so skipping them is correct and the record is counted. Everything else here sends bytes the 413 did not see.
775
+ //
776
+ // Each retry is gated on the hook's remaining budget (`budgetLeftMs`), so this is not D56 §D8's forbidden synchronous
777
+ // retry of the SAME bytes: it is bounded by the budget the hook already has, and out of budget it HOLDS the span.
778
+ let splitTooLarge = false;
779
+ while (cutter !== undefined && outcome.kind === "response" && outcome.status === 413) {
780
+ const at = splitPoint(cutter, span.from, span.to);
781
+ if (at === null) {
782
+ splitTooLarge = true;
783
+ break;
784
+ }
785
+ if (budgetLeftMs(ctx) < MIN_NEXT_DELTA_BUDGET_MS)
786
+ return holdSpan(ctx, key, session, current, span, caps, lean);
787
+ span = { from: span.from, to: at };
788
+ isLast = false;
789
+ const half = readBody(span.from, span.to);
790
+ if (half === null)
791
+ return { kind: "nothing-to-send" };
792
+ attempt = await attemptSend(withinBudget(ctx), agent, seq, span, half, lean, false);
793
+ if (attempt.kind === "unsafe") {
794
+ return refuse(ctx, key, session, current, span, caps, attempt.header === "authorization" ? "fatal" : "never", lean);
795
+ }
796
+ outcome = attempt.outcome;
797
+ }
721
798
  const disposition = classify(outcome);
722
799
  let next = ctx.byHook ? { ...session, seq, liveAt: ctx.nowMs } : { ...session, seq };
723
800
  let inferredSetAsidePairs = 0;
@@ -797,7 +874,9 @@ export async function deliver(ctx, eof, readBody) {
797
874
  // sendable, for a reason unrelated to `lean`) entries are UNAFFECTED by this gate: they are consumed
798
875
  // only via a NORMAL, non-lean call's `capSpool`/`capSuccessors`/`capInferred`, which this gate does not
799
876
  // touch, so a lean session's held lines still drain exactly as soon as a non-lean request reads them.
800
- if (!lean) {
877
+ // PR 4a: AND ONLY ON THE DELTA THAT CARRIED THEM (`isLast`). A delta earlier in the span sent none of the commits,
878
+ // rewrites or inferred pairs, so a 2xx for it says nothing about them and the spool must keep them for the delta that did.
879
+ if (!lean && isLast) {
801
880
  const retry = outcome.kind === "response" ? outcome.commitsRetry : null;
802
881
  const nothingLanded = outcome.kind === "response" && outcome.captureId === "";
803
882
  const holdForNextDelta = nothingLanded && (ctx.commits?.shas.length ?? 0) > 0;
@@ -880,6 +959,14 @@ export async function deliver(ctx, eof, readBody) {
880
959
  break;
881
960
  }
882
961
  saveSessionState(ctx.home, key, next);
962
+ // 0.7.0: remember, for `status`, that THIS repo's upload was refused because its org has not approved capture — and
963
+ // forget it on the next delivery. Only that one error code sets it; a 5xx or a timeout neither sets nor clears it.
964
+ if (disposition === "ok")
965
+ clearOrgPending(ctx.home, ctx.repoSlug);
966
+ else if (disposition === "later" && outcome.kind === "response" && outcome.detail?.code === CAPTURE_NOT_APPROVED) {
967
+ // The slug is the CLIENT's (`ctx.repoSlug`), never the response body's: the 403 names nobody.
968
+ markOrgPending(ctx.home, ctx.nowMs, outcome.detail.ownersNotified ?? null, ctx.repoSlug);
969
+ }
883
970
  // `outcome.detail` is null on every path except a non-2xx with a parseable
884
971
  // error body, so this is a pass-through and not a second decision.
885
972
  return {
@@ -894,6 +981,123 @@ export async function deliver(ctx, eof, readBody) {
894
981
  ...(outcome.kind === "response" && disposition === "ok" && outcome.conversationId !== undefined
895
982
  ? { conversationId: outcome.conversationId }
896
983
  : {}),
984
+ ...(outcome.kind === "unreachable" ? { unreachable: true } : {}),
985
+ // PR 4a: a 413 that no split could shrink was skipped by the `never` arm above; say so, so the caller can count it.
986
+ ...(splitTooLarge && disposition === "never" ? { recordsTooLarge: 1 } : {}),
897
987
  };
898
988
  }
989
+ /**
990
+ * PR 4a — a single record that alone exceeds a delta limit. Nothing is sent: the same bytes would be refused, so the record
991
+ * is skipped, the gap is stamped (`markSkipped`, which `status` reports), and the offset moves past it. `refuse` does
992
+ * exactly that and persists it; this adds the count.
993
+ */
994
+ function recordTooLarge(ctx, key, session, current, span, caps) {
995
+ const skipped = refuse(ctx, key, session, current, span, caps, "never", current.lean);
996
+ return skipped.kind === "attempted" ? { ...skipped, recordsTooLarge: 1 } : skipped;
997
+ }
998
+ /**
999
+ * PR 4a — out of hook budget in the middle of a 413 split: hold the span the failed request covered, as a `later` does. The
1000
+ * `seq` is not persisted for the same reason the split reuses it: the 413 was refused before the server applied it.
1001
+ */
1002
+ function holdSpan(ctx, key, session, current, span, caps, lean) {
1003
+ saveSessionState(ctx.home, key, withFileState(session, ctx.fileKey, { ...markHeld(current, span, ctx.nowMs, caps), lean }));
1004
+ return { kind: "attempted", disposition: "later", detail: null, finalAck: null, inferredStored: null, inferredSetAsidePairs: 0 };
1005
+ }
1006
+ /** Hook budget left for further sends, ms. `Infinity` when the caller set no deadline (`finalize`, `connect`). */
1007
+ function budgetLeftMs(ctx) {
1008
+ return ctx.deadlineAtMs === undefined ? Infinity : ctx.deadlineAtMs - (ctx.clock ?? Date.now)();
1009
+ }
1010
+ /**
1011
+ * `ctx` with this attempt's timeout trimmed to what is left of the hook budget (unchanged when there is no deadline).
1012
+ *
1013
+ * ⛔ CALLED RIGHT BEFORE EACH `attemptSend`, never ahead of the read. The read, the redaction and the compression of a delta
1014
+ * (and every halving pass) take real time, and a timeout fixed before them lets the last delta end past `deadlineAtMs` by
1015
+ * exactly that. Floored like `sendTimeoutMs` (`hooks/entry.ts`): when the budget is already gone the watchdog is the real
1016
+ * limit, and a zero timeout would only abort a send that was about to be refused anyway.
1017
+ */
1018
+ function withinBudget(ctx) {
1019
+ const left = budgetLeftMs(ctx);
1020
+ if (!Number.isFinite(left))
1021
+ return ctx;
1022
+ return { ...ctx, timeoutMs: Math.max(MIN_ATTEMPT_TIMEOUT_MS, Math.min(ctx.timeoutMs, Math.floor(left * NEXT_DELTA_BUDGET_FRACTION))) };
1023
+ }
1024
+ /**
1025
+ * Send the undelivered span of one transcript file, in deltas — PR 4a.
1026
+ *
1027
+ * Without a `cutter` this is the single request it always was: everything from the offset to EOF, at most one attempt.
1028
+ * With one (every production caller), the span goes out as record-aligned deltas, each acknowledged before the offset
1029
+ * moves past it:
1030
+ *
1031
+ * - **A hook sends deltas only while its budget lasts** (`ctx.deadlineAtMs`) and leaves the rest for its next event or
1032
+ * `vibecommit finalize`; `Delivery.more` says it stopped early. `finalize` and `connect` set no deadline and run on.
1033
+ * - **It stops at the first delta that is not `ok`** and returns that delta's outcome: `later` and `fatal` hold exactly
1034
+ * the bytes that failed, so a resume starts at the next unacknowledged delta, with no resend and no gap. A
1035
+ * `capture_not_approved` 403 on any delta is such a stop, so its pending beat is printed once.
1036
+ * - **A skipped oversize record does not stop it.** The records after it still go out.
1037
+ * - **`connect`'s conversation id survives a partial run** (DX D8): it is read from the first 2xx that carries it, no later
1038
+ * delta reads a body, and it is returned even when a later delta came back `later`.
1039
+ *
1040
+ * ⛔ A 2xx never changes a delta's outcome, and a hook reads no body (eng-2 D2): the only body read is `connect`'s opt-in.
1041
+ */
1042
+ export async function deliver(ctx, eof, readBody, cutter) {
1043
+ if (cutter === undefined)
1044
+ return deliverChunk(ctx, eof, readBody, undefined);
1045
+ const key = { repoKey: ctx.repoKey, sessionId: ctx.sessionId };
1046
+ let chunkCtx = ctx;
1047
+ let conversationId;
1048
+ let tooLarge = 0;
1049
+ let deltas = 0;
1050
+ let last = null;
1051
+ const carry = (d, more = false) => d.kind !== "attempted"
1052
+ ? d
1053
+ : {
1054
+ ...d,
1055
+ ...(d.conversationId === undefined && conversationId !== undefined ? { conversationId } : {}),
1056
+ ...(tooLarge > 0 ? { recordsTooLarge: tooLarge } : {}),
1057
+ ...(deltas > 1 ? { deltas } : {}),
1058
+ ...(more ? { more: true } : {}),
1059
+ };
1060
+ for (;;) {
1061
+ const before = fileState(loadSessionState(ctx.home, key), ctx.fileKey).sentOffset;
1062
+ let delivery;
1063
+ try {
1064
+ delivery = await deliverChunk(chunkCtx, eof, readBody, cutter);
1065
+ }
1066
+ catch {
1067
+ // The cutter reads the file and a read can fail. That must read as "could not tell where the record ends", never as
1068
+ // "it does not end" (which would skip a record). Nothing was persisted for this delta, so the next event retries it.
1069
+ delivery = { kind: "nothing-to-send" };
1070
+ }
1071
+ if (delivery.kind !== "attempted")
1072
+ return last === null ? delivery : carry(last);
1073
+ deltas += 1;
1074
+ tooLarge += delivery.recordsTooLarge ?? 0;
1075
+ conversationId ??= delivery.conversationId;
1076
+ // `ok`, or a record skipped as too large, moved the offset on; anything else is a stop, and its outcome is the answer.
1077
+ if (delivery.disposition !== "ok" && delivery.recordsTooLarge === undefined)
1078
+ return carry(delivery);
1079
+ const after = fileState(loadSessionState(ctx.home, key), ctx.fileKey).sentOffset;
1080
+ // No progress is a bug somewhere below; end the loop rather than spin on it.
1081
+ if (after <= before)
1082
+ return carry(delivery);
1083
+ // ⛔ EVERY BYTE IS ACCOUNTED FOR, BUT THE LAST DELTA WAS SKIPPED RATHER THAN SENT, AND THE SESSION-LEVEL HEADERS RODE
1084
+ // ON IT. A skipped record (too large by the raw limit, the frame limit, or a 413 no split can shrink) puts nothing on
1085
+ // the wire, so the `final` assertion, the commits, the head and the rewrites that belong to the delta ending at EOF
1086
+ // never landed. Run ONE more pass: `nextSpan` is null at EOF, so the existing `span === null && final` branch of
1087
+ // `deliverChunk` sends the empty delta that carries them (`isLast` is true for an empty span). Without this a
1088
+ // `finalize` whose last record was oversize reported `notDelivered` after every byte had been sent or written off.
1089
+ // Hooks never set `final`, and their commits ride the next event as they always did.
1090
+ const tailSkipped = delivery.recordsTooLarge !== undefined && after >= eof && ctx.final === true;
1091
+ if (after >= eof && !tailSkipped)
1092
+ return carry(delivery);
1093
+ if (budgetLeftMs(ctx) < MIN_NEXT_DELTA_BUDGET_MS)
1094
+ return carry(delivery, true);
1095
+ last = delivery;
1096
+ chunkCtx = {
1097
+ ...ctx,
1098
+ // Stop reading bodies once the id is in hand: a later delta never needs one.
1099
+ readConversationId: ctx.readConversationId === true && conversationId === undefined,
1100
+ };
1101
+ }
1102
+ }
899
1103
  //# sourceMappingURL=post.js.map