@canonmsg/claude-code-plugin 0.28.0 → 0.29.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.
@@ -1,10 +1,12 @@
1
1
  import type { PermissionMode, SDKMessageOrigin, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
- import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
2
+ import type { DeliveryIntent, ModelOption, TurnLifecycleState, TurnOutputBlock } from '@canonmsg/core';
3
3
  import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
4
4
  export type ClaudeInputKind = 'seed' | 'canon';
5
5
  export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
6
6
  export interface ClaudeInputEnvelope {
7
7
  kind: ClaudeInputKind;
8
+ /** Correlates this envelope with its turn result's `user_message_uuid`. */
9
+ messageUuid: string;
8
10
  msg: SDKUserMessage;
9
11
  intent: DeliveryIntent;
10
12
  sourceMessageId: string | null;
@@ -27,20 +29,52 @@ export interface ClaudePendingFinalDelivery {
27
29
  retryCount: number;
28
30
  /** Carried so a retried failure notice keeps suppressing agent auto-replies. */
29
31
  suppressAutoReply?: boolean;
32
+ /**
33
+ * Ids of the chunked parts already durable in Canon, in part order — the
34
+ * resume cursor. Without it a retry re-ran the whole sequence from part 1,
35
+ * which the server answers with a 409 on every part it already holds, so a
36
+ * long answer could never get its tail delivered.
37
+ */
38
+ deliveredMessageIds?: string[];
39
+ /**
40
+ * The metadata built for the FIRST attempt, replayed byte-for-byte by every
41
+ * retry. The server's idempotency fingerprint spans `metadata`, so a rebuilt
42
+ * object differing by one trail block turns a harmless replay into a 409.
43
+ * Caching removes the determinism assumption instead of relying on it.
44
+ */
45
+ metadata?: Record<string, unknown>;
46
+ /**
47
+ * The self-context the FIRST attempt sent under, replayed by every retry for
48
+ * the same reason as the metadata: `selfContextId` is part of the server's
49
+ * idempotency fingerprint, and the session's live value moves with every
50
+ * inbound message — including ones that only queue behind this delivery. Left
51
+ * to drift, a resumed tail lands in a different self-context from its own
52
+ * head, and a replayed part diverges into a 409.
53
+ */
54
+ selfContextId?: string | null;
55
+ /**
56
+ * The failure that put this delivery in retry, already bounded for quoting.
57
+ * Read when retries run out with parts already delivered, so the stops-short
58
+ * notice can say what went wrong.
59
+ */
60
+ lastErrorMessage?: string;
30
61
  }
62
+ export type ClaudeSessionRunState = 'idle' | 'running' | 'requires_action';
31
63
  export interface ClaudeCompletedTurnState {
32
64
  state: {
33
- state?: 'idle' | 'running' | 'requires_action';
65
+ state?: ClaudeSessionRunState;
34
66
  };
35
67
  pendingFinalText: string | null;
36
68
  pendingFinalDelivery: ClaudePendingFinalDelivery | null;
37
69
  activeInput: ClaudeInputEnvelope | null;
70
+ dispatchingInput?: ClaudeInputEnvelope | null;
38
71
  toolInProgress: boolean;
39
72
  turnState: TurnLifecycleState;
40
73
  currentTurnId: string | null;
41
74
  currentTurnOpenedAt: number | null;
42
75
  currentTurnUpdatedAt: number | null;
43
76
  lastAcceptedIntent: DeliveryIntent | null;
77
+ turnActivity?: ClaudeTurnActivityState;
44
78
  }
45
79
  export declare function createClaudeInputEnvelope(input: {
46
80
  kind: ClaudeInputKind;
@@ -68,7 +102,344 @@ export declare function shouldDeliverClaudeFinal(input: {
68
102
  finalizedTurnKeys: ReadonlySet<string>;
69
103
  interruptedTurnKeys: ReadonlySet<string>;
70
104
  }): boolean;
105
+ export declare function rememberDispatchedClaudeInput(dispatched: Map<string, ClaudeInputEnvelope>, input: ClaudeInputEnvelope): void;
106
+ /**
107
+ * The envelope a turn result belongs to, matched by `user_message_uuid`.
108
+ *
109
+ * The SDK coalesces queued inputs — three rapid messages can produce two turns —
110
+ * and reports the LAST message of a coalesced batch, which is the correct owner
111
+ * of the reply. Entries up to and including the match are consumed, so messages
112
+ * folded into that batch do not linger. Returns null when the result carries no
113
+ * usable uuid, leaving the caller to fall back.
114
+ */
115
+ export declare function takeClaudeResultOwner(dispatched: Map<string, ClaudeInputEnvelope>, userMessageUuid: unknown): ClaudeInputEnvelope | null;
116
+ /**
117
+ * Whether an envelope owns the session's active-turn slot. Only Canon turns do;
118
+ * any future internal envelope must remain invisible to user-facing turn state.
119
+ */
120
+ export declare function claudeInputOwnsTurnSlot(input: Pick<ClaudeInputEnvelope, 'kind'> | null | undefined): boolean;
121
+ /** Why a non-empty final was gated instead of sent. Diagnostics only. */
122
+ export declare function describeUndeliveredClaudeFinal(input: {
123
+ turn: ClaudeInputEnvelope | null;
124
+ finalizedTurnKeys: ReadonlySet<string>;
125
+ interruptedTurnKeys: ReadonlySet<string>;
126
+ }): string;
71
127
  export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
128
+ export interface ClaudeTurnSlotState {
129
+ state: {
130
+ state?: ClaudeSessionRunState;
131
+ };
132
+ }
133
+ /**
134
+ * Whether the session already owns the turn slot.
135
+ *
136
+ * 'running' covers both a turn the SDK is working on and one still being
137
+ * dispatched — the distinction matters only to an interrupt (below), never to
138
+ * whether the session is free to start something else. Deliberately NOT a new
139
+ * published state: `session.state.state` is the host's internal busy flag (the
140
+ * published session snapshot has no such field), and the user-facing signal is
141
+ * the turn state, which `openTurn` already publishes as 'thinking' at dequeue.
142
+ *
143
+ * 'requires_action' counts too: an approval or a question waiting on a human is
144
+ * a turn that is very much alive, holding the turn id and the live node. Reading
145
+ * it as free let a message arriving during the prompt open a SECOND turn over
146
+ * the waiting one — the same collision the reservation exists to stop, and
147
+ * inconsistent with every interrupt path, which already treats it as active.
148
+ */
149
+ export declare function isClaudeTurnSlotReserved(state: ClaudeSessionRunState | undefined): boolean;
150
+ /**
151
+ * Whether the SDK's `session_state_changed` echo may overwrite the host's own
152
+ * busy flag.
153
+ *
154
+ * The echo and the reservation share one field, and the echo lags: the 'idle'
155
+ * that closes turn A is emitted after A's result has flushed, and the host
156
+ * drains its queue INSIDE that result handler — so by the time the echo is
157
+ * processed, turn B may already hold the slot. Applying it there releases a
158
+ * slot that is taken, re-opening the dispatch-window race and killing the
159
+ * typing signal B just started. An 'idle' echo is therefore ignored while the
160
+ * host knows it is busy; every other echo is authoritative, so the SDK can
161
+ * still take the session busy on its own (a resumed session, an internal
162
+ * envelope) and nothing can wedge the flag at 'running'.
163
+ */
164
+ export declare function shouldApplyClaudeEchoedSessionState(input: {
165
+ echoed: ClaudeSessionRunState | undefined;
166
+ /** An input is mid-dispatch: reserved by the host, not yet with the SDK. */
167
+ dispatching: boolean;
168
+ /** A final is still being handed to Canon; the turn is not over. */
169
+ hasPendingFinalDelivery: boolean;
170
+ }): boolean;
171
+ /** Claim the slot for an input about to be dispatched. Must precede any await. */
172
+ export declare function reserveClaudeTurnSlot(session: ClaudeTurnSlotState): void;
173
+ /** Give the slot back — dispatch failed, or the turn is over. */
174
+ export declare function releaseClaudeTurnSlot(session: ClaudeTurnSlotState): void;
175
+ export type ClaudeControlSignalAction = 'act' | 'consume' | 'defer';
176
+ /**
177
+ * What to do with a stop signal from the control channel.
178
+ *
179
+ * The dispatch window is the interesting case. The slot is reserved but the
180
+ * input has not reached Claude, so there is nothing for `query.interrupt()` to
181
+ * stop — yet consuming the signal as stale would make a Stop pressed in that
182
+ * window do NOTHING while the turn it was aimed at ran to completion, silently.
183
+ * Deferring hands it back to the poller, which revisits the same node a cycle
184
+ * later, by which time the input really is in flight.
185
+ *
186
+ * Outside that window a signal with nothing to act on is consumed, except that
187
+ * 'stop_and_drop' still has work to do while inputs are queued: dropping them.
188
+ */
189
+ export declare function decideClaudeControlSignalAction(input: {
190
+ type: 'interrupt' | 'stop_and_drop';
191
+ /** An input is mid-dispatch: reserved by the host, not yet with the SDK. */
192
+ dispatching: boolean;
193
+ sessionState: ClaudeSessionRunState | undefined;
194
+ queueDepth: number;
195
+ }): ClaudeControlSignalAction;
196
+ export type ClaudeInboundDispatchDecision = 'start' | 'queue' | 'queue-front' | 'interrupt';
197
+ /**
198
+ * What to do with an inbound Canon message.
199
+ *
200
+ * `hasInterruptibleInput` is the one piece that is not just "is the session
201
+ * busy": an interrupt can only stop an input the SDK has actually pulled. In
202
+ * the dispatch window there is nothing in flight to interrupt — calling
203
+ * `query.interrupt()` there would fire at an idle SDK and then let the input it
204
+ * was meant to pre-empt reach the SDK a moment later, unaffected, while the
205
+ * interrupting message sat in the queue behind it. Such a message goes to the
206
+ * FRONT of the queue instead, and starts as soon as the dispatched turn ends.
207
+ *
208
+ * A session waiting on an approval ('requires_action') is busy like any other
209
+ * live turn: an ordinary message queues behind the prompt, and an interrupt
210
+ * stops the waiting turn rather than racing a second one alongside it.
211
+ */
212
+ export declare function decideClaudeInboundDispatch(input: {
213
+ intent: DeliveryIntent;
214
+ sessionState: ClaudeSessionRunState | undefined;
215
+ hasPendingFinalDelivery: boolean;
216
+ /** An input the SDK has pulled off the stream — the only thing interrupt() can stop. */
217
+ hasInterruptibleInput: boolean;
218
+ }): ClaudeInboundDispatchDecision;
219
+ /**
220
+ * Whether a turn that has just finished may hand the slot back.
221
+ *
222
+ * Finishing spans awaits — the final send, the context-usage read — and the
223
+ * session reads free to inbound messages from the SDK's idle echo onward, so a
224
+ * message arriving in that window can reserve the slot for ITSELF. Releasing
225
+ * unconditionally afterwards would hand the same slot to a second message and
226
+ * put two turns into the SDK at once, which is exactly what the reservation
227
+ * exists to stop.
228
+ */
229
+ export declare function shouldReleaseClaudeTurnSlot(input: {
230
+ /** A newer turn is mid-dispatch. */
231
+ dispatching: boolean;
232
+ /** The turn the SDK is working on now, if any. */
233
+ activeTurnKey: string | null;
234
+ /** The turn asking to release the slot. */
235
+ completedTurnKey: string | null;
236
+ }): boolean;
237
+ /** Whether the next queued input may be dequeued now. */
238
+ export declare function canDrainClaudeQueuedInput(input: {
239
+ closed: boolean;
240
+ controlInterruptPending: boolean;
241
+ sessionState: ClaudeSessionRunState | undefined;
242
+ queueDepth: number;
243
+ }): boolean;
244
+ export interface ClaudeDispatchSessionState extends ClaudeTurnSlotState {
245
+ dispatchingInput: ClaudeInputEnvelope | null;
246
+ closed: boolean;
247
+ }
248
+ /** Why an input never reached the SDK. */
249
+ export type ClaudeDispatchAbandonReason = 'error' | 'closed';
250
+ export interface ClaudeDispatchDeps {
251
+ /** Publish the turn: thinking header, typing signal, seeded live node. */
252
+ openTurn: (input: ClaudeInputEnvelope) => void;
253
+ /** Tell Canon the message was accepted — a network round-trip. */
254
+ markAccepted: (input: ClaudeInputEnvelope) => Promise<void>;
255
+ /** Capture the artifact baseline — a workspace walk. */
256
+ prepareArtifacts: (input: ClaudeInputEnvelope) => Promise<void>;
257
+ /** Hand the input to the SDK. */
258
+ send: (input: ClaudeInputEnvelope) => void;
259
+ /** Settle an input that will never be answered, and retire its turn. */
260
+ abandon: (input: ClaudeInputEnvelope, reason: ClaudeDispatchAbandonReason, error?: unknown) => void;
261
+ }
262
+ /**
263
+ * Dispatch one input: reserve the slot, publish the turn, then do the slow
264
+ * work, then hand it to the SDK.
265
+ *
266
+ * The ORDER is the point, which is why this lives here rather than inline in
267
+ * the host: the reservation and `openTurn` must both happen before the first
268
+ * await. Marking the message accepted is a network round-trip and the artifact
269
+ * baseline walks the workspace; leaving the session readable as free across
270
+ * that window let a second message bypass the queue and open a competing turn
271
+ * over this one (`seedTurnStreaming` wiping the first turn's live node, two
272
+ * inputs in the SDK under mismatched attribution).
273
+ *
274
+ * Never rejects. An input that cannot be handed over — a throw in the window,
275
+ * or a session torn down mid-dispatch — is ABANDONED rather than dropped: the
276
+ * host settles the message and retires the turn, instead of leaving the sender
277
+ * on 'accepted' forever with a 'thinking' row nothing will complete.
278
+ */
279
+ export declare function dispatchClaudeInput(session: ClaudeDispatchSessionState, input: ClaudeInputEnvelope, deps: ClaudeDispatchDeps): Promise<void>;
280
+ /** States in which a turn is live for the clients (mirrors the turn-state write). */
281
+ export declare function isOpenClaudeTurnState(state: TurnLifecycleState): boolean;
282
+ /**
283
+ * Whether the SDK's `running` confirmation should open the turn itself.
284
+ *
285
+ * When the host already opened this turn the confirmation is a no-op: re-seeding
286
+ * `/streaming` would clobber the blocks and text the turn has produced since.
287
+ */
288
+ export declare function shouldOpenClaudeTurnOnRunning(state: TurnLifecycleState): boolean;
289
+ export interface ClaudeOpenTurnState {
290
+ pendingFinalText: string | null;
291
+ toolInProgress: boolean;
292
+ turnState: TurnLifecycleState;
293
+ currentTurnId: string | null;
294
+ currentTurnOpenedAt: number | null;
295
+ currentTurnUpdatedAt: number | null;
296
+ turnActivity?: ClaudeTurnActivityState;
297
+ }
298
+ /**
299
+ * Move the session into a fresh open turn: 'thinking', with a turn id, before
300
+ * a single SDK message has arrived.
301
+ *
302
+ * The id is minted per OPEN, unconditionally. Keying the mint on "the turn
303
+ * state is not open" reads equivalent and is not: the state a finished turn
304
+ * actually sits in during the final-handoff window is 'streaming'
305
+ * (`markPendingFinalDelivery` puts it there and only the reset clears it), so a
306
+ * message arriving in that window — precisely the case this exists for — would
307
+ * inherit an id that already carries a `turn_complete` message, and the client
308
+ * drops a live turn whose id is already complete.
309
+ *
310
+ * Nothing needs the guard: a turn is only ever opened when none is running —
311
+ * at dequeue, or on the SDK's `running` echo for a turn the host did not open
312
+ * itself (`shouldOpenClaudeTurnOnRunning` has already proved the state is not
313
+ * an open one there).
314
+ */
315
+ export declare function openClaudeTurn(session: ClaudeOpenTurnState, now?: number): void;
316
+ export interface ClaudeTurnActivityState {
317
+ /** content_block index → trail block id, for `content_block_stop`. */
318
+ blockIdByIndex: Map<number, string>;
319
+ /** `tool_use` id → trail block id, for out-of-order `tool_result`s. */
320
+ blockIdByToolUseId: Map<string, string>;
321
+ /** This turn's plan block, once TodoWrite has produced one. */
322
+ planBlockId: string | null;
323
+ }
324
+ /**
325
+ * Whether an SDK message describes the agent's OWN work rather than a
326
+ * subagent's. Nested messages carry the id of the Task call that spawned them;
327
+ * that Task's row already stands for the whole subagent, and publishing its
328
+ * internals would flood the trail — and the 20-block budget the persisted turn
329
+ * trail is bounded to.
330
+ */
331
+ export declare function isClaudeMainTurnMessage(parentToolUseId: unknown): boolean;
332
+ export declare function createClaudeTurnActivityState(): ClaudeTurnActivityState;
333
+ export declare function resetClaudeTurnActivityState(state: ClaudeTurnActivityState): void;
334
+ export declare function claudeToolBlockId(toolUseId: string): string;
335
+ export declare function claudePlanBlockId(turnId: string | null | undefined): string;
336
+ /**
337
+ * This turn's plan block, claimed once and then reused.
338
+ *
339
+ * Reading the claimed id back — rather than re-deriving it from the turn id on
340
+ * every TodoWrite — is what keeps a plan card whole: a turn id that changed
341
+ * mid-turn would otherwise split the plan into a second card instead of
342
+ * updating the first.
343
+ */
344
+ export declare function claimClaudePlanBlockId(state: ClaudeTurnActivityState, turnId: string | null | undefined): string;
345
+ /**
346
+ * Claim the trail block for a tool call the model has started emitting, and
347
+ * remember how to find it again from either the `tool_use` id (tool_result) or
348
+ * the content-block index (content_block_stop).
349
+ */
350
+ export declare function registerClaudeToolCallBlock(state: ClaudeTurnActivityState, input: {
351
+ toolUseId?: unknown;
352
+ index?: unknown;
353
+ turnId?: string | null;
354
+ }): string;
355
+ /** The block a `tool_result` belongs to. Null when this turn never claimed one. */
356
+ export declare function resolveClaudeToolBlockId(state: ClaudeTurnActivityState, toolUseId: unknown): string | null;
357
+ /**
358
+ * The block a `content_block_stop` closes, consumed on read — indexes restart
359
+ * at 0 with every assistant message, so a stale entry would mis-resolve.
360
+ */
361
+ export declare function takeClaudeToolBlockIdByIndex(state: ClaudeTurnActivityState, index: unknown): string | null;
362
+ /** TodoWrite is the plan card, not a tool receipt. */
363
+ export declare const CLAUDE_PLAN_TOOL_NAME = "TodoWrite";
364
+ /** The margin row's title for the plan card, which is the card's own heading. */
365
+ export declare const CLAUDE_PLAN_BLOCK_TITLE = "Plan";
366
+ /**
367
+ * The margin row's title for a tool call: what the agent actually did, not just
368
+ * which tool it reached for. Falls back to the tool name whenever the typed
369
+ * input is missing or shaped differently than expected — an unknown tool is
370
+ * still worth a row.
371
+ */
372
+ export declare function summarizeClaudeToolCall(name: unknown, input: unknown): string;
373
+ /** One-line reason for a failed tool call, for the receipt's summary slot. */
374
+ export declare function summarizeClaudeToolFailure(content: unknown): string;
375
+ /**
376
+ * Progress text for a call that is taking a while. Short calls get nothing:
377
+ * every summary change is an RTDB write, and "Running for 1s" says nothing the
378
+ * running row does not already say.
379
+ */
380
+ export declare function formatClaudeToolElapsed(seconds: unknown): string | null;
381
+ /**
382
+ * TodoWrite's typed input mapped onto the shared plan grammar (`renderPlanSteps`
383
+ * in @canonmsg/coding-agent-host), which both coding-agent hosts render through
384
+ * so a plan card reads the same whichever runtime produced it. What stays here
385
+ * is what is genuinely Claude's: the SDK's todo shape, its `activeForm` wording
386
+ * for the step being worked on ("Running the tests"), the per-step and
387
+ * per-plan bounds, and publishing NOTHING rather than a bare heading when a
388
+ * TodoWrite call carries no usable step.
389
+ *
390
+ * Unusable entries are dropped before rendering, so the numbering the reader
391
+ * sees stays contiguous.
392
+ */
393
+ export declare function renderTodosAsPlan(todos: unknown): string | null;
394
+ export type ClaudeTrailCommand = {
395
+ op: 'add';
396
+ id: string;
397
+ kind: 'tool' | 'plan';
398
+ /** Left off deliberately when an existing row's status must survive. */
399
+ status?: 'running';
400
+ title?: string;
401
+ text?: string;
402
+ } | {
403
+ op: 'update';
404
+ id: string;
405
+ title?: string;
406
+ summary?: string;
407
+ } | {
408
+ op: 'complete';
409
+ id: string;
410
+ summary?: string;
411
+ } | {
412
+ op: 'fail';
413
+ id: string;
414
+ summary: string;
415
+ };
416
+ /**
417
+ * `content_block_start` for a `tool_use`: the call claims its row, titled with
418
+ * the tool's name until the typed input arrives on the assistant message.
419
+ */
420
+ export declare function planClaudeToolCallStart(state: ClaudeTurnActivityState, input: {
421
+ toolUseId?: unknown;
422
+ index?: unknown;
423
+ name?: unknown;
424
+ turnId?: string | null;
425
+ }): ClaudeTrailCommand[];
426
+ /**
427
+ * The assistant message is the first place a call's typed INPUT is visible —
428
+ * `content_block_start` carries only the name. It is what turns "Bash" into
429
+ * "Running: npm test" and TodoWrite into the plan card.
430
+ */
431
+ export declare function planClaudeAssistantTrail(state: ClaudeTurnActivityState, content: unknown, turnId: string | null): ClaudeTrailCommand[];
432
+ /**
433
+ * `tool_result` on a `user` message: where a call's OUTCOME finally arrives.
434
+ * Rows settle here and nowhere else — `content_block_stop` only means the model
435
+ * finished writing the call.
436
+ */
437
+ export declare function planClaudeToolResults(state: ClaudeTurnActivityState, content: unknown): ClaudeTrailCommand[];
438
+ /** `tool_progress`: a long call says how long it has been going. */
439
+ export declare function planClaudeToolProgress(state: ClaudeTurnActivityState, input: {
440
+ toolUseId: unknown;
441
+ elapsedSeconds: unknown;
442
+ }): ClaudeTrailCommand[];
72
443
  export declare function claudeOriginForCanonSender(input: {
73
444
  senderType?: string | null;
74
445
  senderId: string;
@@ -107,6 +478,168 @@ export declare function composeClaudeFinalText(input: {
107
478
  streamedText?: string | null;
108
479
  failureNotice?: string | null;
109
480
  }): string | null;
481
+ export type ClaudeFinalDeliveryFailure = 'retry' | 'permanent' | 'already-delivered';
482
+ /**
483
+ * How the host should react to a failed final-reply send.
484
+ *
485
+ * Core's `isRetryableCanonDeliveryError` is already right about what a retry
486
+ * can fix (429, 5xx, transport errors); what the host lacked was the other
487
+ * half — everything else is PERMANENT and must be surfaced instead of retried.
488
+ * A 409 from the server's idempotency guard means a message already exists
489
+ * under this id, so the reply is durable: that is a success, not a failure.
490
+ *
491
+ * A CHUNKED final is the exception (#616's rule, kept). A chunked send is N
492
+ * sequential messages that abort on the first failure, so a 409 says only that
493
+ * ONE part id is taken, with nothing to say which — and since the part ids are
494
+ * a pure hash of agent + conversation + turnKey, an id can be taken by an
495
+ * ENTIRELY DIFFERENT answer to the same inbound message, written by a previous
496
+ * process. Reading that as "the final is delivered" would finalize the turn
497
+ * with the tail never sent, so it stays 'permanent': a stops-short notice beats
498
+ * silently dropping the end of an answer.
499
+ *
500
+ * Resume support does not soften this. Core absorbs the one conflict that IS
501
+ * provably ours — the first part of a resumed attempt, whose write may have
502
+ * landed before its response was lost — and reports every other one, so a
503
+ * conflict reaching this function is precisely the case that must not be read
504
+ * as success.
505
+ */
506
+ export declare function classifyFinalDeliveryFailure(error: unknown, options?: {
507
+ chunked?: boolean;
508
+ }): ClaudeFinalDeliveryFailure;
509
+ /**
510
+ * The pending-delivery record for a final that has to be retried, carrying the
511
+ * progress the next attempt needs.
512
+ *
513
+ * Progress is monotonic: a later attempt can only ever add parts, so the longer
514
+ * of the two lists wins and a retry can never rewind the cursor. The metadata
515
+ * and the self-context are pinned to the FIRST attempt's values, because those
516
+ * are what the durable parts were fingerprinted against.
517
+ */
518
+ export declare function buildClaudePendingFinalDelivery(input: {
519
+ previous: ClaudePendingFinalDelivery | null;
520
+ turnKey: string;
521
+ messageId: string;
522
+ text: string;
523
+ suppressAutoReply?: boolean;
524
+ metadata?: Record<string, unknown>;
525
+ selfContextId?: string | null;
526
+ deliveredMessageIds?: readonly string[];
527
+ lastErrorMessage?: string;
528
+ }): ClaudePendingFinalDelivery;
529
+ /**
530
+ * What a retry of this turn's final should reuse: the parts already durable,
531
+ * and the metadata and self-context they were written with. Empty for a first
532
+ * attempt, or when the pending record belongs to a different turn.
533
+ */
534
+ export declare function readClaudeFinalDeliveryResume(pending: ClaudePendingFinalDelivery | null, turnKey: string): {
535
+ deliveredMessageIds: string[];
536
+ metadata?: Record<string, unknown>;
537
+ selfContextId?: string | null;
538
+ };
539
+ /**
540
+ * The chunking options a final send opts into, built from the resume cursor.
541
+ *
542
+ * A helper rather than an inline literal so the opt-in is one testable seam:
543
+ * dropping `resumable` restores restart-from-part-1, which no assertion on a
544
+ * hand-built call could catch.
545
+ */
546
+ export declare function buildClaudeFinalChunkingOptions(resume: {
547
+ deliveredMessageIds: readonly string[];
548
+ }): {
549
+ resumable: true;
550
+ deliveredMessageIds: readonly string[];
551
+ };
552
+ /** True when a final of this size will be split across several messages. */
553
+ export declare function claudeFinalWillChunk(finalText: string, maxTextBytes?: number): boolean;
554
+ /**
555
+ * The turn trail to attach to a final that is about to be sent.
556
+ *
557
+ * A chunked final's LAST message carries the answer's tail, while the trail's
558
+ * text blocks carry the compacted head. chat-domain's `buildTurnRenderParts`
559
+ * appends `message.text` after structured speech that is not a prefix of it, so
560
+ * an unfiltered trail would render the head again above the tail. Text blocks
561
+ * are therefore dropped from a chunked final; tool/plan/approval/status blocks
562
+ * are the margin's activity record and stay. Unchunked finals are untouched,
563
+ * which keeps the common case byte-identical to before.
564
+ */
565
+ export declare function prepareTurnTrailForDelivery(trail: ReadonlyArray<TurnOutputBlock>, willChunk: boolean): TurnOutputBlock[];
566
+ export declare const CLAUDE_FINAL_TRUNCATION_MARKER = "\n\n_[truncated \u2014 the full answer exceeded Canon's message size limit]_";
567
+ /**
568
+ * Last-resort degradation for a final Canon refuses to accept: keep as much of
569
+ * the answer as fits in ONE message and say that the rest was dropped. Prefers
570
+ * to cut at a paragraph break near the end so the survivor reads as prose. The
571
+ * result — marker included — always fits the byte budget.
572
+ *
573
+ * Deliberate trade-off: this keeps the HEAD, so a trailing paragraph is lost —
574
+ * including the turn-failure notice `composeClaudeFinalText` appends last. That
575
+ * is acceptable only because the host calls this on a final that already fits
576
+ * (an oversized final is chunked, never truncated), so nothing is actually cut;
577
+ * it is the bound that keeps the degraded send legal, not a text editor. A
578
+ * caller that truncates for real must re-attach its own trailing notice.
579
+ */
580
+ export declare function buildTruncatedFinalText(finalText: string, maxTextBytes?: number): string;
581
+ /**
582
+ * Server-side `metadata` cap, taken from the shared contract rather than
583
+ * re-typed here. Note the UNIT: unlike message text, which the server measures
584
+ * in UTF-8 bytes, metadata is capped by `JSON.stringify(metadata).length` —
585
+ * UTF-16 code units — in both places it is checked (the request schema's
586
+ * misleadingly named `maxJsonBytes` and the handler's inline check). Mirroring
587
+ * it in bytes would be a different, stricter rule that drops the margin's
588
+ * activity record on payloads Canon would have accepted.
589
+ */
590
+ export declare const CLAUDE_MESSAGE_METADATA_MAX_CHARS: 4096;
591
+ /**
592
+ * Defensive pre-flight for message metadata. Hosts already bound the trail via
593
+ * core's `buildBoundedTurnTrail`, so this should never fire; if it ever does,
594
+ * losing the margin's activity record beats losing the answer.
595
+ */
596
+ export declare function boundClaudeFinalMetadata(metadata: Record<string, unknown>, maxChars?: 4096): Record<string, unknown>;
597
+ /**
598
+ * The standalone notice for a final Canon will not accept — the last thing the
599
+ * host can say when the answer itself cannot be delivered.
600
+ *
601
+ * `partiallyDelivered` marks a chunked final whose earlier parts are already in
602
+ * the conversation: the reader can see text, and needs to be told it stops
603
+ * short rather than being left to assume the answer ended there.
604
+ */
605
+ export declare function buildUndeliverableFinalNotice(input: {
606
+ error: unknown;
607
+ partiallyDelivered?: boolean;
608
+ }): string;
609
+ /**
610
+ * What is left to say when the bounded retries for a final run out.
611
+ *
612
+ * With parts of a chunked answer already in the conversation, silence is the
613
+ * wrong ending: the reader takes the last delivered part for the end of the
614
+ * answer. That case gets the stops-short notice, which completes the turn.
615
+ *
616
+ * With NOTHING delivered there is deliberately no notice, so the host can clear
617
+ * `/streaming` and let functions' `onStreamingCleared` preserve the streamed
618
+ * text as a durable progress message — a notice would complete the turn and
619
+ * suppress exactly that fallback, leaving the reader with an apology and no
620
+ * answer at all.
621
+ */
622
+ export declare function planExhaustedFinalDelivery(pending: ClaudePendingFinalDelivery): {
623
+ partiallyDelivered: boolean;
624
+ notice: string | null;
625
+ };
626
+ /**
627
+ * End a final whose retries ran out: say it stops short, THEN retire the turn.
628
+ *
629
+ * The order is the point, which is why the sequence lives here. The pending
630
+ * record is what makes the session read busy to inbound messages, and the
631
+ * notice is a network send with its own retries — clearing the record first
632
+ * left the session 'running' with no timer and no pending delivery, so an
633
+ * interrupt arriving in that window fired `query.interrupt()` at an SDK whose
634
+ * turn had already produced its result, flipped the turn row to 'interrupted'
635
+ * and cleared `/streaming` out from under the retirement.
636
+ */
637
+ export declare function runClaudeExhaustedFinalDelivery(session: {
638
+ pendingFinalDelivery: ClaudePendingFinalDelivery | null;
639
+ }, pending: ClaudePendingFinalDelivery, deps: {
640
+ sendNotice: (notice: string) => Promise<unknown>;
641
+ retireTurn: () => void;
642
+ }): Promise<void>;
110
643
  export declare const MINIMUM_CLAUDE_CLI_VERSION: readonly number[];
111
644
  export declare function parseClaudeCliVersion(output: string): number[] | null;
112
645
  export declare function formatClaudeCliVersion(version: readonly number[]): string;