@securecoms/agent-bridge 0.37.0 → 0.38.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.
@@ -519,6 +519,12 @@ export interface BridgeAgentDeps {
519
519
  * Injected so tests do not wait the full window.
520
520
  */
521
521
  voiceCoalesceWindowMs?: number;
522
+ /**
523
+ * Image + caption coalescing: how long a caption-less image share waits for
524
+ * its sender's caption (default CAPTION_COALESCE_WINDOW_MS; 0 disables the
525
+ * hold, every image is staged at once as before).
526
+ */
527
+ captionCoalesceWindowMs?: number;
522
528
  /**
523
529
  * B7 (FileSafe vault): persistence for the vault kekset's owner-pin TOFU
524
530
  * state. Defaults to a {@link FileOwnerAuthorityStore} at
@@ -566,6 +572,30 @@ export declare function voiceWindowEligible(t: {
566
572
  voiceGuidance: string | undefined;
567
573
  isCallStart: boolean;
568
574
  }): boolean;
575
+ /**
576
+ * Image + caption coalescing (2026-11-11). The web client sends an image with a
577
+ * caption as TWO channel messages: an `artifact_share` with no text, then a plain
578
+ * text message. Delivered as two turns, the agent answered the bare image and
579
+ * then the caption without the image. A caption-less image share from a sender
580
+ * that is not an agent is staged durably but HELD this long; the next message
581
+ * from the same sender in the same channel, if it is a text turn, is delivered
582
+ * as ONE frame: the caption's id, text, authorization and memory, plus the held
583
+ * images' attachments. Anything else in the channel releases the held images
584
+ * first, in order. Rules (see BridgeAgent.holdCaptionImage and the text path):
585
+ * - consecutive images from the same sender join one hold (the window restarts
586
+ * from the latest image), up to CAPTION_MERGE_MAX_ATTACHMENTS; with no caption
587
+ * each image is released as its own frame, in order, as before;
588
+ * - the merge happens only when every held image's authorization equals the
589
+ * caption's (never widens or narrows a grant), else the images go alone first;
590
+ * - in a room, a caption the @mention filter rejects drops the held images too
591
+ * (image + caption was one message, addressed elsewhere); a caption-less image
592
+ * alone still follows the unchanged broadcast rule;
593
+ * - close() releases nothing: held images stay in runtime_inbox and a restart
594
+ * restores them as ordinary turns.
595
+ */
596
+ export declare const CAPTION_COALESCE_WINDOW_MS = 1500;
597
+ /** The connectors accept at most 8 attachments per frame (#1450): a hold never grows past that. */
598
+ export declare const CAPTION_MERGE_MAX_ATTACHMENTS = 8;
569
599
  /** Default inbound-buffer cap (messages) when deps.maxBuffer is unset. */
570
600
  export declare const DEFAULT_MAX_BUFFER = 1000;
571
601
  /**
@@ -1119,6 +1149,14 @@ export declare class BridgeAgent {
1119
1149
  private readonly voiceCoalescedIds;
1120
1150
  /** Merged turn id -> the absorbed fragments' held frames, retired with it. */
1121
1151
  private readonly voiceCoalescedTails;
1152
+ /** Image + caption coalescing: the open hold per channel (CAPTION_COALESCE_WINDOW_MS). */
1153
+ private readonly captionHolds;
1154
+ /**
1155
+ * Ids of held images whose content was consumed (capped FIFO): merged into a caption frame, or dropped with a
1156
+ * caption the room filter rejected. A redelivered row is acked, never downloaded or delivered (audit L1, round 2).
1157
+ */
1158
+ private readonly captionConsumedIds;
1159
+ private readonly captionCoalesceWindowMs;
1122
1160
  /** (4) recv.lag: application queue ids already measured (capped FIFO), so a re-pulled row is not re-counted. */
1123
1161
  private readonly recvLagSeen;
1124
1162
  private static readonly RECV_LAG_SEEN_CAP;
@@ -2934,6 +2972,33 @@ export declare class BridgeAgent {
2934
2972
  * deferring fast reply was actually published — F3).
2935
2973
  */
2936
2974
  private speakAndSettle;
2975
+ /**
2976
+ * Image + caption coalescing: stage a caption-less image share durably but
2977
+ * HELD, joining its sender's open hold in this channel or opening one. The
2978
+ * hold's job takes the channel queue now, so every later frame of the channel
2979
+ * (all staged through stageRuntimeMessageInOrder or the queue) waits behind it.
2980
+ * Returns false on backpressure (the row stays unacked). The caller has
2981
+ * checked the id is not already staged (a redelivered row).
2982
+ */
2983
+ private holdCaptionImage;
2984
+ /** Release one held image as its own frame: the room web_search count it skipped at staging runs now (audit M3). */
2985
+ private releaseCaptionImage;
2986
+ /**
2987
+ * Retire held images whose content left with a merged frame or was dropped with a filtered caption. Never throws
2988
+ * (audit M1: a throw here left the hold job asleep and the channel queue wedged): a row that cannot be retired
2989
+ * stays in runtime_inbox and comes back at a restart as an ordinary turn.
2990
+ */
2991
+ private retireCaptionImages;
2992
+ /** Audit L3: best-effort removal of the decrypted files of images dropped with a filtered caption. Never throws. */
2993
+ private unlinkCaptionImages;
2994
+ private noteCaptionConsumed;
2995
+ private armCaptionHold;
2996
+ /** Detach a hold synchronously: nothing joins or merges into it after this. */
2997
+ private detachCaptionHold;
2998
+ /** Detach (if still open) and wake the hold's job with its outcome. Idempotent on the outcome. */
2999
+ private closeCaptionHold;
3000
+ /** The open hold a text turn from `senderId` in `channelId` may take, if any. */
3001
+ private captionHoldFor;
2937
3002
  /**
2938
3003
  * Stage a runtime frame without overtaking a turn of the same channel that
2939
3004
  * is still queued behind the fast path (F2): while the channel has a queue,