@opengeni/react 1.0.1 → 2.3.0-canary.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 +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -4,17 +4,29 @@ import {
|
|
|
4
4
|
type DraftTimelineAnnotation,
|
|
5
5
|
type EffectiveControlResumeOption,
|
|
6
6
|
type EffectiveSessionControl,
|
|
7
|
+
type LatencyMode,
|
|
7
8
|
type OpenGeniApiError,
|
|
9
|
+
type ReasoningEffort,
|
|
8
10
|
type ResourceRef,
|
|
9
11
|
type SaveComposerDraftRequest,
|
|
10
12
|
type SendMessageInput,
|
|
11
13
|
type SessionEvent,
|
|
14
|
+
type SubmitComposerDraftResponse,
|
|
12
15
|
} from "@opengeni/sdk";
|
|
13
16
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
14
17
|
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
15
18
|
import { useSessionEventTrigger, type SessionEventFeedOptions } from "./internal";
|
|
16
19
|
|
|
17
|
-
export type
|
|
20
|
+
export type ComposerPolicy = {
|
|
21
|
+
model: string;
|
|
22
|
+
reasoningEffort: ReasoningEffort;
|
|
23
|
+
latencyMode: LatencyMode;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type ComposerSendExtras = Omit<
|
|
27
|
+
SendMessageInput,
|
|
28
|
+
"text" | "clientEventId" | "annotations" | "model" | "reasoningEffort" | "latencyMode"
|
|
29
|
+
>;
|
|
18
30
|
|
|
19
31
|
export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
20
32
|
SessionEventFeedOptions & {
|
|
@@ -22,10 +34,13 @@ export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
|
22
34
|
onSubmitted?: ((text: string, input: SendMessageInput) => void) | undefined;
|
|
23
35
|
/** Called with the exact accepted wire input after a successful send. */
|
|
24
36
|
onSent?: ((text: string, input: SendMessageInput) => void) | undefined;
|
|
37
|
+
/** Called with the exact wire input after a delivery failure. */
|
|
38
|
+
onDeliveryError?:
|
|
39
|
+
| ((error: Error, input: SendMessageInput, delivery: "send" | "steer") => void)
|
|
40
|
+
| undefined;
|
|
25
41
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* surrounding UI state (attachment pickers, model selectors, ...).
|
|
42
|
+
* Non-policy message fields merged into every send. Durable policy belongs
|
|
43
|
+
* to this composer draft; attachments and credentials may remain host-owned.
|
|
29
44
|
*/
|
|
30
45
|
sendExtras?: ComposerSendExtras | (() => ComposerSendExtras) | undefined;
|
|
31
46
|
/**
|
|
@@ -36,16 +51,19 @@ export type UseComposerOptions = EmbeddedSessionClientOverride &
|
|
|
36
51
|
sendBlocked?: (() => boolean) | undefined;
|
|
37
52
|
/** Latest server-derived workstream control; bound into Send/Steer OCC. */
|
|
38
53
|
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
39
|
-
/**
|
|
40
|
-
|
|
54
|
+
/** Immediate user-visible destination for an ordinary Send before admission replies. */
|
|
55
|
+
sendDestination?: (() => "chat" | "queue") | undefined;
|
|
41
56
|
/** Disable remote composer-draft reads and writes for embedded hosts. */
|
|
42
57
|
draftPersistence?: "durable" | "disabled" | undefined;
|
|
58
|
+
/** Required explicit authority when durable draft persistence is disabled. */
|
|
59
|
+
initialPolicy?: ComposerPolicy | undefined;
|
|
43
60
|
};
|
|
44
61
|
|
|
45
62
|
type ComposerDraftShadow = {
|
|
46
63
|
text: string;
|
|
47
64
|
resources: ResourceRef[];
|
|
48
65
|
annotations: DraftTimelineAnnotation[];
|
|
66
|
+
policy?: ComposerPolicy | undefined;
|
|
49
67
|
};
|
|
50
68
|
|
|
51
69
|
type PendingComposerOperation = {
|
|
@@ -68,11 +86,16 @@ type StoredPendingComposerOperation = Omit<PendingComposerOperation, "input" | "
|
|
|
68
86
|
|
|
69
87
|
export type ComposerOptimisticMessage = {
|
|
70
88
|
clientEventId: string;
|
|
89
|
+
delivery: "send" | "steer";
|
|
90
|
+
destination: "chat" | "queue";
|
|
71
91
|
text: string;
|
|
72
92
|
annotations: DraftTimelineAnnotation[];
|
|
73
93
|
resources: ResourceRef[];
|
|
74
94
|
occurredAt: string;
|
|
75
95
|
state: "sending" | "queued" | "failed";
|
|
96
|
+
turnId?: string | null | undefined;
|
|
97
|
+
triggerEventId?: string | null | undefined;
|
|
98
|
+
appliedQueueVersion?: number | null | undefined;
|
|
76
99
|
error?: string | undefined;
|
|
77
100
|
outcomeUnknown?: boolean | undefined;
|
|
78
101
|
};
|
|
@@ -151,6 +174,8 @@ function restoreOptimisticSendOperations(key: string | null): OptimisticSendOper
|
|
|
151
174
|
}
|
|
152
175
|
return stored.map(({ hasMcpCredentialUpdates, ...operation }) => ({
|
|
153
176
|
...operation,
|
|
177
|
+
delivery: "send",
|
|
178
|
+
destination: operation.destination === "queue" ? "queue" : "chat",
|
|
154
179
|
newerShadow: operation.newerShadow ?? { text: "", resources: [], annotations: [] },
|
|
155
180
|
state:
|
|
156
181
|
operation.state === "sending" || operation.state === "queued" ? "sending" : operation.state,
|
|
@@ -183,6 +208,15 @@ function rememberOptimisticSendOperations(
|
|
|
183
208
|
hasMcpCredentialUpdates: input.mcpCredentialUpdates !== undefined,
|
|
184
209
|
} satisfies StoredOptimisticSendOperation;
|
|
185
210
|
});
|
|
211
|
+
if (stored.length === 0) {
|
|
212
|
+
optimisticSendOperations.delete(key);
|
|
213
|
+
try {
|
|
214
|
+
pendingComposerStorage()?.removeItem(optimisticSendStorageKey(key));
|
|
215
|
+
} catch {
|
|
216
|
+
// Best effort; the in-memory entry is already gone.
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
186
220
|
optimisticSendOperations.set(key, stored);
|
|
187
221
|
try {
|
|
188
222
|
pendingComposerStorage()?.setItem(optimisticSendStorageKey(key), JSON.stringify(stored));
|
|
@@ -344,6 +378,7 @@ function rememberPendingComposerOperation(
|
|
|
344
378
|
text: operation.newerShadow.text,
|
|
345
379
|
resources: [...operation.newerShadow.resources],
|
|
346
380
|
annotations: cloneAnnotations(operation.newerShadow.annotations),
|
|
381
|
+
...(operation.newerShadow.policy ? { policy: { ...operation.newerShadow.policy } } : {}),
|
|
347
382
|
},
|
|
348
383
|
} satisfies StoredPendingComposerOperation;
|
|
349
384
|
pendingComposerOperations.set(key, stored);
|
|
@@ -412,11 +447,18 @@ export type ComposerState = {
|
|
|
412
447
|
resume: (reason?: string) => Promise<void>;
|
|
413
448
|
resumeScope: (option: EffectiveControlResumeOption) => Promise<void>;
|
|
414
449
|
resuming: boolean;
|
|
450
|
+
/** Mutation-confirmed control while the streamed queue projection catches up. */
|
|
451
|
+
effectiveControl?: EffectiveSessionControl | null | undefined;
|
|
415
452
|
draft: ComposerDraft | null;
|
|
416
453
|
draftRevision: number;
|
|
417
454
|
draftLoading: boolean;
|
|
418
455
|
draftSaving: boolean;
|
|
419
456
|
draftConflict: Error | null;
|
|
457
|
+
/** Exact policy owned by this actor/session composer. Null while hydrating. */
|
|
458
|
+
policy?: ComposerPolicy | null | undefined;
|
|
459
|
+
setModel?: ((model: string) => void) | undefined;
|
|
460
|
+
setReasoningEffort?: ((effort: ReasoningEffort) => void) | undefined;
|
|
461
|
+
setLatencyMode?: ((mode: LatencyMode) => void) | undefined;
|
|
420
462
|
/** Whether this controller owns a durable server-side draft. */
|
|
421
463
|
draftPersistence?: "durable" | "disabled" | undefined;
|
|
422
464
|
/** Apply an atomic queue Edit checkout without a second read. */
|
|
@@ -429,19 +471,23 @@ export type ComposerState = {
|
|
|
429
471
|
clearError: () => void;
|
|
430
472
|
};
|
|
431
473
|
|
|
474
|
+
export type ComposerControllerState = ComposerState & {
|
|
475
|
+
policy: ComposerPolicy | null;
|
|
476
|
+
setModel: (model: string) => void;
|
|
477
|
+
setReasoningEffort: (effort: ReasoningEffort) => void;
|
|
478
|
+
setLatencyMode: (mode: LatencyMode) => void;
|
|
479
|
+
};
|
|
480
|
+
|
|
432
481
|
export type ComposerSteeringState = {
|
|
433
|
-
phase: "submitting" | "accepted";
|
|
482
|
+
phase: "submitting" | "accepted" | "failed";
|
|
434
483
|
text: string;
|
|
435
484
|
clientEventId: string | null;
|
|
436
485
|
triggerEventId: string | null;
|
|
437
486
|
turnId: string | null;
|
|
438
487
|
/** True only when the accepted Steer durably interrupted a live attempt. */
|
|
439
488
|
stoppingPreviousAttempt?: boolean | undefined;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
type ComposerControlStoppingState = {
|
|
443
|
-
controlVersion: number;
|
|
444
|
-
controlEtag: string;
|
|
489
|
+
error?: string | undefined;
|
|
490
|
+
outcomeUnknown?: boolean | undefined;
|
|
445
491
|
};
|
|
446
492
|
|
|
447
493
|
const STEERING_SETTLEMENT_EVENT_TYPES = new Set([
|
|
@@ -468,6 +514,32 @@ function steeringAcceptedEvent(
|
|
|
468
514
|
);
|
|
469
515
|
}
|
|
470
516
|
|
|
517
|
+
function promptTurnIdForTrigger(
|
|
518
|
+
triggerEventId: string | null | undefined,
|
|
519
|
+
events: readonly SessionEvent[],
|
|
520
|
+
): string | null {
|
|
521
|
+
if (!triggerEventId) return null;
|
|
522
|
+
const queued = events.find((event) => {
|
|
523
|
+
if (event.type !== "turn.queued") return false;
|
|
524
|
+
const payload = event.payload;
|
|
525
|
+
return (
|
|
526
|
+
typeof payload === "object" &&
|
|
527
|
+
payload !== null &&
|
|
528
|
+
"triggerEventId" in payload &&
|
|
529
|
+
payload.triggerEventId === triggerEventId
|
|
530
|
+
);
|
|
531
|
+
});
|
|
532
|
+
if (!queued) return null;
|
|
533
|
+
if (queued.turnId) return queued.turnId;
|
|
534
|
+
const payload = queued.payload;
|
|
535
|
+
return typeof payload === "object" &&
|
|
536
|
+
payload !== null &&
|
|
537
|
+
"turnId" in payload &&
|
|
538
|
+
typeof payload.turnId === "string"
|
|
539
|
+
? payload.turnId
|
|
540
|
+
: null;
|
|
541
|
+
}
|
|
542
|
+
|
|
471
543
|
function steeringSettledByEvents(
|
|
472
544
|
steering: ComposerSteeringState,
|
|
473
545
|
events: readonly SessionEvent[],
|
|
@@ -489,6 +561,63 @@ function steeringSettledByEvents(
|
|
|
489
561
|
});
|
|
490
562
|
}
|
|
491
563
|
|
|
564
|
+
function reconcileOptimisticSendFromEvents(
|
|
565
|
+
operation: OptimisticSendOperation,
|
|
566
|
+
events: readonly SessionEvent[],
|
|
567
|
+
): { operation: OptimisticSendOperation; admitted: boolean; settled: boolean } {
|
|
568
|
+
const accepted = events.find(
|
|
569
|
+
(event) => event.type === "user.message" && event.clientEventId === operation.clientEventId,
|
|
570
|
+
);
|
|
571
|
+
const triggerEventId = operation.triggerEventId ?? accepted?.id ?? null;
|
|
572
|
+
const turnId = operation.turnId ?? promptTurnIdForTrigger(triggerEventId, events);
|
|
573
|
+
const needsAdmissionUpdate =
|
|
574
|
+
accepted !== undefined &&
|
|
575
|
+
(operation.state !== "queued" ||
|
|
576
|
+
operation.triggerEventId !== triggerEventId ||
|
|
577
|
+
operation.turnId !== turnId ||
|
|
578
|
+
operation.error !== undefined ||
|
|
579
|
+
operation.outcomeUnknown !== false);
|
|
580
|
+
const reconciled: OptimisticSendOperation = needsAdmissionUpdate
|
|
581
|
+
? {
|
|
582
|
+
...operation,
|
|
583
|
+
state: "queued",
|
|
584
|
+
triggerEventId,
|
|
585
|
+
turnId,
|
|
586
|
+
error: undefined,
|
|
587
|
+
outcomeUnknown: false,
|
|
588
|
+
}
|
|
589
|
+
: operation;
|
|
590
|
+
const withdrawnFromQueue = events.some((event) => {
|
|
591
|
+
if (event.type !== "session.queue.changed" || !turnId) return false;
|
|
592
|
+
const payload = event.payload;
|
|
593
|
+
if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return false;
|
|
594
|
+
const record = payload as Record<string, unknown>;
|
|
595
|
+
return (
|
|
596
|
+
record.turnId === turnId && (record.operation === "edit" || record.operation === "delete")
|
|
597
|
+
);
|
|
598
|
+
});
|
|
599
|
+
return {
|
|
600
|
+
operation: reconciled,
|
|
601
|
+
admitted: accepted !== undefined,
|
|
602
|
+
// Edit/Delete atomically withdraw the queued turn. Keeping its optimistic
|
|
603
|
+
// send alive after that point lets its old `newerShadow` become the newest
|
|
604
|
+
// composer draft again when a later turn settles, duplicating submitted
|
|
605
|
+
// text. The queue mutation is the durable terminal fact for this send.
|
|
606
|
+
settled:
|
|
607
|
+
withdrawnFromQueue ||
|
|
608
|
+
steeringSettledByEvents(
|
|
609
|
+
{
|
|
610
|
+
phase: "accepted",
|
|
611
|
+
text: reconciled.text,
|
|
612
|
+
clientEventId: reconciled.clientEventId,
|
|
613
|
+
triggerEventId: reconciled.triggerEventId ?? null,
|
|
614
|
+
turnId: reconciled.turnId ?? null,
|
|
615
|
+
},
|
|
616
|
+
events,
|
|
617
|
+
),
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
492
621
|
/**
|
|
493
622
|
* Draft + send + Pause/Resume state for the chat composer — the only
|
|
494
623
|
* human-to-agent input surface. The draft survives a failed send (nothing is
|
|
@@ -498,9 +627,12 @@ function steeringSettledByEvents(
|
|
|
498
627
|
export function useComposer(
|
|
499
628
|
sessionId: string | null | undefined,
|
|
500
629
|
options: UseComposerOptions = {},
|
|
501
|
-
):
|
|
630
|
+
): ComposerControllerState {
|
|
502
631
|
const { client, workspaceId, registerSessionReconciler } = useEmbeddedSession(options);
|
|
503
632
|
const durableDrafts = options.draftPersistence !== "disabled";
|
|
633
|
+
if (!durableDrafts && !options.initialPolicy) {
|
|
634
|
+
throw new Error("useComposer requires initialPolicy when draft persistence is disabled");
|
|
635
|
+
}
|
|
504
636
|
const targetKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${durableDrafts ? "durable" : "disabled"}`;
|
|
505
637
|
const pendingOperationKey = pendingComposerOperationKey(workspaceId, sessionId);
|
|
506
638
|
const initialPendingOperation = restorePendingComposerOperation(pendingOperationKey);
|
|
@@ -521,22 +653,27 @@ export function useComposer(
|
|
|
521
653
|
const [steering, setSteering] = useState<ComposerSteeringState | null>(() =>
|
|
522
654
|
initialPendingOperation?.delivery === "steer"
|
|
523
655
|
? {
|
|
524
|
-
phase: "
|
|
656
|
+
phase: "failed",
|
|
525
657
|
text: initialPendingOperation.input.text,
|
|
526
658
|
clientEventId: initialPendingOperation.input.clientEventId ?? null,
|
|
527
659
|
triggerEventId: null,
|
|
528
660
|
turnId: null,
|
|
661
|
+
error: "Not confirmed. Retry to check the same direction change.",
|
|
662
|
+
outcomeUnknown: true,
|
|
529
663
|
}
|
|
530
664
|
: null,
|
|
531
665
|
);
|
|
532
666
|
const [pausing, setPausing] = useState(false);
|
|
533
|
-
const [
|
|
667
|
+
const [acceptedControl, setAcceptedControl] = useState<EffectiveSessionControl | null>(null);
|
|
534
668
|
const [resuming, setResuming] = useState(false);
|
|
535
669
|
const [error, setError] = useState<Error | null>(null);
|
|
536
670
|
const [draft, setDraft] = useState<ComposerDraft | null>(null);
|
|
537
671
|
const [draftLoading, setDraftLoading] = useState(Boolean(sessionId) && durableDrafts);
|
|
538
672
|
const [draftSaving, setDraftSaving] = useState(false);
|
|
539
673
|
const [draftConflict, setDraftConflict] = useState<Error | null>(null);
|
|
674
|
+
const [policy, setPolicy] = useState<ComposerPolicy | null>(
|
|
675
|
+
() => initialShadow?.policy ?? options.initialPolicy ?? null,
|
|
676
|
+
);
|
|
540
677
|
const [restoredResources, setRestoredResources] = useState<ResourceRef[]>(
|
|
541
678
|
() => initialShadow?.resources ?? [],
|
|
542
679
|
);
|
|
@@ -546,11 +683,15 @@ export function useComposer(
|
|
|
546
683
|
const optimisticCallbackIdsRef = useRef(new Set<string>());
|
|
547
684
|
const steeringSettlementEventsRef = useRef<SessionEvent[]>([]);
|
|
548
685
|
const steeringRef = useRef(steering);
|
|
686
|
+
const steeringOccurredAtRef = useRef(new Date().toISOString());
|
|
549
687
|
const pendingClientEventId = useRef<string | null>(
|
|
550
688
|
initialPendingOperation?.input.clientEventId ?? null,
|
|
551
689
|
);
|
|
552
690
|
const valueRef = useRef(initialShadow?.text ?? "");
|
|
553
691
|
const annotationsRef = useRef<DraftTimelineAnnotation[]>(initialShadow?.annotations ?? []);
|
|
692
|
+
const policyRef = useRef<ComposerPolicy | null>(
|
|
693
|
+
initialShadow?.policy ?? options.initialPolicy ?? null,
|
|
694
|
+
);
|
|
554
695
|
const draftRef = useRef<ComposerDraft | null>(null);
|
|
555
696
|
const restoredResourcesRef = useRef<ResourceRef[]>(initialShadow?.resources ?? []);
|
|
556
697
|
const localEditRevision = useRef(initialShadow ? 1 : 0);
|
|
@@ -560,15 +701,8 @@ export function useComposer(
|
|
|
560
701
|
const saveChain = useRef<Promise<void>>(Promise.resolve());
|
|
561
702
|
const onSent = options.onSent;
|
|
562
703
|
const onSubmitted = options.onSubmitted;
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
// apply callback without invalidating the draft loader and re-running its
|
|
566
|
-
// initial-load effect. Publish only committed callbacks: a suspended target
|
|
567
|
-
// render must not retarget an in-flight read owned by the committed session.
|
|
568
|
-
const onDraftAppliedRef = useRef(onDraftApplied);
|
|
569
|
-
useLayoutEffect(() => {
|
|
570
|
-
onDraftAppliedRef.current = onDraftApplied;
|
|
571
|
-
}, [onDraftApplied]);
|
|
704
|
+
const onDeliveryErrorRef = useRef(options.onDeliveryError);
|
|
705
|
+
onDeliveryErrorRef.current = options.onDeliveryError;
|
|
572
706
|
useLayoutEffect(() => {
|
|
573
707
|
steeringRef.current = steering;
|
|
574
708
|
}, [steering]);
|
|
@@ -576,8 +710,14 @@ export function useComposer(
|
|
|
576
710
|
// callers passing inline functions) does not invalidate `send`.
|
|
577
711
|
const sendExtrasRef = useRef(options.sendExtras);
|
|
578
712
|
sendExtrasRef.current = options.sendExtras;
|
|
713
|
+
const sendDestinationRef = useRef(options.sendDestination);
|
|
714
|
+
sendDestinationRef.current = options.sendDestination;
|
|
579
715
|
const sendBlockedRef = useRef(options.sendBlocked);
|
|
580
716
|
sendBlockedRef.current = options.sendBlocked;
|
|
717
|
+
const effectiveControlRef = useRef<EffectiveSessionControl | null>(
|
|
718
|
+
acceptedControl ?? options.effectiveControl ?? null,
|
|
719
|
+
);
|
|
720
|
+
effectiveControlRef.current = acceptedControl ?? options.effectiveControl ?? null;
|
|
581
721
|
const liveExtrasVersion = JSON.stringify(resolveSendExtras(options.sendExtras));
|
|
582
722
|
|
|
583
723
|
// A composer is bound to one session: switching targets must not leak the
|
|
@@ -600,6 +740,7 @@ export function useComposer(
|
|
|
600
740
|
localEditRevision.current = shadow ? 1 : 0;
|
|
601
741
|
valueRef.current = shadow?.text ?? "";
|
|
602
742
|
annotationsRef.current = shadow?.annotations ?? [];
|
|
743
|
+
policyRef.current = shadow?.policy ?? options.initialPolicy ?? null;
|
|
603
744
|
draftRef.current = null;
|
|
604
745
|
restoredResourcesRef.current = shadow?.resources ?? [];
|
|
605
746
|
lastSavedSignature.current = null;
|
|
@@ -609,22 +750,25 @@ export function useComposer(
|
|
|
609
750
|
setStateTargetKey(targetKey);
|
|
610
751
|
setValue(shadow?.text ?? "");
|
|
611
752
|
setAnnotations(shadow?.annotations ?? []);
|
|
753
|
+
setPolicy(policyRef.current);
|
|
612
754
|
setAnnotationReviewTargetId(null);
|
|
613
755
|
setSending(false);
|
|
614
756
|
setOptimisticSends(restoredOptimisticSends);
|
|
615
757
|
setSteering(
|
|
616
758
|
pendingOperationRef.current?.delivery === "steer"
|
|
617
759
|
? {
|
|
618
|
-
phase: "
|
|
760
|
+
phase: "failed",
|
|
619
761
|
text: pendingOperationRef.current.input.text,
|
|
620
762
|
clientEventId: pendingOperationRef.current.input.clientEventId ?? null,
|
|
621
763
|
triggerEventId: null,
|
|
622
764
|
turnId: null,
|
|
765
|
+
error: "Not confirmed. Retry to check the same direction change.",
|
|
766
|
+
outcomeUnknown: true,
|
|
623
767
|
}
|
|
624
768
|
: null,
|
|
625
769
|
);
|
|
626
770
|
setPausing(false);
|
|
627
|
-
|
|
771
|
+
setAcceptedControl(null);
|
|
628
772
|
setResuming(false);
|
|
629
773
|
setError(null);
|
|
630
774
|
setDraft(null);
|
|
@@ -632,7 +776,7 @@ export function useComposer(
|
|
|
632
776
|
setDraftSaving(false);
|
|
633
777
|
setDraftConflict(null);
|
|
634
778
|
setRestoredResources(shadow?.resources ?? []);
|
|
635
|
-
}, [durableDrafts, pendingOperationKey, sessionId, targetKey]);
|
|
779
|
+
}, [durableDrafts, options.initialPolicy, pendingOperationKey, sessionId, targetKey]);
|
|
636
780
|
|
|
637
781
|
const setOptimisticDraftShadow = useCallback(
|
|
638
782
|
(shadow: ComposerDraftShadow): void => {
|
|
@@ -643,6 +787,7 @@ export function useComposer(
|
|
|
643
787
|
text: shadow.text,
|
|
644
788
|
resources: [...shadow.resources],
|
|
645
789
|
annotations: cloneAnnotations(shadow.annotations),
|
|
790
|
+
...(shadow.policy ? { policy: { ...shadow.policy } } : {}),
|
|
646
791
|
},
|
|
647
792
|
}));
|
|
648
793
|
optimisticSendsRef.current = next;
|
|
@@ -656,15 +801,18 @@ export function useComposer(
|
|
|
656
801
|
(next: ComposerDraft): void => {
|
|
657
802
|
if (targetKeyRef.current !== targetKey) return;
|
|
658
803
|
if (!durableDrafts) {
|
|
804
|
+
const nextPolicy = policyFromDraft(next);
|
|
659
805
|
localEditRevision.current += 1;
|
|
660
806
|
valueRef.current = next.text;
|
|
661
807
|
annotationsRef.current = next.annotations ?? [];
|
|
808
|
+
policyRef.current = nextPolicy;
|
|
662
809
|
restoredResourcesRef.current = next.resources;
|
|
663
810
|
draftRef.current = null;
|
|
664
811
|
lastSavedSignature.current = null;
|
|
665
812
|
setDraft(null);
|
|
666
813
|
setValue(next.text);
|
|
667
814
|
setAnnotations(next.annotations ?? []);
|
|
815
|
+
setPolicy(nextPolicy);
|
|
668
816
|
setRestoredResources(next.resources);
|
|
669
817
|
pendingOperationRef.current = updatePendingComposerShadow(
|
|
670
818
|
pendingOperationKey,
|
|
@@ -676,6 +824,7 @@ export function useComposer(
|
|
|
676
824
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
677
825
|
),
|
|
678
826
|
annotations: next.annotations ?? [],
|
|
827
|
+
policy: nextPolicy,
|
|
679
828
|
},
|
|
680
829
|
);
|
|
681
830
|
setOptimisticDraftShadow({
|
|
@@ -685,12 +834,15 @@ export function useComposer(
|
|
|
685
834
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
686
835
|
),
|
|
687
836
|
annotations: next.annotations ?? [],
|
|
837
|
+
policy: nextPolicy,
|
|
688
838
|
});
|
|
689
839
|
setDraftConflict(null);
|
|
690
840
|
return;
|
|
691
841
|
}
|
|
842
|
+
const nextPolicy = policyFromDraft(next);
|
|
692
843
|
valueRef.current = next.text;
|
|
693
844
|
annotationsRef.current = next.annotations ?? [];
|
|
845
|
+
policyRef.current = nextPolicy;
|
|
694
846
|
draftRef.current = next;
|
|
695
847
|
restoredResourcesRef.current = next.resources;
|
|
696
848
|
lastSavedSignature.current = draftSignature(draftPayload(next));
|
|
@@ -698,6 +850,7 @@ export function useComposer(
|
|
|
698
850
|
setDraft(next);
|
|
699
851
|
setValue(next.text);
|
|
700
852
|
setAnnotations(next.annotations ?? []);
|
|
853
|
+
setPolicy(nextPolicy);
|
|
701
854
|
setRestoredResources(next.resources);
|
|
702
855
|
pendingOperationRef.current = updatePendingComposerShadow(
|
|
703
856
|
pendingOperationKey,
|
|
@@ -709,6 +862,7 @@ export function useComposer(
|
|
|
709
862
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
710
863
|
),
|
|
711
864
|
annotations: next.annotations ?? [],
|
|
865
|
+
policy: nextPolicy,
|
|
712
866
|
},
|
|
713
867
|
);
|
|
714
868
|
setOptimisticDraftShadow({
|
|
@@ -718,9 +872,9 @@ export function useComposer(
|
|
|
718
872
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
719
873
|
),
|
|
720
874
|
annotations: next.annotations ?? [],
|
|
875
|
+
policy: nextPolicy,
|
|
721
876
|
});
|
|
722
877
|
setDraftConflict(null);
|
|
723
|
-
onDraftAppliedRef.current?.(next);
|
|
724
878
|
},
|
|
725
879
|
[durableDrafts, pendingOperationKey, setOptimisticDraftShadow, targetKey],
|
|
726
880
|
);
|
|
@@ -736,17 +890,20 @@ export function useComposer(
|
|
|
736
890
|
const readTicket = ++draftReadGeneration.current;
|
|
737
891
|
const localAtStart = localEditRevision.current;
|
|
738
892
|
const baseAtStart = draftRef.current;
|
|
739
|
-
const
|
|
893
|
+
const policyAtStart = policyRef.current;
|
|
740
894
|
const localSignatureAtStart = baseAtStart
|
|
741
|
-
?
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
895
|
+
? policyAtStart
|
|
896
|
+
? draftSignature(
|
|
897
|
+
composerDraftPayload(
|
|
898
|
+
baseAtStart,
|
|
899
|
+
valueRef.current,
|
|
900
|
+
restoredResourcesRef.current,
|
|
901
|
+
annotationsRef.current,
|
|
902
|
+
policyAtStart,
|
|
903
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
904
|
+
),
|
|
905
|
+
)
|
|
906
|
+
: null
|
|
750
907
|
: null;
|
|
751
908
|
const localWasDirtyAtStart =
|
|
752
909
|
localSignatureAtStart === null
|
|
@@ -782,32 +939,35 @@ export function useComposer(
|
|
|
782
939
|
valueRef.current = shadow.text;
|
|
783
940
|
restoredResourcesRef.current = shadow.resources;
|
|
784
941
|
annotationsRef.current = shadow.annotations;
|
|
942
|
+
if (shadow.policy) policyRef.current = shadow.policy;
|
|
785
943
|
localEditRevision.current ||= 1;
|
|
786
944
|
setValue(shadow.text);
|
|
787
945
|
setRestoredResources(shadow.resources);
|
|
788
946
|
setAnnotations(shadow.annotations);
|
|
947
|
+
if (shadow.policy) setPolicy(shadow.policy);
|
|
789
948
|
} else if (
|
|
790
949
|
replaceLocal ||
|
|
791
950
|
(!localWasDirtyAtStart && localAtStart === localEditRevision.current)
|
|
792
951
|
) {
|
|
793
|
-
|
|
794
|
-
// localEditRevision). If
|
|
795
|
-
// the picker changed during this fetch, skip onDraftApplied so a
|
|
796
|
-
// stale server policy cannot undo the operator's pick.
|
|
797
|
-
const extrasNow = resolveSendExtras(sendExtrasRef.current);
|
|
798
|
-
const pickerChangedDuringFetch =
|
|
799
|
-
extrasNow.model !== extrasAtStart.model ||
|
|
800
|
-
extrasNow.reasoningEffort !== extrasAtStart.reasoningEffort ||
|
|
801
|
-
extrasNow.latencyMode !== extrasAtStart.latencyMode;
|
|
952
|
+
const fetchedPolicy = policyFromDraft(fetched);
|
|
802
953
|
valueRef.current = fetched.text;
|
|
803
954
|
restoredResourcesRef.current = fetched.resources;
|
|
804
955
|
annotationsRef.current = fetched.annotations ?? [];
|
|
956
|
+
policyRef.current = fetchedPolicy;
|
|
805
957
|
lastSavedSignature.current = draftSignature(draftPayload(fetched));
|
|
806
958
|
setValue(fetched.text);
|
|
807
959
|
setRestoredResources(fetched.resources);
|
|
808
960
|
setAnnotations(fetched.annotations ?? []);
|
|
809
|
-
|
|
810
|
-
|
|
961
|
+
setPolicy(fetchedPolicy);
|
|
962
|
+
} else {
|
|
963
|
+
// Keep the in-flight edit, but the fetched row is still the OCC
|
|
964
|
+
// base. First hydrate has no local policy until this read lands;
|
|
965
|
+
// without it autosave cannot persist the typed text.
|
|
966
|
+
lastSavedSignature.current = draftSignature(draftPayload(fetched));
|
|
967
|
+
if (!policyRef.current) {
|
|
968
|
+
const fetchedPolicy = policyFromDraft(fetched);
|
|
969
|
+
policyRef.current = fetchedPolicy;
|
|
970
|
+
setPolicy(fetchedPolicy);
|
|
811
971
|
}
|
|
812
972
|
}
|
|
813
973
|
}
|
|
@@ -930,32 +1090,41 @@ export function useComposer(
|
|
|
930
1090
|
}, [options.events, steering]);
|
|
931
1091
|
|
|
932
1092
|
useEffect(() => {
|
|
933
|
-
if (!
|
|
1093
|
+
if (!acceptedControl) return;
|
|
934
1094
|
const effectiveControl = options.effectiveControl;
|
|
935
|
-
if (!effectiveControl || effectiveControl.controlVersion <
|
|
936
|
-
return;
|
|
937
|
-
}
|
|
938
|
-
if (
|
|
939
|
-
effectiveControl.controlVersion === controlStopping.controlVersion &&
|
|
940
|
-
effectiveControl.controlEtag === controlStopping.controlEtag &&
|
|
941
|
-
effectiveControl.settlement !== null
|
|
942
|
-
) {
|
|
1095
|
+
if (!effectiveControl || effectiveControl.controlVersion < acceptedControl.controlVersion) {
|
|
943
1096
|
return;
|
|
944
1097
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
// mutation receipt. SessionChrome then falls back to durable queue truth.
|
|
948
|
-
setControlStopping((current) => (current === controlStopping ? null : current));
|
|
949
|
-
}, [controlStopping, options.effectiveControl]);
|
|
1098
|
+
setAcceptedControl((current) => (current === acceptedControl ? null : current));
|
|
1099
|
+
}, [acceptedControl, options.effectiveControl]);
|
|
950
1100
|
|
|
951
1101
|
const currentDraftPayload = useCallback((): SaveComposerDraftRequest | null => {
|
|
952
1102
|
if (!durableDrafts || targetKeyRef.current !== targetKey) return null;
|
|
953
1103
|
const base = draftRef.current;
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
return composerDraftPayload(
|
|
1104
|
+
const currentPolicy = policyRef.current;
|
|
1105
|
+
if (!base || !currentPolicy) return null;
|
|
1106
|
+
return composerDraftPayload(
|
|
1107
|
+
base,
|
|
1108
|
+
value,
|
|
1109
|
+
restoredResources,
|
|
1110
|
+
annotations,
|
|
1111
|
+
currentPolicy,
|
|
1112
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1113
|
+
);
|
|
957
1114
|
}, [annotations, durableDrafts, restoredResources, targetKey, value]);
|
|
958
1115
|
|
|
1116
|
+
const adoptDraftBase = useCallback(
|
|
1117
|
+
(next: ComposerDraft): void => {
|
|
1118
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
1119
|
+
draftRef.current = next;
|
|
1120
|
+
setDraft(next);
|
|
1121
|
+
lastSavedSignature.current = draftSignature(draftPayload(next));
|
|
1122
|
+
setDraftConflict(null);
|
|
1123
|
+
setError(null);
|
|
1124
|
+
},
|
|
1125
|
+
[targetKey],
|
|
1126
|
+
);
|
|
1127
|
+
|
|
959
1128
|
const persistPayload = useCallback(
|
|
960
1129
|
async (payload: SaveComposerDraftRequest): Promise<boolean> => {
|
|
961
1130
|
const ownedTargetKey = targetKey;
|
|
@@ -981,30 +1150,14 @@ export function useComposer(
|
|
|
981
1150
|
}
|
|
982
1151
|
setDraftSaving(true);
|
|
983
1152
|
try {
|
|
984
|
-
const saved = await
|
|
985
|
-
client,
|
|
986
|
-
workspaceId,
|
|
987
|
-
sessionId,
|
|
988
|
-
request,
|
|
989
|
-
onAdoptRemote: (remote) => {
|
|
990
|
-
draftRef.current = remote;
|
|
991
|
-
setDraft(remote);
|
|
992
|
-
},
|
|
993
|
-
});
|
|
1153
|
+
const saved = await client.saveComposerDraft(workspaceId, sessionId, request);
|
|
994
1154
|
if (
|
|
995
1155
|
targetKeyRef.current !== ownedTargetKey ||
|
|
996
1156
|
targetGeneration.current !== ownedGeneration
|
|
997
1157
|
) {
|
|
998
1158
|
return;
|
|
999
1159
|
}
|
|
1000
|
-
|
|
1001
|
-
setDraft(saved);
|
|
1002
|
-
lastSavedSignature.current = draftSignature({
|
|
1003
|
-
...request,
|
|
1004
|
-
expectedRevision: saved.revision,
|
|
1005
|
-
});
|
|
1006
|
-
setDraftConflict(null);
|
|
1007
|
-
setError(null);
|
|
1160
|
+
adoptDraftBase(saved);
|
|
1008
1161
|
success = true;
|
|
1009
1162
|
} catch (cause) {
|
|
1010
1163
|
if (
|
|
@@ -1028,7 +1181,7 @@ export function useComposer(
|
|
|
1028
1181
|
await saveChain.current;
|
|
1029
1182
|
return success;
|
|
1030
1183
|
},
|
|
1031
|
-
[client, durableDrafts, sessionId, targetKey, workspaceId],
|
|
1184
|
+
[adoptDraftBase, client, durableDrafts, sessionId, targetKey, workspaceId],
|
|
1032
1185
|
);
|
|
1033
1186
|
|
|
1034
1187
|
const replaceOptimisticSends = useCallback(
|
|
@@ -1058,17 +1211,46 @@ export function useComposer(
|
|
|
1058
1211
|
);
|
|
1059
1212
|
|
|
1060
1213
|
useEffect(() => {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1214
|
+
const events = options.events ?? [];
|
|
1215
|
+
if (events.length === 0 || optimisticSendsRef.current.length === 0) return;
|
|
1216
|
+
const reconciled = optimisticSendsRef.current.map((operation) =>
|
|
1217
|
+
reconcileOptimisticSendFromEvents(operation, events),
|
|
1065
1218
|
);
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
acceptedIds.has(operation.clientEventId),
|
|
1219
|
+
const settledIds = new Set(
|
|
1220
|
+
reconciled.filter((item) => item.settled).map((item) => item.operation.clientEventId),
|
|
1069
1221
|
);
|
|
1070
|
-
|
|
1071
|
-
|
|
1222
|
+
const changed = reconciled.some(
|
|
1223
|
+
(item, index) =>
|
|
1224
|
+
item.operation !== optimisticSendsRef.current[index] &&
|
|
1225
|
+
!settledIds.has(item.operation.clientEventId),
|
|
1226
|
+
);
|
|
1227
|
+
for (const item of reconciled) {
|
|
1228
|
+
if (item.admitted && !optimisticCallbackIdsRef.current.has(item.operation.clientEventId)) {
|
|
1229
|
+
optimisticCallbackIdsRef.current.add(item.operation.clientEventId);
|
|
1230
|
+
onSent?.(item.operation.input.text, item.operation.input);
|
|
1231
|
+
}
|
|
1232
|
+
if (item.settled) markOptimisticAccepted(item.operation);
|
|
1233
|
+
}
|
|
1234
|
+
if (changed) {
|
|
1235
|
+
replaceOptimisticSends((current) =>
|
|
1236
|
+
current
|
|
1237
|
+
.map(
|
|
1238
|
+
(operation) =>
|
|
1239
|
+
reconciled.find((item) => item.operation.clientEventId === operation.clientEventId)
|
|
1240
|
+
?.operation ?? operation,
|
|
1241
|
+
)
|
|
1242
|
+
.filter((operation) => !settledIds.has(operation.clientEventId)),
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
if (durableDrafts && reconciled.some((item) => item.admitted)) void loadDraft(false);
|
|
1246
|
+
}, [
|
|
1247
|
+
durableDrafts,
|
|
1248
|
+
loadDraft,
|
|
1249
|
+
markOptimisticAccepted,
|
|
1250
|
+
onSent,
|
|
1251
|
+
options.events,
|
|
1252
|
+
replaceOptimisticSends,
|
|
1253
|
+
]);
|
|
1072
1254
|
|
|
1073
1255
|
const processOptimisticSends = useCallback((): void => {
|
|
1074
1256
|
if (!sessionId || optimisticProcessorBusyRef.current) return;
|
|
@@ -1082,20 +1264,69 @@ export function useComposer(
|
|
|
1082
1264
|
const ownedGeneration = targetGeneration.current;
|
|
1083
1265
|
optimisticProcessorBusyRef.current = true;
|
|
1084
1266
|
void (async () => {
|
|
1267
|
+
let mutationFailureObserved = false;
|
|
1268
|
+
const markFailed = (problem: Error, outcomeUnknown: boolean): void => {
|
|
1269
|
+
if (
|
|
1270
|
+
targetKeyRef.current !== ownedTargetKey ||
|
|
1271
|
+
targetGeneration.current !== ownedGeneration
|
|
1272
|
+
) {
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
replaceOptimisticSends((current) =>
|
|
1276
|
+
current.map((candidate) =>
|
|
1277
|
+
candidate.clientEventId === operation.clientEventId
|
|
1278
|
+
? {
|
|
1279
|
+
...candidate,
|
|
1280
|
+
state: "failed",
|
|
1281
|
+
error: problem.message,
|
|
1282
|
+
outcomeUnknown,
|
|
1283
|
+
}
|
|
1284
|
+
: candidate,
|
|
1285
|
+
),
|
|
1286
|
+
);
|
|
1287
|
+
};
|
|
1085
1288
|
try {
|
|
1086
1289
|
if (operation.outcomeUnknown) {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1290
|
+
let events: SessionEvent[];
|
|
1291
|
+
try {
|
|
1292
|
+
events = await client.listEvents(workspaceId, sessionId, {
|
|
1293
|
+
includeTypes: [
|
|
1294
|
+
"user.message",
|
|
1295
|
+
"turn.queued",
|
|
1296
|
+
"turn.started",
|
|
1297
|
+
"turn.completed",
|
|
1298
|
+
"turn.failed",
|
|
1299
|
+
"turn.cancelled",
|
|
1300
|
+
"turn.superseded",
|
|
1301
|
+
],
|
|
1302
|
+
limit: 250,
|
|
1303
|
+
payloadMode: "full",
|
|
1304
|
+
});
|
|
1305
|
+
} catch (cause) {
|
|
1306
|
+
// This read cannot settle the prior mutation in either direction.
|
|
1307
|
+
// Preserve its exact key and uncertain status until reconciliation
|
|
1308
|
+
// succeeds or that same mutation is actually replayed.
|
|
1309
|
+
markFailed(asError(cause), true);
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
const reconciled = reconcileOptimisticSendFromEvents(operation, events);
|
|
1313
|
+
if (reconciled.admitted) {
|
|
1314
|
+
if (durableDrafts) await loadDraft(false);
|
|
1315
|
+
if (reconciled.settled) {
|
|
1316
|
+
markOptimisticAccepted(reconciled.operation);
|
|
1317
|
+
} else {
|
|
1318
|
+
replaceOptimisticSends((current) =>
|
|
1319
|
+
current.map((candidate) =>
|
|
1320
|
+
candidate.clientEventId === operation.clientEventId
|
|
1321
|
+
? reconciled.operation
|
|
1322
|
+
: candidate,
|
|
1323
|
+
),
|
|
1324
|
+
);
|
|
1325
|
+
if (!optimisticCallbackIdsRef.current.has(operation.clientEventId)) {
|
|
1326
|
+
optimisticCallbackIdsRef.current.add(operation.clientEventId);
|
|
1327
|
+
onSent?.(reconciled.operation.input.text, reconciled.operation.input);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1099
1330
|
return;
|
|
1100
1331
|
}
|
|
1101
1332
|
if (!operation.canRetry) {
|
|
@@ -1121,20 +1352,79 @@ export function useComposer(
|
|
|
1121
1352
|
...operation.input,
|
|
1122
1353
|
...(expectedDraftRevision !== undefined ? { expectedDraftRevision } : {}),
|
|
1123
1354
|
};
|
|
1124
|
-
|
|
1355
|
+
let acceptedResult: SubmitComposerDraftResponse | null = null;
|
|
1356
|
+
let acceptedEvent: SessionEvent | null = null;
|
|
1357
|
+
if (durableDrafts) {
|
|
1358
|
+
if (!operation.draftPayload || expectedDraftRevision === undefined) {
|
|
1359
|
+
throw new Error("The durable composer draft is not ready for delivery.");
|
|
1360
|
+
}
|
|
1361
|
+
const result = await client
|
|
1362
|
+
.submitComposerDraft(workspaceId, sessionId, {
|
|
1363
|
+
text: operation.draftPayload.text,
|
|
1364
|
+
annotations: operation.draftPayload.annotations,
|
|
1365
|
+
resources: operation.draftPayload.resources,
|
|
1366
|
+
model: operation.draftPayload.model,
|
|
1367
|
+
reasoningEffort: operation.draftPayload.reasoningEffort,
|
|
1368
|
+
latencyMode: operation.draftPayload.latencyMode,
|
|
1369
|
+
expectedDraftRevision,
|
|
1370
|
+
clientEventId: operation.clientEventId,
|
|
1371
|
+
delivery: "send",
|
|
1372
|
+
...(wireInput.controlEtag ? { controlEtag: wireInput.controlEtag } : {}),
|
|
1373
|
+
...(wireInput.modelContext ? { modelContext: wireInput.modelContext } : {}),
|
|
1374
|
+
...(wireInput.mcpCredentialUpdates
|
|
1375
|
+
? { mcpCredentialUpdates: wireInput.mcpCredentialUpdates }
|
|
1376
|
+
: {}),
|
|
1377
|
+
...(wireInput.connectionAuthorities
|
|
1378
|
+
? { connectionAuthorities: wireInput.connectionAuthorities }
|
|
1379
|
+
: {}),
|
|
1380
|
+
...(wireInput.personalResourceAttachment
|
|
1381
|
+
? { personalResourceAttachment: wireInput.personalResourceAttachment }
|
|
1382
|
+
: {}),
|
|
1383
|
+
})
|
|
1384
|
+
.catch((cause: unknown) => {
|
|
1385
|
+
mutationFailureObserved = true;
|
|
1386
|
+
throw cause;
|
|
1387
|
+
});
|
|
1388
|
+
acceptedResult = result;
|
|
1389
|
+
adoptDraftBase(result.draft);
|
|
1390
|
+
} else {
|
|
1391
|
+
acceptedEvent = await client
|
|
1392
|
+
.sendMessage(workspaceId, sessionId, wireInput)
|
|
1393
|
+
.catch((cause: unknown) => {
|
|
1394
|
+
mutationFailureObserved = true;
|
|
1395
|
+
throw cause;
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1125
1398
|
if (
|
|
1126
1399
|
targetKeyRef.current !== ownedTargetKey ||
|
|
1127
1400
|
targetGeneration.current !== ownedGeneration
|
|
1128
1401
|
) {
|
|
1129
1402
|
return;
|
|
1130
1403
|
}
|
|
1131
|
-
|
|
1404
|
+
const acceptedDestination =
|
|
1405
|
+
acceptedResult?.routing === "queued_for_execution"
|
|
1406
|
+
? "queue"
|
|
1407
|
+
: acceptedResult?.routing === "accepted_for_execution"
|
|
1408
|
+
? "chat"
|
|
1409
|
+
: operation.destination;
|
|
1410
|
+
if (options.events === undefined && acceptedDestination === "chat") {
|
|
1132
1411
|
markOptimisticAccepted({ ...operation, input: wireInput });
|
|
1133
1412
|
} else {
|
|
1134
1413
|
replaceOptimisticSends((current) =>
|
|
1135
1414
|
current.map((candidate) =>
|
|
1136
1415
|
candidate.clientEventId === operation.clientEventId
|
|
1137
|
-
? {
|
|
1416
|
+
? {
|
|
1417
|
+
...candidate,
|
|
1418
|
+
input: wireInput,
|
|
1419
|
+
destination: acceptedDestination,
|
|
1420
|
+
turnId: acceptedResult?.turn.id ?? candidate.turnId,
|
|
1421
|
+
triggerEventId:
|
|
1422
|
+
acceptedResult?.accepted.id ?? acceptedEvent?.id ?? candidate.triggerEventId,
|
|
1423
|
+
appliedQueueVersion:
|
|
1424
|
+
acceptedResult?.receipt?.appliedQueueVersion ?? candidate.appliedQueueVersion,
|
|
1425
|
+
state: "queued",
|
|
1426
|
+
error: undefined,
|
|
1427
|
+
}
|
|
1138
1428
|
: candidate,
|
|
1139
1429
|
),
|
|
1140
1430
|
);
|
|
@@ -1143,41 +1433,24 @@ export function useComposer(
|
|
|
1143
1433
|
optimisticCallbackIdsRef.current.add(operation.clientEventId);
|
|
1144
1434
|
onSent?.(wireInput.text, wireInput);
|
|
1145
1435
|
}
|
|
1146
|
-
if (durableDrafts && draftRef.current) {
|
|
1147
|
-
const cleared = {
|
|
1148
|
-
...draftRef.current,
|
|
1149
|
-
revision: 0,
|
|
1150
|
-
text: "",
|
|
1151
|
-
resources: [],
|
|
1152
|
-
annotations: [],
|
|
1153
|
-
sourceTurnId: null,
|
|
1154
|
-
sourceTurnVersion: null,
|
|
1155
|
-
updatedAt: null,
|
|
1156
|
-
};
|
|
1157
|
-
draftRef.current = cleared;
|
|
1158
|
-
setDraft(cleared);
|
|
1159
|
-
lastSavedSignature.current = draftSignature(draftPayload(cleared));
|
|
1160
|
-
}
|
|
1161
1436
|
} catch (cause) {
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
) {
|
|
1168
|
-
|
|
1169
|
-
current.map((candidate) =>
|
|
1170
|
-
candidate.clientEventId === operation.clientEventId
|
|
1171
|
-
? {
|
|
1172
|
-
...candidate,
|
|
1173
|
-
state: "failed",
|
|
1174
|
-
error: problem.message,
|
|
1175
|
-
outcomeUnknown,
|
|
1176
|
-
}
|
|
1177
|
-
: candidate,
|
|
1178
|
-
),
|
|
1179
|
-
);
|
|
1437
|
+
const current = optimisticSendsRef.current.find(
|
|
1438
|
+
(candidate) => candidate.clientEventId === operation.clientEventId,
|
|
1439
|
+
);
|
|
1440
|
+
// A live durable event can prove admission before the original HTTP
|
|
1441
|
+
// response times out. Never regress that confirmed state to failure.
|
|
1442
|
+
if (!current || (current.state === "queued" && current.outcomeUnknown === false)) {
|
|
1443
|
+
return;
|
|
1180
1444
|
}
|
|
1445
|
+
const problem = asError(cause);
|
|
1446
|
+
onDeliveryErrorRef.current?.(problem, operation.input, "send");
|
|
1447
|
+
// Only an actual mutation replay can replace prior uncertainty with a
|
|
1448
|
+
// definitive response. Local preparation failures remain retry-only.
|
|
1449
|
+
const outcomeUnknown =
|
|
1450
|
+
operation.outcomeUnknown && !mutationFailureObserved
|
|
1451
|
+
? true
|
|
1452
|
+
: isOutcomeUnknownError(cause);
|
|
1453
|
+
markFailed(problem, outcomeUnknown);
|
|
1181
1454
|
} finally {
|
|
1182
1455
|
optimisticProcessorBusyRef.current = false;
|
|
1183
1456
|
if (targetKeyRef.current === ownedTargetKey) {
|
|
@@ -1186,9 +1459,11 @@ export function useComposer(
|
|
|
1186
1459
|
}
|
|
1187
1460
|
})();
|
|
1188
1461
|
}, [
|
|
1462
|
+
adoptDraftBase,
|
|
1189
1463
|
client,
|
|
1190
1464
|
durableDrafts,
|
|
1191
1465
|
markOptimisticAccepted,
|
|
1466
|
+
loadDraft,
|
|
1192
1467
|
onSent,
|
|
1193
1468
|
options.events,
|
|
1194
1469
|
persistPayload,
|
|
@@ -1214,11 +1489,13 @@ export function useComposer(
|
|
|
1214
1489
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1215
1490
|
),
|
|
1216
1491
|
annotations: annotationsRef.current,
|
|
1492
|
+
...(policyRef.current ? { policy: policyRef.current } : {}),
|
|
1217
1493
|
};
|
|
1218
1494
|
if (
|
|
1219
1495
|
pending.newerShadow.text !== shadow.text ||
|
|
1220
1496
|
JSON.stringify(pending.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
|
|
1221
|
-
JSON.stringify(pending.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
|
|
1497
|
+
JSON.stringify(pending.newerShadow.annotations) !== JSON.stringify(shadow.annotations) ||
|
|
1498
|
+
JSON.stringify(pending.newerShadow.policy) !== JSON.stringify(shadow.policy)
|
|
1222
1499
|
) {
|
|
1223
1500
|
pendingOperationRef.current = updatePendingComposerShadow(
|
|
1224
1501
|
pendingOperationKey,
|
|
@@ -1237,11 +1514,13 @@ export function useComposer(
|
|
|
1237
1514
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1238
1515
|
),
|
|
1239
1516
|
annotations: annotationsRef.current,
|
|
1517
|
+
...(policyRef.current ? { policy: policyRef.current } : {}),
|
|
1240
1518
|
};
|
|
1241
1519
|
if (
|
|
1242
1520
|
optimistic.newerShadow.text !== shadow.text ||
|
|
1243
1521
|
JSON.stringify(optimistic.newerShadow.resources) !== JSON.stringify(shadow.resources) ||
|
|
1244
|
-
JSON.stringify(optimistic.newerShadow.annotations) !== JSON.stringify(shadow.annotations)
|
|
1522
|
+
JSON.stringify(optimistic.newerShadow.annotations) !== JSON.stringify(shadow.annotations) ||
|
|
1523
|
+
JSON.stringify(optimistic.newerShadow.policy) !== JSON.stringify(shadow.policy)
|
|
1245
1524
|
) {
|
|
1246
1525
|
setOptimisticDraftShadow(shadow);
|
|
1247
1526
|
}
|
|
@@ -1290,6 +1569,7 @@ export function useComposer(
|
|
|
1290
1569
|
const rawText = explicit ?? draftAtSend;
|
|
1291
1570
|
const hasText = rawText.trim().length > 0;
|
|
1292
1571
|
const hasAnnotations = annotationsAtSend.length > 0;
|
|
1572
|
+
const currentPolicy = policyRef.current;
|
|
1293
1573
|
const annotationsComplete = annotationsAtSend.every(
|
|
1294
1574
|
(annotation) => annotation.note.trim().length > 0,
|
|
1295
1575
|
);
|
|
@@ -1300,6 +1580,7 @@ export function useComposer(
|
|
|
1300
1580
|
if (
|
|
1301
1581
|
(!pending && (!annotationsComplete || (!hasText && !hasResources && !hasAnnotations))) ||
|
|
1302
1582
|
!sessionId ||
|
|
1583
|
+
!currentPolicy ||
|
|
1303
1584
|
sending ||
|
|
1304
1585
|
sendBlockedRef.current?.() === true ||
|
|
1305
1586
|
targetKeyRef.current !== ownedTargetKey
|
|
@@ -1323,22 +1604,6 @@ export function useComposer(
|
|
|
1323
1604
|
JSON.stringify(operation.resourcesAtSend);
|
|
1324
1605
|
const annotationsWereUnchanged =
|
|
1325
1606
|
JSON.stringify(annotationsRef.current) === JSON.stringify(operation.annotationsAtSend);
|
|
1326
|
-
const previousDraft = draftRef.current;
|
|
1327
|
-
if (previousDraft) {
|
|
1328
|
-
const cleared = {
|
|
1329
|
-
...previousDraft,
|
|
1330
|
-
revision: 0,
|
|
1331
|
-
text: "",
|
|
1332
|
-
resources: [],
|
|
1333
|
-
annotations: [],
|
|
1334
|
-
sourceTurnId: null,
|
|
1335
|
-
sourceTurnVersion: null,
|
|
1336
|
-
updatedAt: null,
|
|
1337
|
-
};
|
|
1338
|
-
draftRef.current = cleared;
|
|
1339
|
-
setDraft(cleared);
|
|
1340
|
-
lastSavedSignature.current = draftSignature(draftPayload(cleared));
|
|
1341
|
-
}
|
|
1342
1607
|
if (resourcesWereUnchanged) {
|
|
1343
1608
|
restoredResourcesRef.current = [];
|
|
1344
1609
|
setRestoredResources([]);
|
|
@@ -1356,6 +1621,42 @@ export function useComposer(
|
|
|
1356
1621
|
};
|
|
1357
1622
|
|
|
1358
1623
|
const deliver = async (operation: PendingComposerOperation) => {
|
|
1624
|
+
if (durableDrafts) {
|
|
1625
|
+
const input = operation.input;
|
|
1626
|
+
if (
|
|
1627
|
+
input.expectedDraftRevision === undefined ||
|
|
1628
|
+
input.clientEventId === undefined ||
|
|
1629
|
+
input.model === undefined ||
|
|
1630
|
+
input.reasoningEffort === undefined ||
|
|
1631
|
+
input.latencyMode === undefined
|
|
1632
|
+
) {
|
|
1633
|
+
throw new Error("The durable composer draft is not ready for delivery.");
|
|
1634
|
+
}
|
|
1635
|
+
const result = await client.submitComposerDraft(workspaceId, sessionId, {
|
|
1636
|
+
text: input.text,
|
|
1637
|
+
annotations: input.annotations ?? [],
|
|
1638
|
+
resources: input.resources ?? [],
|
|
1639
|
+
model: input.model,
|
|
1640
|
+
reasoningEffort: input.reasoningEffort,
|
|
1641
|
+
latencyMode: input.latencyMode,
|
|
1642
|
+
expectedDraftRevision: input.expectedDraftRevision,
|
|
1643
|
+
clientEventId: input.clientEventId,
|
|
1644
|
+
delivery: operation.delivery,
|
|
1645
|
+
...(input.controlEtag ? { controlEtag: input.controlEtag } : {}),
|
|
1646
|
+
...(input.modelContext ? { modelContext: input.modelContext } : {}),
|
|
1647
|
+
...(input.mcpCredentialUpdates
|
|
1648
|
+
? { mcpCredentialUpdates: input.mcpCredentialUpdates }
|
|
1649
|
+
: {}),
|
|
1650
|
+
...(input.connectionAuthorities
|
|
1651
|
+
? { connectionAuthorities: input.connectionAuthorities }
|
|
1652
|
+
: {}),
|
|
1653
|
+
...(input.personalResourceAttachment
|
|
1654
|
+
? { personalResourceAttachment: input.personalResourceAttachment }
|
|
1655
|
+
: {}),
|
|
1656
|
+
});
|
|
1657
|
+
adoptDraftBase(result.draft);
|
|
1658
|
+
return result;
|
|
1659
|
+
}
|
|
1359
1660
|
if (operation.delivery === "steer") {
|
|
1360
1661
|
return await client.steerMessage(workspaceId, sessionId, operation.input);
|
|
1361
1662
|
}
|
|
@@ -1364,6 +1665,9 @@ export function useComposer(
|
|
|
1364
1665
|
};
|
|
1365
1666
|
|
|
1366
1667
|
if (delivery === "steer") {
|
|
1668
|
+
if (steeringRef.current?.clientEventId !== pending?.input.clientEventId) {
|
|
1669
|
+
steeringOccurredAtRef.current = new Date().toISOString();
|
|
1670
|
+
}
|
|
1367
1671
|
setSteering({
|
|
1368
1672
|
phase: "submitting",
|
|
1369
1673
|
text: rawText,
|
|
@@ -1394,7 +1698,20 @@ export function useComposer(
|
|
|
1394
1698
|
targetKeyRef.current === ownedTargetKey &&
|
|
1395
1699
|
targetGeneration.current === ownedGeneration
|
|
1396
1700
|
) {
|
|
1397
|
-
|
|
1701
|
+
const problem = asError(cause);
|
|
1702
|
+
setError(problem);
|
|
1703
|
+
if (pending.delivery === "steer") {
|
|
1704
|
+
keepSteering = true;
|
|
1705
|
+
setSteering({
|
|
1706
|
+
phase: "failed",
|
|
1707
|
+
text: pending.input.text,
|
|
1708
|
+
clientEventId: pending.input.clientEventId ?? null,
|
|
1709
|
+
triggerEventId: null,
|
|
1710
|
+
turnId: null,
|
|
1711
|
+
error: "Not confirmed. Retry.",
|
|
1712
|
+
outcomeUnknown: true,
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1398
1715
|
}
|
|
1399
1716
|
return false;
|
|
1400
1717
|
}
|
|
@@ -1405,6 +1722,7 @@ export function useComposer(
|
|
|
1405
1722
|
return false;
|
|
1406
1723
|
}
|
|
1407
1724
|
if (acceptedEvent) {
|
|
1725
|
+
if (durableDrafts) await loadDraft(false);
|
|
1408
1726
|
if (pending.delivery === "steer") {
|
|
1409
1727
|
keepSteering = true;
|
|
1410
1728
|
setSteering({
|
|
@@ -1441,12 +1759,29 @@ export function useComposer(
|
|
|
1441
1759
|
});
|
|
1442
1760
|
}
|
|
1443
1761
|
} catch (cause) {
|
|
1444
|
-
|
|
1762
|
+
const problem = asError(cause);
|
|
1763
|
+
const outcomeUnknown = isOutcomeUnknownError(cause);
|
|
1764
|
+
onDeliveryErrorRef.current?.(problem, pending.input, pending.delivery);
|
|
1765
|
+
if (!outcomeUnknown) {
|
|
1766
|
+
clearPending();
|
|
1767
|
+
keepSteering = false;
|
|
1768
|
+
} else if (pending.delivery === "steer") {
|
|
1769
|
+
keepSteering = true;
|
|
1770
|
+
setSteering({
|
|
1771
|
+
phase: "failed",
|
|
1772
|
+
text: pending.input.text,
|
|
1773
|
+
clientEventId: pending.input.clientEventId ?? null,
|
|
1774
|
+
triggerEventId: null,
|
|
1775
|
+
turnId: null,
|
|
1776
|
+
error: "Not confirmed. Retry.",
|
|
1777
|
+
outcomeUnknown: true,
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1445
1780
|
if (
|
|
1446
1781
|
targetKeyRef.current === ownedTargetKey &&
|
|
1447
1782
|
targetGeneration.current === ownedGeneration
|
|
1448
1783
|
) {
|
|
1449
|
-
setError(
|
|
1784
|
+
setError(problem);
|
|
1450
1785
|
}
|
|
1451
1786
|
return false;
|
|
1452
1787
|
}
|
|
@@ -1478,10 +1813,10 @@ export function useComposer(
|
|
|
1478
1813
|
// worker rejects whitespace-only text; a file-only message therefore
|
|
1479
1814
|
// carries a minimal default so the attachments still get delivered.
|
|
1480
1815
|
pendingClientEventId.current ??= generateClientEventId();
|
|
1816
|
+
const effectiveControl = effectiveControlRef.current;
|
|
1481
1817
|
const input = composeSendInput(sendText, pendingClientEventId.current, extras, {
|
|
1482
|
-
...
|
|
1483
|
-
|
|
1484
|
-
: {}),
|
|
1818
|
+
...currentPolicy,
|
|
1819
|
+
...(effectiveControl?.controlEtag ? { controlEtag: effectiveControl.controlEtag } : {}),
|
|
1485
1820
|
...(durableDrafts && draftRef.current
|
|
1486
1821
|
? { expectedDraftRevision: draftRef.current.revision }
|
|
1487
1822
|
: {}),
|
|
@@ -1498,6 +1833,7 @@ export function useComposer(
|
|
|
1498
1833
|
text: draftAtSend,
|
|
1499
1834
|
resources: mergeResources(restoredResources, extras.resources ?? []),
|
|
1500
1835
|
annotations: cloneAnnotations(annotationsAtSend),
|
|
1836
|
+
policy: currentPolicy,
|
|
1501
1837
|
},
|
|
1502
1838
|
clearDraftOnAccept: explicit === undefined,
|
|
1503
1839
|
canRetry: true,
|
|
@@ -1528,10 +1864,20 @@ export function useComposer(
|
|
|
1528
1864
|
});
|
|
1529
1865
|
}
|
|
1530
1866
|
} catch (cause) {
|
|
1867
|
+
onDeliveryErrorRef.current?.(asError(cause), operation.input, operation.delivery);
|
|
1531
1868
|
if (!isOutcomeUnknownError(cause)) {
|
|
1532
1869
|
clearPending();
|
|
1533
1870
|
} else if (delivery === "steer") {
|
|
1534
1871
|
keepSteering = true;
|
|
1872
|
+
setSteering({
|
|
1873
|
+
phase: "failed",
|
|
1874
|
+
text: operation.input.text,
|
|
1875
|
+
clientEventId: operation.input.clientEventId ?? null,
|
|
1876
|
+
triggerEventId: null,
|
|
1877
|
+
turnId: null,
|
|
1878
|
+
error: "Not confirmed. Retry.",
|
|
1879
|
+
outcomeUnknown: true,
|
|
1880
|
+
});
|
|
1535
1881
|
}
|
|
1536
1882
|
if (
|
|
1537
1883
|
targetKeyRef.current === ownedTargetKey &&
|
|
@@ -1564,15 +1910,16 @@ export function useComposer(
|
|
|
1564
1910
|
currentDraftPayload,
|
|
1565
1911
|
durableDrafts,
|
|
1566
1912
|
onSent,
|
|
1567
|
-
options.effectiveControl?.controlEtag,
|
|
1568
1913
|
persistPayload,
|
|
1569
1914
|
restoredResources,
|
|
1570
1915
|
annotations,
|
|
1916
|
+
adoptDraftBase,
|
|
1571
1917
|
sending,
|
|
1572
1918
|
sessionId,
|
|
1573
1919
|
targetKey,
|
|
1574
1920
|
value,
|
|
1575
1921
|
workspaceId,
|
|
1922
|
+
loadDraft,
|
|
1576
1923
|
],
|
|
1577
1924
|
);
|
|
1578
1925
|
|
|
@@ -1583,6 +1930,7 @@ export function useComposer(
|
|
|
1583
1930
|
}
|
|
1584
1931
|
const rawText = explicit ?? valueRef.current;
|
|
1585
1932
|
const annotationsAtSend = cloneAnnotations(annotationsRef.current);
|
|
1933
|
+
const currentPolicy = policyRef.current;
|
|
1586
1934
|
const hasText = rawText.trim().length > 0;
|
|
1587
1935
|
const hasAnnotations = annotationsAtSend.length > 0;
|
|
1588
1936
|
const annotationsComplete = annotationsAtSend.every(
|
|
@@ -1592,6 +1940,7 @@ export function useComposer(
|
|
|
1592
1940
|
const resources = mergeResources(restoredResourcesRef.current, extras.resources ?? []);
|
|
1593
1941
|
if (
|
|
1594
1942
|
!sessionId ||
|
|
1943
|
+
!currentPolicy ||
|
|
1595
1944
|
sending ||
|
|
1596
1945
|
!annotationsComplete ||
|
|
1597
1946
|
(!hasText && !hasAnnotations && resources.length === 0) ||
|
|
@@ -1602,16 +1951,24 @@ export function useComposer(
|
|
|
1602
1951
|
}
|
|
1603
1952
|
const sendText = hasText ? rawText : hasAnnotations ? "" : FILE_ONLY_MESSAGE_TEXT;
|
|
1604
1953
|
const clientEventId = generateClientEventId();
|
|
1954
|
+
const effectiveControl = effectiveControlRef.current;
|
|
1605
1955
|
const input = composeSendInput(sendText, clientEventId, extras, {
|
|
1606
|
-
...
|
|
1607
|
-
|
|
1608
|
-
: {}),
|
|
1956
|
+
...currentPolicy,
|
|
1957
|
+
...(effectiveControl?.controlEtag ? { controlEtag: effectiveControl.controlEtag } : {}),
|
|
1609
1958
|
resources,
|
|
1610
1959
|
annotations: annotationsAtSend,
|
|
1611
1960
|
});
|
|
1612
1961
|
const currentPayload = currentDraftPayload();
|
|
1962
|
+
const hasEarlierUnsettledSend = optimisticSendsRef.current.some(
|
|
1963
|
+
(candidate) => candidate.state !== "failed" || candidate.outcomeUnknown === true,
|
|
1964
|
+
);
|
|
1613
1965
|
const operation: OptimisticSendOperation = {
|
|
1614
1966
|
clientEventId,
|
|
1967
|
+
delivery: "send",
|
|
1968
|
+
destination:
|
|
1969
|
+
effectiveControl?.state === "paused" || hasEarlierUnsettledSend
|
|
1970
|
+
? "queue"
|
|
1971
|
+
: (sendDestinationRef.current?.() ?? "chat"),
|
|
1615
1972
|
text: sendText,
|
|
1616
1973
|
annotations: annotationsAtSend,
|
|
1617
1974
|
resources,
|
|
@@ -1623,6 +1980,7 @@ export function useComposer(
|
|
|
1623
1980
|
text: explicit === undefined ? "" : valueRef.current,
|
|
1624
1981
|
resources: explicit === undefined ? [] : [...restoredResourcesRef.current],
|
|
1625
1982
|
annotations: explicit === undefined ? [] : cloneAnnotations(annotationsRef.current),
|
|
1983
|
+
policy: currentPolicy,
|
|
1626
1984
|
},
|
|
1627
1985
|
canRetry: true,
|
|
1628
1986
|
};
|
|
@@ -1646,7 +2004,6 @@ export function useComposer(
|
|
|
1646
2004
|
currentDraftPayload,
|
|
1647
2005
|
dispatch,
|
|
1648
2006
|
onSubmitted,
|
|
1649
|
-
options.effectiveControl?.controlEtag,
|
|
1650
2007
|
processOptimisticSends,
|
|
1651
2008
|
replaceOptimisticSends,
|
|
1652
2009
|
sending,
|
|
@@ -1658,6 +2015,12 @@ export function useComposer(
|
|
|
1658
2015
|
|
|
1659
2016
|
const retryOptimisticMessage = useCallback(
|
|
1660
2017
|
(clientEventId: string): void => {
|
|
2018
|
+
if (sendBlockedRef.current?.() === true) return;
|
|
2019
|
+
const currentSteering = steeringRef.current;
|
|
2020
|
+
if (currentSteering?.phase === "failed" && currentSteering.clientEventId === clientEventId) {
|
|
2021
|
+
void dispatch("steer");
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
1661
2024
|
replaceOptimisticSends((current) =>
|
|
1662
2025
|
current.map((operation) => {
|
|
1663
2026
|
if (operation.clientEventId !== clientEventId || operation.state !== "failed") {
|
|
@@ -1667,27 +2030,31 @@ export function useComposer(
|
|
|
1667
2030
|
return { ...operation, state: "sending", error: undefined };
|
|
1668
2031
|
}
|
|
1669
2032
|
const nextClientEventId = generateClientEventId();
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2033
|
+
const currentPersonalResourceAttachment = resolveSendExtras(
|
|
2034
|
+
sendExtrasRef.current,
|
|
2035
|
+
).personalResourceAttachment;
|
|
2036
|
+
const retryInput = composeSendInput(
|
|
2037
|
+
operation.text,
|
|
2038
|
+
nextClientEventId,
|
|
2039
|
+
{
|
|
2040
|
+
...operation.input,
|
|
2041
|
+
...(currentPersonalResourceAttachment
|
|
2042
|
+
? { personalResourceAttachment: currentPersonalResourceAttachment }
|
|
2043
|
+
: {}),
|
|
2044
|
+
},
|
|
2045
|
+
{
|
|
2046
|
+
...(effectiveControlRef.current?.controlEtag
|
|
2047
|
+
? { controlEtag: effectiveControlRef.current.controlEtag }
|
|
2048
|
+
: {}),
|
|
2049
|
+
resources: operation.resources,
|
|
2050
|
+
annotations: operation.annotations,
|
|
2051
|
+
},
|
|
2052
|
+
);
|
|
1678
2053
|
return {
|
|
1679
2054
|
...operation,
|
|
1680
2055
|
clientEventId: nextClientEventId,
|
|
1681
2056
|
input: retryInput,
|
|
1682
|
-
draftPayload: operation.draftPayload
|
|
1683
|
-
? {
|
|
1684
|
-
...operation.draftPayload,
|
|
1685
|
-
model: retryExtras.model ?? operation.draftPayload.model,
|
|
1686
|
-
reasoningEffort:
|
|
1687
|
-
retryExtras.reasoningEffort ?? operation.draftPayload.reasoningEffort,
|
|
1688
|
-
latencyMode: retryExtras.latencyMode ?? operation.draftPayload.latencyMode,
|
|
1689
|
-
}
|
|
1690
|
-
: null,
|
|
2057
|
+
draftPayload: operation.draftPayload,
|
|
1691
2058
|
occurredAt: new Date().toISOString(),
|
|
1692
2059
|
state: "sending",
|
|
1693
2060
|
error: undefined,
|
|
@@ -1698,16 +2065,24 @@ export function useComposer(
|
|
|
1698
2065
|
);
|
|
1699
2066
|
queueMicrotask(processOptimisticSends);
|
|
1700
2067
|
},
|
|
1701
|
-
[
|
|
2068
|
+
[dispatch, processOptimisticSends, replaceOptimisticSends],
|
|
1702
2069
|
);
|
|
1703
2070
|
|
|
1704
2071
|
const removeOptimisticMessage = useCallback(
|
|
1705
2072
|
(clientEventId: string): void => {
|
|
2073
|
+
if (steeringRef.current?.clientEventId === clientEventId) {
|
|
2074
|
+
pendingOperationRef.current = null;
|
|
2075
|
+
pendingClientEventId.current = null;
|
|
2076
|
+
forgetPendingComposerOperation(pendingComposerOperationKey(workspaceId, sessionId));
|
|
2077
|
+
setSteering(null);
|
|
2078
|
+
setError(null);
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
1706
2081
|
replaceOptimisticSends((current) =>
|
|
1707
2082
|
current.filter((operation) => operation.clientEventId !== clientEventId),
|
|
1708
2083
|
);
|
|
1709
2084
|
},
|
|
1710
|
-
[replaceOptimisticSends],
|
|
2085
|
+
[replaceOptimisticSends, sessionId, workspaceId],
|
|
1711
2086
|
);
|
|
1712
2087
|
|
|
1713
2088
|
// A send is possible with non-empty text OR with ≥1 attached resource (a
|
|
@@ -1731,26 +2106,22 @@ export function useComposer(
|
|
|
1731
2106
|
setPausing(true);
|
|
1732
2107
|
setError(null);
|
|
1733
2108
|
try {
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
2109
|
+
const clientEventId = generateClientEventId();
|
|
2110
|
+
const effectiveControl = effectiveControlRef.current;
|
|
2111
|
+
const result = await replayOutcomeUnknown(() =>
|
|
2112
|
+
client.pauseSession(workspaceId, sessionId, {
|
|
2113
|
+
clientEventId,
|
|
2114
|
+
...(reason !== undefined ? { reason } : {}),
|
|
2115
|
+
...(effectiveControl?.controlEtag
|
|
2116
|
+
? { expectedControlEtag: effectiveControl.controlEtag }
|
|
2117
|
+
: {}),
|
|
2118
|
+
}),
|
|
2119
|
+
);
|
|
1740
2120
|
if (
|
|
1741
2121
|
targetKeyRef.current === ownedTargetKey &&
|
|
1742
2122
|
targetGeneration.current === ownedGeneration
|
|
1743
2123
|
) {
|
|
1744
|
-
|
|
1745
|
-
result.interruptionCount > 0 &&
|
|
1746
|
-
result.effectiveControl.state === "paused" &&
|
|
1747
|
-
result.effectiveControl.settlement !== null
|
|
1748
|
-
? {
|
|
1749
|
-
controlVersion: result.effectiveControl.controlVersion,
|
|
1750
|
-
controlEtag: result.effectiveControl.controlEtag,
|
|
1751
|
-
}
|
|
1752
|
-
: null,
|
|
1753
|
-
);
|
|
2124
|
+
setAcceptedControl(result.effectiveControl);
|
|
1754
2125
|
}
|
|
1755
2126
|
} catch (cause) {
|
|
1756
2127
|
if (
|
|
@@ -1768,7 +2139,7 @@ export function useComposer(
|
|
|
1768
2139
|
}
|
|
1769
2140
|
}
|
|
1770
2141
|
},
|
|
1771
|
-
[client, workspaceId, sessionId, pausing,
|
|
2142
|
+
[client, workspaceId, sessionId, pausing, targetKey],
|
|
1772
2143
|
);
|
|
1773
2144
|
|
|
1774
2145
|
const resume = useCallback(
|
|
@@ -1779,12 +2150,23 @@ export function useComposer(
|
|
|
1779
2150
|
setResuming(true);
|
|
1780
2151
|
setError(null);
|
|
1781
2152
|
try {
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2153
|
+
const clientEventId = generateClientEventId();
|
|
2154
|
+
const effectiveControl = effectiveControlRef.current;
|
|
2155
|
+
const result = await replayOutcomeUnknown(() =>
|
|
2156
|
+
client.resumeSession(workspaceId, sessionId, {
|
|
2157
|
+
clientEventId,
|
|
2158
|
+
...(reason !== undefined ? { reason } : {}),
|
|
2159
|
+
...(effectiveControl?.controlEtag
|
|
2160
|
+
? { expectedControlEtag: effectiveControl.controlEtag }
|
|
2161
|
+
: {}),
|
|
2162
|
+
}),
|
|
2163
|
+
);
|
|
2164
|
+
if (
|
|
2165
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
2166
|
+
targetGeneration.current === ownedGeneration
|
|
2167
|
+
) {
|
|
2168
|
+
setAcceptedControl(result.effectiveControl);
|
|
2169
|
+
}
|
|
1788
2170
|
} catch (cause) {
|
|
1789
2171
|
if (
|
|
1790
2172
|
targetKeyRef.current === ownedTargetKey &&
|
|
@@ -1801,7 +2183,7 @@ export function useComposer(
|
|
|
1801
2183
|
}
|
|
1802
2184
|
}
|
|
1803
2185
|
},
|
|
1804
|
-
[client, workspaceId, sessionId, resuming,
|
|
2186
|
+
[client, workspaceId, sessionId, resuming, targetKey],
|
|
1805
2187
|
);
|
|
1806
2188
|
|
|
1807
2189
|
const resumeScope = useCallback(
|
|
@@ -1813,7 +2195,7 @@ export function useComposer(
|
|
|
1813
2195
|
setError(null);
|
|
1814
2196
|
try {
|
|
1815
2197
|
if (option.scope === "workspace") {
|
|
1816
|
-
const workspaceBlocker =
|
|
2198
|
+
const workspaceBlocker = effectiveControlRef.current?.blockers.find(
|
|
1817
2199
|
(blocker) => blocker.kind === "workspace",
|
|
1818
2200
|
);
|
|
1819
2201
|
if (!client.setWorkspaceInferenceState) {
|
|
@@ -1821,11 +2203,14 @@ export function useComposer(
|
|
|
1821
2203
|
"@opengeni/react: workspace-scoped resume requires setWorkspaceInferenceState.",
|
|
1822
2204
|
);
|
|
1823
2205
|
}
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2206
|
+
const clientEventId = generateClientEventId();
|
|
2207
|
+
await replayOutcomeUnknown(() =>
|
|
2208
|
+
client.setWorkspaceInferenceState!(workspaceId, {
|
|
2209
|
+
action: "resume",
|
|
2210
|
+
clientEventId,
|
|
2211
|
+
...(workspaceBlocker ? { expectedRevision: workspaceBlocker.revision } : {}),
|
|
2212
|
+
}),
|
|
2213
|
+
);
|
|
1829
2214
|
} else if (option.scope === "session" && option.targetId) {
|
|
1830
2215
|
const target = await client.getQueue(workspaceId, option.targetId);
|
|
1831
2216
|
if (
|
|
@@ -1834,15 +2219,30 @@ export function useComposer(
|
|
|
1834
2219
|
) {
|
|
1835
2220
|
return;
|
|
1836
2221
|
}
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2222
|
+
const clientEventId = generateClientEventId();
|
|
2223
|
+
await replayOutcomeUnknown(() =>
|
|
2224
|
+
client.resumeSession(workspaceId, option.targetId!, {
|
|
2225
|
+
clientEventId,
|
|
2226
|
+
expectedControlEtag: target.effectiveControl.controlEtag,
|
|
2227
|
+
}),
|
|
2228
|
+
);
|
|
1840
2229
|
} else {
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
2230
|
+
const clientEventId = generateClientEventId();
|
|
2231
|
+
const effectiveControl = effectiveControlRef.current;
|
|
2232
|
+
const result = await replayOutcomeUnknown(() =>
|
|
2233
|
+
client.resumeSession(workspaceId, sessionId, {
|
|
2234
|
+
clientEventId,
|
|
2235
|
+
...(effectiveControl?.controlEtag
|
|
2236
|
+
? { expectedControlEtag: effectiveControl.controlEtag }
|
|
2237
|
+
: {}),
|
|
2238
|
+
}),
|
|
2239
|
+
);
|
|
2240
|
+
if (
|
|
2241
|
+
targetKeyRef.current === ownedTargetKey &&
|
|
2242
|
+
targetGeneration.current === ownedGeneration
|
|
2243
|
+
) {
|
|
2244
|
+
setAcceptedControl(result.effectiveControl);
|
|
2245
|
+
}
|
|
1846
2246
|
}
|
|
1847
2247
|
} catch (cause) {
|
|
1848
2248
|
if (
|
|
@@ -1860,7 +2260,7 @@ export function useComposer(
|
|
|
1860
2260
|
}
|
|
1861
2261
|
}
|
|
1862
2262
|
},
|
|
1863
|
-
[client,
|
|
2263
|
+
[client, resuming, sessionId, targetKey, workspaceId],
|
|
1864
2264
|
);
|
|
1865
2265
|
|
|
1866
2266
|
const updateValue = useCallback(
|
|
@@ -1878,6 +2278,7 @@ export function useComposer(
|
|
|
1878
2278
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1879
2279
|
),
|
|
1880
2280
|
annotations: annotationsRef.current,
|
|
2281
|
+
policy: policyRef.current ?? undefined,
|
|
1881
2282
|
},
|
|
1882
2283
|
);
|
|
1883
2284
|
setOptimisticDraftShadow({
|
|
@@ -1887,6 +2288,7 @@ export function useComposer(
|
|
|
1887
2288
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1888
2289
|
),
|
|
1889
2290
|
annotations: annotationsRef.current,
|
|
2291
|
+
policy: policyRef.current ?? undefined,
|
|
1890
2292
|
});
|
|
1891
2293
|
setValue(next);
|
|
1892
2294
|
},
|
|
@@ -1908,6 +2310,7 @@ export function useComposer(
|
|
|
1908
2310
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1909
2311
|
),
|
|
1910
2312
|
annotations: next,
|
|
2313
|
+
policy: policyRef.current ?? undefined,
|
|
1911
2314
|
},
|
|
1912
2315
|
);
|
|
1913
2316
|
setOptimisticDraftShadow({
|
|
@@ -1917,6 +2320,7 @@ export function useComposer(
|
|
|
1917
2320
|
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
1918
2321
|
),
|
|
1919
2322
|
annotations: next,
|
|
2323
|
+
policy: policyRef.current ?? undefined,
|
|
1920
2324
|
});
|
|
1921
2325
|
setAnnotations(next);
|
|
1922
2326
|
setAnnotationReviewTargetId(reviewTargetId);
|
|
@@ -1958,6 +2362,55 @@ export function useComposer(
|
|
|
1958
2362
|
setAnnotationReviewTargetId(null);
|
|
1959
2363
|
}, []);
|
|
1960
2364
|
|
|
2365
|
+
const updatePolicy = useCallback(
|
|
2366
|
+
(next: ComposerPolicy): void => {
|
|
2367
|
+
if (targetKeyRef.current !== targetKey) return;
|
|
2368
|
+
localEditRevision.current += 1;
|
|
2369
|
+
policyRef.current = next;
|
|
2370
|
+
const shadow = {
|
|
2371
|
+
text: valueRef.current,
|
|
2372
|
+
resources: mergeResources(
|
|
2373
|
+
restoredResourcesRef.current,
|
|
2374
|
+
resolveSendExtras(sendExtrasRef.current).resources ?? [],
|
|
2375
|
+
),
|
|
2376
|
+
annotations: annotationsRef.current,
|
|
2377
|
+
policy: next,
|
|
2378
|
+
};
|
|
2379
|
+
pendingOperationRef.current = updatePendingComposerShadow(
|
|
2380
|
+
pendingOperationKey,
|
|
2381
|
+
pendingOperationRef.current,
|
|
2382
|
+
shadow,
|
|
2383
|
+
);
|
|
2384
|
+
setOptimisticDraftShadow(shadow);
|
|
2385
|
+
setPolicy(next);
|
|
2386
|
+
},
|
|
2387
|
+
[pendingOperationKey, setOptimisticDraftShadow, targetKey],
|
|
2388
|
+
);
|
|
2389
|
+
|
|
2390
|
+
const updateModel = useCallback(
|
|
2391
|
+
(model: string): void => {
|
|
2392
|
+
const current = policyRef.current;
|
|
2393
|
+
if (current) updatePolicy({ ...current, model });
|
|
2394
|
+
},
|
|
2395
|
+
[updatePolicy],
|
|
2396
|
+
);
|
|
2397
|
+
|
|
2398
|
+
const updateReasoningEffort = useCallback(
|
|
2399
|
+
(reasoningEffort: ReasoningEffort): void => {
|
|
2400
|
+
const current = policyRef.current;
|
|
2401
|
+
if (current) updatePolicy({ ...current, reasoningEffort });
|
|
2402
|
+
},
|
|
2403
|
+
[updatePolicy],
|
|
2404
|
+
);
|
|
2405
|
+
|
|
2406
|
+
const updateLatencyMode = useCallback(
|
|
2407
|
+
(latencyMode: LatencyMode): void => {
|
|
2408
|
+
const current = policyRef.current;
|
|
2409
|
+
if (current) updatePolicy({ ...current, latencyMode });
|
|
2410
|
+
},
|
|
2411
|
+
[updatePolicy],
|
|
2412
|
+
);
|
|
2413
|
+
|
|
1961
2414
|
const removeRestoredResource = useCallback(
|
|
1962
2415
|
(index: number) => {
|
|
1963
2416
|
if (targetKeyRef.current !== targetKey) return;
|
|
@@ -1971,12 +2424,14 @@ export function useComposer(
|
|
|
1971
2424
|
text: valueRef.current,
|
|
1972
2425
|
resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
|
|
1973
2426
|
annotations: annotationsRef.current,
|
|
2427
|
+
policy: policyRef.current ?? undefined,
|
|
1974
2428
|
},
|
|
1975
2429
|
);
|
|
1976
2430
|
setOptimisticDraftShadow({
|
|
1977
2431
|
text: valueRef.current,
|
|
1978
2432
|
resources: mergeResources(next, resolveSendExtras(sendExtrasRef.current).resources ?? []),
|
|
1979
2433
|
annotations: annotationsRef.current,
|
|
2434
|
+
policy: policyRef.current ?? undefined,
|
|
1980
2435
|
});
|
|
1981
2436
|
setRestoredResources(next);
|
|
1982
2437
|
},
|
|
@@ -2030,6 +2485,44 @@ export function useComposer(
|
|
|
2030
2485
|
|
|
2031
2486
|
const identityMatches = stateTargetKey === targetKey;
|
|
2032
2487
|
const visibleSteering = identityMatches ? steering : null;
|
|
2488
|
+
const visibleOptimisticMessages: ComposerOptimisticMessage[] = identityMatches
|
|
2489
|
+
? [
|
|
2490
|
+
...optimisticSends.map(
|
|
2491
|
+
({ input: _input, draftPayload: _payload, canRetry: _retry, ...item }) => item,
|
|
2492
|
+
),
|
|
2493
|
+
...(visibleSteering?.clientEventId
|
|
2494
|
+
? [
|
|
2495
|
+
{
|
|
2496
|
+
clientEventId: visibleSteering.clientEventId,
|
|
2497
|
+
delivery: "steer" as const,
|
|
2498
|
+
destination: "chat" as const,
|
|
2499
|
+
text: visibleSteering.text,
|
|
2500
|
+
annotations:
|
|
2501
|
+
pendingOperationRef.current?.delivery === "steer"
|
|
2502
|
+
? pendingOperationRef.current.annotationsAtSend
|
|
2503
|
+
: [],
|
|
2504
|
+
resources:
|
|
2505
|
+
pendingOperationRef.current?.delivery === "steer"
|
|
2506
|
+
? pendingOperationRef.current.resourcesAtSend
|
|
2507
|
+
: [],
|
|
2508
|
+
occurredAt: steeringOccurredAtRef.current,
|
|
2509
|
+
state:
|
|
2510
|
+
visibleSteering.phase === "submitting"
|
|
2511
|
+
? ("sending" as const)
|
|
2512
|
+
: visibleSteering.phase === "failed"
|
|
2513
|
+
? ("failed" as const)
|
|
2514
|
+
: ("queued" as const),
|
|
2515
|
+
turnId: visibleSteering.turnId,
|
|
2516
|
+
triggerEventId: visibleSteering.triggerEventId,
|
|
2517
|
+
...(visibleSteering.error ? { error: visibleSteering.error } : {}),
|
|
2518
|
+
...(visibleSteering.outcomeUnknown !== undefined
|
|
2519
|
+
? { outcomeUnknown: visibleSteering.outcomeUnknown }
|
|
2520
|
+
: {}),
|
|
2521
|
+
},
|
|
2522
|
+
]
|
|
2523
|
+
: []),
|
|
2524
|
+
]
|
|
2525
|
+
: [];
|
|
2033
2526
|
const reloadDraft = useCallback(async () => await loadDraft(true), [loadDraft]);
|
|
2034
2527
|
const clearError = useCallback(() => {
|
|
2035
2528
|
if (targetKeyRef.current !== targetKey) return;
|
|
@@ -2048,27 +2541,23 @@ export function useComposer(
|
|
|
2048
2541
|
clearAnnotationReviewTarget,
|
|
2049
2542
|
hasDraftContent,
|
|
2050
2543
|
send,
|
|
2051
|
-
optimisticMessages:
|
|
2052
|
-
? optimisticSends.map(
|
|
2053
|
-
({ input: _input, draftPayload: _payload, canRetry: _retry, ...item }) => item,
|
|
2054
|
-
)
|
|
2055
|
-
: [],
|
|
2544
|
+
optimisticMessages: visibleOptimisticMessages,
|
|
2056
2545
|
retryOptimisticMessage,
|
|
2057
2546
|
removeOptimisticMessage,
|
|
2058
2547
|
steer,
|
|
2059
2548
|
steering: visibleSteering,
|
|
2060
2549
|
stoppingAttempt: identityMatches
|
|
2061
|
-
?
|
|
2062
|
-
? "
|
|
2063
|
-
:
|
|
2064
|
-
? "previous"
|
|
2065
|
-
: null
|
|
2550
|
+
? visibleSteering?.phase === "accepted" && visibleSteering.stoppingPreviousAttempt === true
|
|
2551
|
+
? "previous"
|
|
2552
|
+
: null
|
|
2066
2553
|
: null,
|
|
2067
2554
|
sending: identityMatches ? sending : false,
|
|
2068
2555
|
canSend:
|
|
2069
2556
|
identityMatches &&
|
|
2070
2557
|
Boolean(sessionId) &&
|
|
2071
2558
|
!sending &&
|
|
2559
|
+
!draftLoading &&
|
|
2560
|
+
policy !== null &&
|
|
2072
2561
|
sendBlockedRef.current?.() !== true &&
|
|
2073
2562
|
annotationsComplete &&
|
|
2074
2563
|
(hasPendingOperation ||
|
|
@@ -2080,11 +2569,16 @@ export function useComposer(
|
|
|
2080
2569
|
resume,
|
|
2081
2570
|
resumeScope,
|
|
2082
2571
|
resuming: identityMatches ? resuming : false,
|
|
2572
|
+
effectiveControl: identityMatches ? effectiveControlRef.current : null,
|
|
2083
2573
|
draft: identityMatches ? draft : null,
|
|
2084
2574
|
draftRevision: identityMatches ? (draft?.revision ?? 0) : 0,
|
|
2085
2575
|
draftLoading: identityMatches ? draftLoading : Boolean(sessionId) && durableDrafts,
|
|
2086
2576
|
draftSaving: identityMatches ? draftSaving : false,
|
|
2087
2577
|
draftConflict: identityMatches ? draftConflict : null,
|
|
2578
|
+
policy: identityMatches ? policy : null,
|
|
2579
|
+
setModel: updateModel,
|
|
2580
|
+
setReasoningEffort: updateReasoningEffort,
|
|
2581
|
+
setLatencyMode: updateLatencyMode,
|
|
2088
2582
|
draftPersistence: durableDrafts ? "durable" : "disabled",
|
|
2089
2583
|
applyDraft,
|
|
2090
2584
|
reloadDraft,
|
|
@@ -2160,58 +2654,24 @@ function isOutcomeUnknownError(cause: unknown): boolean {
|
|
|
2160
2654
|
);
|
|
2161
2655
|
}
|
|
2162
2656
|
|
|
2657
|
+
async function replayOutcomeUnknown<T>(command: () => Promise<T>): Promise<T> {
|
|
2658
|
+
try {
|
|
2659
|
+
return await command();
|
|
2660
|
+
} catch (cause) {
|
|
2661
|
+
if (!isOutcomeUnknownError(cause)) throw cause;
|
|
2662
|
+
return await command();
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2163
2666
|
function asError(cause: unknown): Error {
|
|
2164
2667
|
return cause instanceof Error ? cause : new Error(String(cause));
|
|
2165
2668
|
}
|
|
2166
2669
|
|
|
2167
2670
|
function isDraftConflictError(error: Error): boolean {
|
|
2168
2671
|
const apiError = error as Partial<OpenGeniApiError>;
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
const code = apiError.code;
|
|
2173
|
-
if (
|
|
2174
|
-
code === undefined ||
|
|
2175
|
-
code === "DRAFT_CHANGED" ||
|
|
2176
|
-
code === "conflict" ||
|
|
2177
|
-
code === "idempotency_conflict"
|
|
2178
|
-
) {
|
|
2179
|
-
return true;
|
|
2180
|
-
}
|
|
2181
|
-
return /draft changed/i.test(error.message);
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
/**
|
|
2185
|
-
* One OCC retry: adopt the server revision and rewrite the same local content.
|
|
2186
|
-
* Covers the common "tab slept through a successful autosave" case without
|
|
2187
|
-
* stranding the operator on a raw 409 toast.
|
|
2188
|
-
*/
|
|
2189
|
-
async function saveComposerDraftWithStaleRetry(input: {
|
|
2190
|
-
client: {
|
|
2191
|
-
getComposerDraft: (workspaceId: string, sessionId: string) => Promise<ComposerDraft>;
|
|
2192
|
-
saveComposerDraft: (
|
|
2193
|
-
workspaceId: string,
|
|
2194
|
-
sessionId: string,
|
|
2195
|
-
request: SaveComposerDraftRequest,
|
|
2196
|
-
) => Promise<ComposerDraft>;
|
|
2197
|
-
};
|
|
2198
|
-
workspaceId: string;
|
|
2199
|
-
sessionId: string;
|
|
2200
|
-
request: SaveComposerDraftRequest;
|
|
2201
|
-
onAdoptRemote: (remote: ComposerDraft) => void;
|
|
2202
|
-
}): Promise<ComposerDraft> {
|
|
2203
|
-
try {
|
|
2204
|
-
return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, input.request);
|
|
2205
|
-
} catch (cause) {
|
|
2206
|
-
const problem = asError(cause);
|
|
2207
|
-
if (!isDraftConflictError(problem)) throw problem;
|
|
2208
|
-
const remote = await input.client.getComposerDraft(input.workspaceId, input.sessionId);
|
|
2209
|
-
input.onAdoptRemote(remote);
|
|
2210
|
-
return await input.client.saveComposerDraft(input.workspaceId, input.sessionId, {
|
|
2211
|
-
...input.request,
|
|
2212
|
-
expectedRevision: remote.revision,
|
|
2213
|
-
});
|
|
2214
|
-
}
|
|
2672
|
+
return (
|
|
2673
|
+
apiError.status === 409 && apiError.outcomeUnknown !== true && apiError.code === "DRAFT_CHANGED"
|
|
2674
|
+
);
|
|
2215
2675
|
}
|
|
2216
2676
|
|
|
2217
2677
|
function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
|
|
@@ -2222,7 +2682,15 @@ function draftPayload(draft: ComposerDraft): SaveComposerDraftRequest {
|
|
|
2222
2682
|
annotations: draft.annotations ?? [],
|
|
2223
2683
|
model: draft.model,
|
|
2224
2684
|
reasoningEffort: draft.reasoningEffort,
|
|
2225
|
-
latencyMode: draft.latencyMode
|
|
2685
|
+
latencyMode: draft.latencyMode,
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2689
|
+
function policyFromDraft(draft: ComposerDraft): ComposerPolicy {
|
|
2690
|
+
return {
|
|
2691
|
+
model: draft.model,
|
|
2692
|
+
reasoningEffort: draft.reasoningEffort,
|
|
2693
|
+
latencyMode: draft.latencyMode,
|
|
2226
2694
|
};
|
|
2227
2695
|
}
|
|
2228
2696
|
|
|
@@ -2231,16 +2699,17 @@ function composerDraftPayload(
|
|
|
2231
2699
|
text: string,
|
|
2232
2700
|
restoredResources: ResourceRef[],
|
|
2233
2701
|
annotations: DraftTimelineAnnotation[],
|
|
2234
|
-
|
|
2702
|
+
policy: ComposerPolicy,
|
|
2703
|
+
additionalResources: ResourceRef[],
|
|
2235
2704
|
): SaveComposerDraftRequest {
|
|
2236
2705
|
return {
|
|
2237
2706
|
expectedRevision: base.revision,
|
|
2238
2707
|
text,
|
|
2239
|
-
resources: mergeResources(restoredResources,
|
|
2708
|
+
resources: mergeResources(restoredResources, additionalResources),
|
|
2240
2709
|
annotations,
|
|
2241
|
-
model:
|
|
2242
|
-
reasoningEffort:
|
|
2243
|
-
latencyMode:
|
|
2710
|
+
model: policy.model,
|
|
2711
|
+
reasoningEffort: policy.reasoningEffort,
|
|
2712
|
+
latencyMode: policy.latencyMode,
|
|
2244
2713
|
};
|
|
2245
2714
|
}
|
|
2246
2715
|
|