@omercnet/paseo-omp 0.2.1 → 0.3.0-next.101.1
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/CHANGELOG.md +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +7 -3
- package/TESTING.md +21 -18
- package/client/composer-pill-settings.tsx +157 -0
- package/client/external-url.ts +15 -0
- package/client/mcp-authorization.tsx +169 -0
- package/client/mcp-popover.tsx +155 -0
- package/client/memory-panel.tsx +8 -3
- package/client/memory-popover.tsx +8 -4
- package/client/omp-config-surface.tsx +189 -29
- package/client/omp-plugin-manager.tsx +302 -131
- package/client/omp-store-picker.tsx +89 -0
- package/client/omp-store-state.ts +45 -0
- package/client/paseo-types.ts +9 -0
- package/client/provider-diagnostics-state.ts +18 -7
- package/client/quota-popover.tsx +8 -3
- package/client/quota-state.ts +16 -7
- package/client/sessions-popover.tsx +8 -3
- package/docs/alpha-release-checklist.md +6 -8
- package/docs/configuration.md +8 -4
- package/docs/core-provider-issue-audit.md +3 -2
- package/docs/images/mcp-authorization-compact.png +0 -0
- package/docs/images/mcp-controls-wide.png +0 -0
- package/docs/images/plugin-manager.png +0 -0
- package/docs/images/workspace-settings.png +0 -0
- package/docs/installation.md +35 -19
- package/index.client.tsx +339 -123
- package/index.server.ts +44 -14
- package/package.json +7 -8
- package/paseo-plugin.json +2 -2
- package/scripts/prepare-dependencies.mjs +24 -0
- package/server/mcp-browser.ts +95 -0
- package/server/memory.ts +2 -2
- package/server/omp-config.ts +16 -7
- package/server/omp-plugins.ts +70 -21
- package/server/omp-settings.ts +232 -24
- package/server/paths.ts +128 -11
- package/server/provider/catalog.ts +3 -4
- package/server/provider/connection.ts +248 -17
- package/server/provider/host-tools.ts +294 -26
- package/server/provider/omp-rpc.ts +499 -72
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/security.ts +8 -10
- package/server/provider/session-descriptors.ts +340 -1
- package/server/provider/session.ts +716 -249
- package/server/provider/subsessions.ts +25 -2
- package/server/provider/timeline-projector.ts +104 -44
- package/server/provider-diagnostics.ts +122 -36
- package/server/quota.ts +3 -2
- package/server/sessions.ts +2 -2
- package/shared/composer-pill-settings.ts +28 -0
- package/shared/external-url.ts +21 -0
- package/shared/hub.ts +3 -3
- package/shared/mcp.ts +47 -0
- package/shared/memory.ts +2 -1
- package/shared/omp-config.ts +5 -1
- package/shared/omp-plugins.ts +74 -33
- package/shared/omp-settings.ts +8 -1
- package/shared/omp-store.ts +58 -0
- package/shared/provider-diagnostics.ts +12 -3
- package/shared/quota.ts +2 -1
- package/shared/sessions.ts +2 -1
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import type {
|
|
3
3
|
ProviderConfigState,
|
|
4
4
|
ProviderContent,
|
|
5
|
+
ProviderError,
|
|
5
6
|
ProviderEvent,
|
|
6
7
|
ProviderInput,
|
|
7
8
|
ProviderPermissionResponse,
|
|
@@ -72,6 +73,7 @@ type OmpQuestionRequest = Extract<
|
|
|
72
73
|
type Emit = (event: ProviderEvent) => void;
|
|
73
74
|
const LOCAL_ONLY_SETTLE_MS = 5_000;
|
|
74
75
|
const AGENT_END_STATE_TIMEOUT_MS = 2_000;
|
|
76
|
+
const AGENT_END_HISTORY_TIMEOUT_MS = 2_000;
|
|
75
77
|
const CONFIG_REFRESH_RETRY_BASE_MS = 250;
|
|
76
78
|
const CONFIG_REFRESH_MAX_ATTEMPTS = 3;
|
|
77
79
|
const USAGE_POLL_MS = 1_000;
|
|
@@ -82,6 +84,7 @@ const AGENT_END_SETTLE_MS = 5_000;
|
|
|
82
84
|
const MAX_PROMPT_PARTS = 64;
|
|
83
85
|
const MAX_PROMPT_TEXT_LENGTH = 1024 * 1024;
|
|
84
86
|
const RPC_REQUEST_ID_BYTES = 36;
|
|
87
|
+
const MAX_AGENT_END_CORRELATION_MESSAGES = 512;
|
|
85
88
|
const MAX_TRACKED_ENTRY_IDS = 1_024;
|
|
86
89
|
const MAX_UNCLAIMED_BRANCH_ENTRIES = 1_024;
|
|
87
90
|
const MAX_PENDING_USERS = 256;
|
|
@@ -140,6 +143,16 @@ function applicableThinkingLevel(
|
|
|
140
143
|
return model?.reasoning === false ? undefined : level;
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
function fixedSessionMode(modeId = "full") {
|
|
147
|
+
const mode = OMP_MODES.find((candidate) => candidate.id === modeId);
|
|
148
|
+
if (!mode) throw new OmpPublicError("OMP mode is unavailable");
|
|
149
|
+
return {
|
|
150
|
+
...mode,
|
|
151
|
+
label: `${mode.label} (fixed for session)`,
|
|
152
|
+
description: `${mode.description} Approval mode is fixed for this session; create a new session to choose another mode.`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
143
156
|
export function ompPersistenceSessionId(input: SessionOpenInput): string | undefined {
|
|
144
157
|
if (!input.persistence) return;
|
|
145
158
|
if (input.persistence.version !== 1) {
|
|
@@ -215,6 +228,24 @@ type PendingUser = {
|
|
|
215
228
|
fallbackOnFinish: boolean;
|
|
216
229
|
bufferedEchoes: OmpMessage[];
|
|
217
230
|
};
|
|
231
|
+
type TerminalCorrelationEvidence = "fresh-native-user" | "current-assistant";
|
|
232
|
+
|
|
233
|
+
type TerminalCorrelation = {
|
|
234
|
+
policy: "initial-turn" | "ordered-legacy" | "native-command";
|
|
235
|
+
evidence: Map<TerminalCorrelationEvidence, number>;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
type TerminalCandidate = {
|
|
239
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>;
|
|
240
|
+
arrivalSequence: number;
|
|
241
|
+
confidence:
|
|
242
|
+
| "keyed"
|
|
243
|
+
| "initial-turn"
|
|
244
|
+
| "ordered-legacy"
|
|
245
|
+
| "native-command"
|
|
246
|
+
| "interrupted"
|
|
247
|
+
| "ambiguous";
|
|
248
|
+
};
|
|
218
249
|
|
|
219
250
|
type ActiveTurn = {
|
|
220
251
|
turnId: string;
|
|
@@ -232,10 +263,8 @@ type ActiveTurn = {
|
|
|
232
263
|
awaitingPermissionEvidence: boolean;
|
|
233
264
|
activitySequence: number;
|
|
234
265
|
acknowledged: boolean;
|
|
235
|
-
|
|
236
|
-
terminalOwnershipRequired: boolean;
|
|
266
|
+
terminalCorrelation: TerminalCorrelation;
|
|
237
267
|
replayingBufferedEvents: boolean;
|
|
238
|
-
bufferedTerminalOwnershipEvidence: boolean;
|
|
239
268
|
agentInvoked?: boolean;
|
|
240
269
|
nativeRequestId?: string;
|
|
241
270
|
promptAcceptedEventIndex?: number;
|
|
@@ -250,21 +279,25 @@ type ActiveTurn = {
|
|
|
250
279
|
agentEndRetryTimer?: unknown;
|
|
251
280
|
agentEndDeadlineTimer?: unknown;
|
|
252
281
|
agentEndCheck?: Promise<void>;
|
|
253
|
-
|
|
282
|
+
ambiguousTerminalTimer?: unknown;
|
|
254
283
|
terminalizing: boolean;
|
|
255
284
|
terminalization?: Promise<void>;
|
|
256
285
|
terminalOutcome?: TurnOutcome;
|
|
257
286
|
terminalWake?: VoidDeferred;
|
|
258
287
|
steerReady: VoidDeferred;
|
|
259
288
|
steersInFlight: number;
|
|
260
|
-
deferredAgentEnd?:
|
|
289
|
+
deferredAgentEnd?: TerminalCandidate;
|
|
290
|
+
activeToolCallIds: Set<string>;
|
|
261
291
|
bufferedEvents: OmpRpcEvent[];
|
|
262
292
|
pendingUsers: PendingUser[];
|
|
263
293
|
userEchoes: OmpMessage[];
|
|
264
294
|
userCorrelationActive: boolean;
|
|
265
295
|
userLookups: Set<Promise<void>>;
|
|
266
296
|
completedMessageCount: number;
|
|
267
|
-
|
|
297
|
+
streamedMessageEntryIds: string[];
|
|
298
|
+
streamedMessageIdentityComplete: boolean;
|
|
299
|
+
lastCompletedAssistantOutcome?: AgentEndOutcome;
|
|
300
|
+
lastCompletedAssistantEntryId?: string;
|
|
268
301
|
};
|
|
269
302
|
|
|
270
303
|
type PendingAbort = {
|
|
@@ -322,7 +355,7 @@ type VoidDeferred = {
|
|
|
322
355
|
|
|
323
356
|
type TurnOutcome = {
|
|
324
357
|
state: "completed" | "failed" | "canceled";
|
|
325
|
-
error?:
|
|
358
|
+
error?: ProviderError;
|
|
326
359
|
usageSampled: boolean;
|
|
327
360
|
};
|
|
328
361
|
|
|
@@ -598,30 +631,130 @@ function nativeEntryId(message: OmpMessage): string | undefined {
|
|
|
598
631
|
return message.entryId;
|
|
599
632
|
}
|
|
600
633
|
|
|
601
|
-
|
|
634
|
+
type AgentEndOutcome = "completed" | "failed" | "canceled";
|
|
635
|
+
type AssistantTerminalStatus = AgentEndOutcome | "unavailable";
|
|
636
|
+
|
|
637
|
+
function assistantTerminalOutcome(
|
|
638
|
+
message: Extract<OmpMessage, { role: "assistant" }>,
|
|
639
|
+
): AgentEndOutcome {
|
|
640
|
+
const stopReason = message.stopReason?.toLowerCase();
|
|
641
|
+
if (stopReason === "aborted" || stopReason === "canceled" || stopReason === "cancelled") {
|
|
642
|
+
return "canceled";
|
|
643
|
+
}
|
|
644
|
+
return stopReason === "error" || message.errorMessage ? "failed" : "completed";
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function lastAssistantStatus(
|
|
648
|
+
messages: readonly OmpMessage[],
|
|
649
|
+
startIndex = 0,
|
|
650
|
+
): AssistantTerminalStatus {
|
|
651
|
+
for (let index = messages.length - 1; index >= startIndex; index -= 1) {
|
|
652
|
+
const message = messages[index];
|
|
653
|
+
if (message?.role !== "assistant") continue;
|
|
654
|
+
return assistantTerminalOutcome(message);
|
|
655
|
+
}
|
|
656
|
+
return "unavailable";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function terminalOutcome(
|
|
602
660
|
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
603
|
-
turn: Pick<ActiveTurn, "completedMessageCount" | "
|
|
604
|
-
):
|
|
661
|
+
turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
|
|
662
|
+
): AgentEndOutcome | undefined {
|
|
605
663
|
const messages = event.messages;
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
: undefined;
|
|
613
|
-
}
|
|
664
|
+
if (
|
|
665
|
+
messages !== undefined &&
|
|
666
|
+
(event.messageCount === undefined || messages.length >= event.messageCount)
|
|
667
|
+
) {
|
|
668
|
+
const status = lastAssistantStatus(messages);
|
|
669
|
+
return status === "unavailable" ? "completed" : status;
|
|
614
670
|
}
|
|
615
|
-
if (
|
|
616
|
-
if (event.messageCount === 0) return undefined;
|
|
671
|
+
if (event.messageCount === 0) return "completed";
|
|
617
672
|
if (
|
|
618
|
-
turn.
|
|
619
|
-
(event.messageCount === undefined ||
|
|
620
|
-
turn.completedMessageCount + (messages?.length ?? 0) >= event.messageCount)
|
|
673
|
+
turn.lastCompletedAssistantOutcome !== undefined &&
|
|
674
|
+
(event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
|
|
621
675
|
) {
|
|
622
|
-
return turn.
|
|
676
|
+
return turn.lastCompletedAssistantOutcome;
|
|
623
677
|
}
|
|
624
|
-
return
|
|
678
|
+
return undefined;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function historyTerminalOutcome(
|
|
682
|
+
messages: readonly OmpMessage[],
|
|
683
|
+
declaredCount: number,
|
|
684
|
+
turn: Pick<
|
|
685
|
+
ActiveTurn,
|
|
686
|
+
| "streamedMessageEntryIds"
|
|
687
|
+
| "streamedMessageIdentityComplete"
|
|
688
|
+
| "lastCompletedAssistantOutcome"
|
|
689
|
+
| "lastCompletedAssistantEntryId"
|
|
690
|
+
>,
|
|
691
|
+
retainedMessages: readonly OmpMessage[],
|
|
692
|
+
): AgentEndOutcome | undefined {
|
|
693
|
+
if (
|
|
694
|
+
messages.length < declaredCount ||
|
|
695
|
+
!turn.streamedMessageIdentityComplete ||
|
|
696
|
+
turn.streamedMessageEntryIds.length === 0
|
|
697
|
+
) {
|
|
698
|
+
return undefined;
|
|
699
|
+
}
|
|
700
|
+
const startIndex = messages.length - declaredCount;
|
|
701
|
+
let historyIndex = startIndex;
|
|
702
|
+
for (const entryId of turn.streamedMessageEntryIds) {
|
|
703
|
+
while (historyIndex < messages.length) {
|
|
704
|
+
const historyMessage = messages[historyIndex];
|
|
705
|
+
if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
|
|
706
|
+
historyIndex += 1;
|
|
707
|
+
}
|
|
708
|
+
if (historyIndex >= messages.length) return undefined;
|
|
709
|
+
const correlated = messages[historyIndex];
|
|
710
|
+
if (
|
|
711
|
+
entryId === turn.lastCompletedAssistantEntryId &&
|
|
712
|
+
(correlated?.role !== "assistant" ||
|
|
713
|
+
assistantTerminalOutcome(correlated) !== turn.lastCompletedAssistantOutcome)
|
|
714
|
+
)
|
|
715
|
+
return undefined;
|
|
716
|
+
historyIndex += 1;
|
|
717
|
+
}
|
|
718
|
+
historyIndex = startIndex;
|
|
719
|
+
for (const retained of retainedMessages) {
|
|
720
|
+
const entryId = nativeEntryId(retained);
|
|
721
|
+
if (!entryId) return undefined;
|
|
722
|
+
while (historyIndex < messages.length) {
|
|
723
|
+
const historyMessage = messages[historyIndex];
|
|
724
|
+
if (historyMessage && nativeEntryId(historyMessage) === entryId) break;
|
|
725
|
+
historyIndex += 1;
|
|
726
|
+
}
|
|
727
|
+
if (historyIndex >= messages.length) return undefined;
|
|
728
|
+
const correlated = messages[historyIndex];
|
|
729
|
+
if (!correlated || correlated.role !== retained.role) return undefined;
|
|
730
|
+
if (
|
|
731
|
+
retained.role === "assistant" &&
|
|
732
|
+
correlated.role === "assistant" &&
|
|
733
|
+
assistantTerminalOutcome(retained) !== assistantTerminalOutcome(correlated)
|
|
734
|
+
)
|
|
735
|
+
return undefined;
|
|
736
|
+
historyIndex += 1;
|
|
737
|
+
}
|
|
738
|
+
const status = lastAssistantStatus(messages, startIndex);
|
|
739
|
+
return status === "unavailable" ? undefined : status;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function unknownTerminalOutcomeError(
|
|
743
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
744
|
+
turn: Pick<ActiveTurn, "completedMessageCount" | "lastCompletedAssistantOutcome">,
|
|
745
|
+
): string {
|
|
746
|
+
const retainedMessages = event.messages?.length ?? 0;
|
|
747
|
+
const retainedStatus = event.messages ? lastAssistantStatus(event.messages) : "unavailable";
|
|
748
|
+
const lastStatus =
|
|
749
|
+
retainedStatus === "unavailable"
|
|
750
|
+
? (turn.lastCompletedAssistantOutcome ?? "unavailable")
|
|
751
|
+
: retainedStatus;
|
|
752
|
+
return (
|
|
753
|
+
"OMP agent_end omitted terminal messages; outcome is unknown " +
|
|
754
|
+
`(declaredCount=${event.messageCount ?? "unavailable"}, ` +
|
|
755
|
+
`observedCount=${turn.completedMessageCount}, ` +
|
|
756
|
+
`retainedTerminalMessages=${retainedMessages}, lastAssistantStatus=${lastStatus})`
|
|
757
|
+
);
|
|
625
758
|
}
|
|
626
759
|
|
|
627
760
|
function isNativeTurnActivity(event: OmpRpcEvent): boolean {
|
|
@@ -668,7 +801,7 @@ function createActiveTurn(
|
|
|
668
801
|
clientMessageId: string,
|
|
669
802
|
text: string,
|
|
670
803
|
generation: number,
|
|
671
|
-
|
|
804
|
+
terminalPolicy: TerminalCorrelation["policy"],
|
|
672
805
|
manualCompaction = false,
|
|
673
806
|
): ActiveTurn {
|
|
674
807
|
return {
|
|
@@ -693,17 +826,18 @@ function createActiveTurn(
|
|
|
693
826
|
manualCompaction,
|
|
694
827
|
activitySequence: 0,
|
|
695
828
|
acknowledged: false,
|
|
696
|
-
|
|
829
|
+
terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
|
|
697
830
|
replayingBufferedEvents: false,
|
|
698
|
-
bufferedTerminalOwnershipEvidence: false,
|
|
699
|
-
terminalOwnershipRequired,
|
|
700
831
|
steersInFlight: 0,
|
|
832
|
+
activeToolCallIds: new Set(),
|
|
701
833
|
steerReady: Promise.withResolvers<void>(),
|
|
702
834
|
userCorrelationActive: false,
|
|
703
835
|
userLookups: new Set(),
|
|
704
836
|
userEchoes: [],
|
|
705
837
|
completedMessageCount: 0,
|
|
706
838
|
bufferedEvents: [],
|
|
839
|
+
streamedMessageEntryIds: [],
|
|
840
|
+
streamedMessageIdentityComplete: true,
|
|
707
841
|
pendingUsers: [
|
|
708
842
|
{
|
|
709
843
|
clientMessageId,
|
|
@@ -715,6 +849,35 @@ function createActiveTurn(
|
|
|
715
849
|
],
|
|
716
850
|
};
|
|
717
851
|
}
|
|
852
|
+
function classifyTerminalCandidate(
|
|
853
|
+
turn: ActiveTurn,
|
|
854
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
855
|
+
arrivalSequence = turn.activitySequence,
|
|
856
|
+
): TerminalCandidate {
|
|
857
|
+
if (event.requestId !== undefined) return { event, confidence: "keyed", arrivalSequence };
|
|
858
|
+
if (turn.agentInvoked === false) return { event, confidence: "ambiguous", arrivalSequence };
|
|
859
|
+
if (turn.terminalCorrelation.policy === "initial-turn") {
|
|
860
|
+
return { event, confidence: "initial-turn", arrivalSequence };
|
|
861
|
+
}
|
|
862
|
+
const assistantSequence = turn.terminalCorrelation.evidence.get("current-assistant");
|
|
863
|
+
if (
|
|
864
|
+
turn.terminalCorrelation.policy === "native-command" &&
|
|
865
|
+
assistantSequence !== undefined &&
|
|
866
|
+
assistantSequence <= arrivalSequence
|
|
867
|
+
) {
|
|
868
|
+
return { event, confidence: "native-command", arrivalSequence };
|
|
869
|
+
}
|
|
870
|
+
const userSequence = turn.terminalCorrelation.evidence.get("fresh-native-user");
|
|
871
|
+
if (
|
|
872
|
+
userSequence !== undefined &&
|
|
873
|
+
assistantSequence !== undefined &&
|
|
874
|
+
userSequence < assistantSequence &&
|
|
875
|
+
assistantSequence <= arrivalSequence
|
|
876
|
+
) {
|
|
877
|
+
return { event, confidence: "ordered-legacy", arrivalSequence };
|
|
878
|
+
}
|
|
879
|
+
return { event, confidence: "ambiguous", arrivalSequence };
|
|
880
|
+
}
|
|
718
881
|
|
|
719
882
|
export class OmpProviderSession {
|
|
720
883
|
readonly id: string;
|
|
@@ -731,6 +894,7 @@ export class OmpProviderSession {
|
|
|
731
894
|
private readonly emittedEntryIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
|
|
732
895
|
private readonly seenEntryIds = new BoundedStringSet(MAX_TRACKED_ENTRY_IDS);
|
|
733
896
|
private branchWatermarkValid = true;
|
|
897
|
+
private readonly branchEntryIds = new Set<string>();
|
|
734
898
|
private readonly unclaimedBranchEntries: Array<{ entryId: string; text: string }> = [];
|
|
735
899
|
private readonly scheduler: OmpTimelineScheduler;
|
|
736
900
|
private readonly dataFilter: OmpPublicDataSerializer;
|
|
@@ -782,7 +946,7 @@ export class OmpProviderSession {
|
|
|
782
946
|
private recoveryOptions: OmpRecoveryOptions,
|
|
783
947
|
private readonly hostTools: OmpHostToolsBridge,
|
|
784
948
|
private nativeSessionId: string,
|
|
785
|
-
nativeSessionFile: string | undefined,
|
|
949
|
+
private readonly nativeSessionFile: string | undefined,
|
|
786
950
|
private readonly config: ProviderSessionConfig,
|
|
787
951
|
private configState: ProviderConfigState,
|
|
788
952
|
outputRedactionValues: readonly string[],
|
|
@@ -813,6 +977,7 @@ export class OmpProviderSession {
|
|
|
813
977
|
scheduler,
|
|
814
978
|
outputRedactionValues,
|
|
815
979
|
capabilities.includes("session.revert.conversation"),
|
|
980
|
+
capabilities.includes("timeline.plugin"),
|
|
816
981
|
hostTools.labels,
|
|
817
982
|
);
|
|
818
983
|
this.subsessions = capabilities.includes("session.subsession")
|
|
@@ -825,6 +990,7 @@ export class OmpProviderSession {
|
|
|
825
990
|
scheduler,
|
|
826
991
|
() => this.resumeDeferredAgentEnd(),
|
|
827
992
|
outputRedactionValues,
|
|
993
|
+
capabilities.includes("timeline.plugin"),
|
|
828
994
|
)
|
|
829
995
|
: null;
|
|
830
996
|
this.bindRuntime(runtime);
|
|
@@ -832,6 +998,13 @@ export class OmpProviderSession {
|
|
|
832
998
|
get persistenceSessionId(): string | undefined {
|
|
833
999
|
return this.persistSession ? this.nativeSessionId : undefined;
|
|
834
1000
|
}
|
|
1001
|
+
async openPaseoBrowser(url: string): Promise<void> {
|
|
1002
|
+
if (this.closed) throw new OmpPublicError("The OMP session is closed");
|
|
1003
|
+
await this.hostTools.openPaseoBrowser(url);
|
|
1004
|
+
}
|
|
1005
|
+
setBrowserAuthorizationIssuer(issue: ((url: string) => string | undefined) | null): void {
|
|
1006
|
+
this.projector.setBrowserAuthorizationIssuer(issue);
|
|
1007
|
+
}
|
|
835
1008
|
|
|
836
1009
|
private readonly imageMaterializer = new OmpImageMaterializer();
|
|
837
1010
|
static async open(
|
|
@@ -976,10 +1149,12 @@ export class OmpProviderSession {
|
|
|
976
1149
|
}
|
|
977
1150
|
const configState: ProviderConfigState = {
|
|
978
1151
|
...(state.model ? { model: ompModelId(state.model) } : {}),
|
|
979
|
-
mode: normalizedConfig.mode,
|
|
1152
|
+
mode: normalizedConfig.mode ?? "full",
|
|
980
1153
|
...(committedThinkingLevel ? { thinkingOption: committedThinkingLevel } : {}),
|
|
981
1154
|
models,
|
|
982
|
-
|
|
1155
|
+
// OMP fixes approval mode at process launch. Publish only the selected mode so
|
|
1156
|
+
// Paseo shows the security state without offering unsupported transitions.
|
|
1157
|
+
modes: [fixedSessionMode(normalizedConfig.mode)],
|
|
983
1158
|
thinkingOptions,
|
|
984
1159
|
settings: [],
|
|
985
1160
|
};
|
|
@@ -1093,7 +1268,7 @@ export class OmpProviderSession {
|
|
|
1093
1268
|
...(this.config.title ? { title: this.dataFilter.text(this.config.title, 256) } : {}),
|
|
1094
1269
|
});
|
|
1095
1270
|
this.emit({ type: "session.config", sessionId: this.id, config: this.configState });
|
|
1096
|
-
if (this.replayHistoryOnOpen) await this.replayHistory();
|
|
1271
|
+
if (this.replayHistoryOnOpen) await this.replayHistory(true);
|
|
1097
1272
|
this.publishCommands(this.commandCatalog);
|
|
1098
1273
|
this.emit({ type: "session.ready", requestId, sessionId: this.id });
|
|
1099
1274
|
this.readyPublished = true;
|
|
@@ -1383,7 +1558,7 @@ export class OmpProviderSession {
|
|
|
1383
1558
|
});
|
|
1384
1559
|
}
|
|
1385
1560
|
|
|
1386
|
-
private async replayHistory(): Promise<void> {
|
|
1561
|
+
private async replayHistory(preferPersistedTranscript = false): Promise<void> {
|
|
1387
1562
|
if (!this.runtime.canReplayHistory) {
|
|
1388
1563
|
throw new OmpPublicError("OMP session history cannot be replayed safely");
|
|
1389
1564
|
}
|
|
@@ -1397,19 +1572,60 @@ export class OmpProviderSession {
|
|
|
1397
1572
|
this.replayTimeoutMs,
|
|
1398
1573
|
);
|
|
1399
1574
|
try {
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1575
|
+
let messages: OmpMessage[] | undefined;
|
|
1576
|
+
// OMP's RPC history is model context, which excludes failed/aborted turns. On initial
|
|
1577
|
+
// resume/import prefer the already authorized journal; rewinds still use the runtime's
|
|
1578
|
+
// in-memory branch because an uncommitted leaf move is not represented by file order.
|
|
1579
|
+
if (
|
|
1580
|
+
preferPersistedTranscript &&
|
|
1581
|
+
this.nativeSessionFile &&
|
|
1582
|
+
this.runtimeFactory.readPersistedSessionTranscript
|
|
1583
|
+
) {
|
|
1584
|
+
try {
|
|
1585
|
+
const transcript = await waitForReplay(
|
|
1586
|
+
this.runtimeFactory.readPersistedSessionTranscript({
|
|
1587
|
+
sessionFile: this.nativeSessionFile,
|
|
1588
|
+
sessionId: this.nativeSessionId,
|
|
1589
|
+
cwd: this.cwd,
|
|
1590
|
+
signal: replay.signal,
|
|
1591
|
+
}),
|
|
1592
|
+
replay.signal,
|
|
1593
|
+
);
|
|
1594
|
+
messages = transcript.messages;
|
|
1595
|
+
if (transcript.imageReplayWarning) {
|
|
1596
|
+
this.emit({
|
|
1597
|
+
type: "timeline.item",
|
|
1598
|
+
sessionId: this.id,
|
|
1599
|
+
item: {
|
|
1600
|
+
id: "omp:replay-image-unavailable",
|
|
1601
|
+
type: "notification",
|
|
1602
|
+
level: "warning",
|
|
1603
|
+
message: "OMP skipped one or more unavailable images while replaying this session.",
|
|
1604
|
+
},
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
} catch (error) {
|
|
1608
|
+
if (replay.signal.aborted) throw error;
|
|
1609
|
+
this.emit({
|
|
1610
|
+
type: "timeline.item",
|
|
1611
|
+
sessionId: this.id,
|
|
1612
|
+
item: {
|
|
1613
|
+
id: "omp:replay-incomplete",
|
|
1614
|
+
type: "error",
|
|
1615
|
+
message:
|
|
1616
|
+
"OMP could not read its complete persisted transcript; displayed history may be incomplete.",
|
|
1617
|
+
},
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1404
1620
|
}
|
|
1405
|
-
this.
|
|
1621
|
+
messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
|
|
1622
|
+
this.quarantineBranchEntries();
|
|
1406
1623
|
for (const message of messages) {
|
|
1407
1624
|
replay.signal.throwIfAborted();
|
|
1408
1625
|
const entryId = nativeEntryId(message);
|
|
1409
1626
|
if (entryId) this.seenEntryIds.add(entryId);
|
|
1410
1627
|
this.projector.projectReplayMessage(message);
|
|
1411
1628
|
}
|
|
1412
|
-
this.branchWatermarkValid = true;
|
|
1413
1629
|
this.projector.finishReplay();
|
|
1414
1630
|
await this.subsessions?.replay(messages, this.runtime, this.runtimeFactory, replay.signal);
|
|
1415
1631
|
replay.signal.throwIfAborted();
|
|
@@ -1705,7 +1921,7 @@ export class OmpProviderSession {
|
|
|
1705
1921
|
input.prompt.clientMessageId,
|
|
1706
1922
|
payload.text,
|
|
1707
1923
|
this.generation,
|
|
1708
|
-
this.runtimeTurnCompleted,
|
|
1924
|
+
this.runtimeTurnCompleted ? "ordered-legacy" : "initial-turn",
|
|
1709
1925
|
slashCommandName(payload.text) === "compact",
|
|
1710
1926
|
);
|
|
1711
1927
|
this.activeTurn = turn;
|
|
@@ -1741,9 +1957,32 @@ export class OmpProviderSession {
|
|
|
1741
1957
|
}, COMPACTION_MAX_WAIT_MS);
|
|
1742
1958
|
return;
|
|
1743
1959
|
}
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1960
|
+
const ownershipPending = turn.pendingUsers[0];
|
|
1961
|
+
if (
|
|
1962
|
+
turn.terminalCorrelation.policy === "ordered-legacy" &&
|
|
1963
|
+
!this.branchWatermarkValid &&
|
|
1964
|
+
ownershipPending
|
|
1965
|
+
) {
|
|
1966
|
+
await this.refreshBranchEntries(turn, ownershipPending);
|
|
1967
|
+
if (
|
|
1968
|
+
this.closed ||
|
|
1969
|
+
turn.terminal ||
|
|
1970
|
+
this.activeTurn !== turn ||
|
|
1971
|
+
turn.generation !== this.generation
|
|
1972
|
+
) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
const acknowledgement = await runtime.prompt(
|
|
1977
|
+
payload.text,
|
|
1978
|
+
payload.images,
|
|
1979
|
+
() => {
|
|
1980
|
+
turn.promptAcceptedEventIndex ??= turn.bufferedEvents.length;
|
|
1981
|
+
},
|
|
1982
|
+
(requestId) => {
|
|
1983
|
+
turn.nativeRequestId = requestId;
|
|
1984
|
+
},
|
|
1985
|
+
);
|
|
1747
1986
|
if (this.closed || turn.terminal) return;
|
|
1748
1987
|
turn.nativeRequestId = acknowledgement.requestId;
|
|
1749
1988
|
this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
|
|
@@ -1761,13 +2000,10 @@ export class OmpProviderSession {
|
|
|
1761
2000
|
0,
|
|
1762
2001
|
turn.promptAcceptedEventIndex ?? bufferedEvents.length,
|
|
1763
2002
|
);
|
|
1764
|
-
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event);
|
|
2003
|
+
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event, false);
|
|
1765
2004
|
this.projector.acceptLiveTurn(turn.turnId);
|
|
1766
2005
|
for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
|
|
1767
2006
|
turn.replayingBufferedEvents = false;
|
|
1768
|
-
if (acknowledgement.agentInvoked === true && turn.bufferedTerminalOwnershipEvidence) {
|
|
1769
|
-
this.markTerminalOwnershipEvidence(turn);
|
|
1770
|
-
}
|
|
1771
2007
|
if (
|
|
1772
2008
|
acknowledgement.agentInvoked === false &&
|
|
1773
2009
|
turn.agentInvoked !== true &&
|
|
@@ -1915,11 +2151,26 @@ export class OmpProviderSession {
|
|
|
1915
2151
|
text: string,
|
|
1916
2152
|
invoke: () => Promise<void>,
|
|
1917
2153
|
): Promise<void> {
|
|
1918
|
-
|
|
2154
|
+
if (this.activeTurn) {
|
|
2155
|
+
throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
|
|
2156
|
+
}
|
|
2157
|
+
const turn = createActiveTurn(clientMessageId, text, this.generation, "native-command");
|
|
1919
2158
|
this.activeTurn = turn;
|
|
1920
2159
|
try {
|
|
1921
2160
|
await invoke();
|
|
1922
|
-
if (this.closed || turn.terminal || this.activeTurn !== turn)
|
|
2161
|
+
if (this.closed || turn.terminal || this.activeTurn !== turn) {
|
|
2162
|
+
if (!turn.terminal) {
|
|
2163
|
+
this.publishPendingUsers(turn);
|
|
2164
|
+
this.publishPromptResult(turn, {
|
|
2165
|
+
type: "failed",
|
|
2166
|
+
error: {
|
|
2167
|
+
message: this.closed ? "OMP session is closed" : "OMP command lost turn ownership",
|
|
2168
|
+
},
|
|
2169
|
+
});
|
|
2170
|
+
this.settleUnstartedTurn(turn);
|
|
2171
|
+
}
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
1923
2174
|
this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
|
|
1924
2175
|
turn.starting = false;
|
|
1925
2176
|
turn.acknowledged = true;
|
|
@@ -1930,16 +2181,13 @@ export class OmpProviderSession {
|
|
|
1930
2181
|
for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
|
|
1931
2182
|
turn.replayingBufferedEvents = false;
|
|
1932
2183
|
} catch (error) {
|
|
1933
|
-
if (turn.terminal
|
|
2184
|
+
if (turn.terminal) return;
|
|
2185
|
+
const ownsTurn = this.activeTurn === turn;
|
|
1934
2186
|
const failure = providerError(error, "OMP command failed");
|
|
1935
2187
|
this.publishPendingUsers(turn);
|
|
1936
2188
|
this.publishPromptResult(turn, { type: "failed", error: failure });
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
this.imageMaterializer.clear();
|
|
1940
|
-
this.resolveTurnPermissions(turn.turnId);
|
|
1941
|
-
this.projector.finishTurn(turn.turnId);
|
|
1942
|
-
this.activeTurn = null;
|
|
2189
|
+
this.settleUnstartedTurn(turn);
|
|
2190
|
+
if (ownsTurn) this.imageMaterializer.clear();
|
|
1943
2191
|
}
|
|
1944
2192
|
}
|
|
1945
2193
|
|
|
@@ -2040,6 +2288,14 @@ export class OmpProviderSession {
|
|
|
2040
2288
|
this.lifetime.abort(new Error("OMP provider connection closed"));
|
|
2041
2289
|
this.unsubscribe();
|
|
2042
2290
|
this.unsubscribe = () => {};
|
|
2291
|
+
const turn = this.activeTurn;
|
|
2292
|
+
if (turn && !turn.started) {
|
|
2293
|
+
this.publishPromptResult(turn, {
|
|
2294
|
+
type: "failed",
|
|
2295
|
+
error: { message: "OMP session closed before the prompt was accepted" },
|
|
2296
|
+
});
|
|
2297
|
+
this.settleUnstartedTurn(turn);
|
|
2298
|
+
}
|
|
2043
2299
|
}
|
|
2044
2300
|
async configure(input: SessionConfigureInput): Promise<void> {
|
|
2045
2301
|
if (this.revertInFlight) {
|
|
@@ -2296,6 +2552,23 @@ export class OmpProviderSession {
|
|
|
2296
2552
|
}
|
|
2297
2553
|
}
|
|
2298
2554
|
|
|
2555
|
+
private async readRuntimeHistoryWithTimeout(
|
|
2556
|
+
runtime: OmpRuntimeSession,
|
|
2557
|
+
): Promise<OmpMessage[] | undefined> {
|
|
2558
|
+
if (!runtime.canReplayHistory) return undefined;
|
|
2559
|
+
const historyRequest = runtime.getMessages();
|
|
2560
|
+
void historyRequest.catch(() => undefined);
|
|
2561
|
+
const timeout = Promise.withResolvers<null>();
|
|
2562
|
+
const timer = this.scheduler.set(() => timeout.resolve(null), AGENT_END_HISTORY_TIMEOUT_MS);
|
|
2563
|
+
try {
|
|
2564
|
+
return (await Promise.race([historyRequest, timeout.promise])) ?? undefined;
|
|
2565
|
+
} catch {
|
|
2566
|
+
return undefined;
|
|
2567
|
+
} finally {
|
|
2568
|
+
this.scheduler.clear(timer);
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2299
2572
|
private publishCommittedConfig(
|
|
2300
2573
|
state: OmpSessionState,
|
|
2301
2574
|
runtime: OmpRuntimeSession,
|
|
@@ -2499,12 +2772,8 @@ export class OmpProviderSession {
|
|
|
2499
2772
|
});
|
|
2500
2773
|
if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
|
|
2501
2774
|
else {
|
|
2502
|
-
|
|
2503
|
-
turn.terminal = true;
|
|
2504
|
-
this.stopUsagePoll(turn);
|
|
2775
|
+
this.settleUnstartedTurn(turn);
|
|
2505
2776
|
this.finishCompaction("canceled");
|
|
2506
|
-
this.projector.finishTurn(turn.turnId);
|
|
2507
|
-
if (this.activeTurn === turn) this.activeTurn = null;
|
|
2508
2777
|
}
|
|
2509
2778
|
}
|
|
2510
2779
|
this.imageMaterializer.clear();
|
|
@@ -2783,6 +3052,7 @@ export class OmpProviderSession {
|
|
|
2783
3052
|
return;
|
|
2784
3053
|
}
|
|
2785
3054
|
turn.localOnlyDisabled = true;
|
|
3055
|
+
this.cancelAmbiguousTerminal(turn);
|
|
2786
3056
|
turn.deferredAgentEnd = undefined;
|
|
2787
3057
|
this.acceptPendingUser(turn, pending);
|
|
2788
3058
|
this.emit({
|
|
@@ -2905,6 +3175,13 @@ export class OmpProviderSession {
|
|
|
2905
3175
|
}
|
|
2906
3176
|
return;
|
|
2907
3177
|
}
|
|
3178
|
+
if (
|
|
3179
|
+
event.type === "agent_end" &&
|
|
3180
|
+
event.requestId !== undefined &&
|
|
3181
|
+
event.requestId !== turn.nativeRequestId
|
|
3182
|
+
) {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
2908
3185
|
if (turn.starting) {
|
|
2909
3186
|
if (
|
|
2910
3187
|
turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
|
|
@@ -2927,7 +3204,11 @@ export class OmpProviderSession {
|
|
|
2927
3204
|
this.handleTurnEvent(turn, event);
|
|
2928
3205
|
}
|
|
2929
3206
|
|
|
2930
|
-
private handleTurnEvent(
|
|
3207
|
+
private handleTurnEvent(
|
|
3208
|
+
turn: ActiveTurn,
|
|
3209
|
+
event: OmpRpcEvent,
|
|
3210
|
+
correlatesToAcceptedPrompt = true,
|
|
3211
|
+
): void {
|
|
2931
3212
|
if (
|
|
2932
3213
|
turn.generation !== this.generation ||
|
|
2933
3214
|
turn.terminal ||
|
|
@@ -2936,16 +3217,32 @@ export class OmpProviderSession {
|
|
|
2936
3217
|
) {
|
|
2937
3218
|
return;
|
|
2938
3219
|
}
|
|
2939
|
-
if (
|
|
3220
|
+
if (
|
|
3221
|
+
event.type === "agent_end" &&
|
|
3222
|
+
event.requestId !== undefined &&
|
|
3223
|
+
event.requestId !== turn.nativeRequestId
|
|
3224
|
+
) {
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
if (event.type === "message_end" && correlatesToAcceptedPrompt) {
|
|
3228
|
+
const entryId = nativeEntryId(event.message);
|
|
3229
|
+
if (!entryId || turn.streamedMessageEntryIds.length >= MAX_AGENT_END_CORRELATION_MESSAGES) {
|
|
3230
|
+
turn.streamedMessageIdentityComplete = false;
|
|
3231
|
+
} else {
|
|
3232
|
+
turn.streamedMessageEntryIds.push(entryId);
|
|
3233
|
+
}
|
|
2940
3234
|
turn.completedMessageCount += 1;
|
|
2941
3235
|
if (event.message.role === "assistant") {
|
|
2942
|
-
turn.
|
|
2943
|
-
|
|
3236
|
+
turn.lastCompletedAssistantOutcome = assistantTerminalOutcome(event.message);
|
|
3237
|
+
turn.lastCompletedAssistantEntryId = entryId;
|
|
2944
3238
|
}
|
|
2945
3239
|
}
|
|
2946
3240
|
if (event.type === "prompt_error") {
|
|
2947
3241
|
if (event.id !== turn.nativeRequestId) return;
|
|
2948
|
-
const error = {
|
|
3242
|
+
const error: ProviderError = {
|
|
3243
|
+
message: this.dataFilter.text(event.error, 4_096),
|
|
3244
|
+
...(event.code ? { code: this.dataFilter.text(event.code, 256) } : {}),
|
|
3245
|
+
};
|
|
2949
3246
|
this.publishPendingUsers(turn);
|
|
2950
3247
|
void this.finishTurn(turn, "failed", error);
|
|
2951
3248
|
return;
|
|
@@ -2953,15 +3250,19 @@ export class OmpProviderSession {
|
|
|
2953
3250
|
if (event.type === "prompt_result") {
|
|
2954
3251
|
if (!event.id || event.id !== turn.nativeRequestId) return;
|
|
2955
3252
|
if (event.agentInvoked) {
|
|
3253
|
+
// A request-keyed prompt result proves dispatch, but an unkeyed terminal still needs
|
|
3254
|
+
// ordered native user and assistant evidence from this accepted prompt.
|
|
2956
3255
|
this.markAgentEvidence(turn);
|
|
2957
|
-
if (turn.replayingBufferedEvents) turn.bufferedTerminalOwnershipEvidence = true;
|
|
2958
|
-
else this.markTerminalOwnershipEvidence(turn);
|
|
2959
3256
|
return;
|
|
2960
3257
|
}
|
|
2961
3258
|
if (turn.localOnlyDisabled || turn.steersInFlight > 0) return;
|
|
2962
3259
|
if (!turn.nativeActivity && !turn.awaitingPermissionEvidence) {
|
|
2963
3260
|
turn.agentInvoked = false;
|
|
2964
3261
|
turn.localOnlyEligible = true;
|
|
3262
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
3263
|
+
turn.deferredAgentEnd = undefined;
|
|
3264
|
+
this.cancelAmbiguousTerminal(turn);
|
|
3265
|
+
}
|
|
2965
3266
|
this.scheduleLocalOnlyCompletion(turn);
|
|
2966
3267
|
}
|
|
2967
3268
|
return;
|
|
@@ -3009,8 +3310,10 @@ export class OmpProviderSession {
|
|
|
3009
3310
|
return;
|
|
3010
3311
|
}
|
|
3011
3312
|
if (event.type === "agent_end" && turn.manualCompactionPending) return;
|
|
3012
|
-
if (event.type === "
|
|
3013
|
-
|
|
3313
|
+
if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
|
|
3314
|
+
if (event.type === "tool_execution_end") {
|
|
3315
|
+
turn.activeToolCallIds.delete(event.toolCallId);
|
|
3316
|
+
if (event.toolName === "ask_user") this.pendingFreeformSelection = null;
|
|
3014
3317
|
}
|
|
3015
3318
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
3016
3319
|
try {
|
|
@@ -3020,6 +3323,37 @@ export class OmpProviderSession {
|
|
|
3020
3323
|
return;
|
|
3021
3324
|
}
|
|
3022
3325
|
}
|
|
3326
|
+
if (event.type === "agent_end") {
|
|
3327
|
+
if (event.isTerminal === false) {
|
|
3328
|
+
turn.completedMessageCount = 0;
|
|
3329
|
+
turn.streamedMessageEntryIds.length = 0;
|
|
3330
|
+
turn.streamedMessageIdentityComplete = true;
|
|
3331
|
+
turn.lastCompletedAssistantOutcome = undefined;
|
|
3332
|
+
turn.lastCompletedAssistantEntryId = undefined;
|
|
3333
|
+
return;
|
|
3334
|
+
}
|
|
3335
|
+
const candidate: TerminalCandidate = turn.interrupted
|
|
3336
|
+
? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
|
|
3337
|
+
: classifyTerminalCandidate(turn, event);
|
|
3338
|
+
if (candidate.confidence === "ambiguous") {
|
|
3339
|
+
this.deferAmbiguousTerminal(turn, candidate);
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
turn.nativeActivity = true;
|
|
3343
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
3344
|
+
if (
|
|
3345
|
+
(!turn.interrupted &&
|
|
3346
|
+
candidate.confidence !== "keyed" &&
|
|
3347
|
+
this.hasTerminalConflict(turn, false)) ||
|
|
3348
|
+
turn.steersInFlight > 0 ||
|
|
3349
|
+
turn.terminalizing
|
|
3350
|
+
) {
|
|
3351
|
+
turn.deferredAgentEnd = candidate;
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
this.beginTerminalization(turn, candidate);
|
|
3355
|
+
return;
|
|
3356
|
+
}
|
|
3023
3357
|
if (isNativeTurnActivity(event)) {
|
|
3024
3358
|
turn.nativeActivity = true;
|
|
3025
3359
|
this.cancelLocalOnlyCompletion(turn);
|
|
@@ -3036,34 +3370,13 @@ export class OmpProviderSession {
|
|
|
3036
3370
|
event.message.role === "assistant"
|
|
3037
3371
|
) {
|
|
3038
3372
|
turn.awaitingPermissionEvidence = false;
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
const hasAssistantEvidence =
|
|
3042
|
-
event.messages?.some((message) => message.role === "assistant") ?? false;
|
|
3043
|
-
if (turn.awaitingPermissionEvidence && !hasAssistantEvidence) {
|
|
3044
|
-
turn.deferredAgentEnd = event;
|
|
3045
|
-
return;
|
|
3046
|
-
}
|
|
3047
|
-
if (hasAssistantEvidence) turn.awaitingPermissionEvidence = false;
|
|
3048
|
-
if (event.isTerminal === false) return;
|
|
3049
|
-
if (turn.terminalizing) {
|
|
3050
|
-
turn.deferredAgentEnd = event;
|
|
3051
|
-
return;
|
|
3373
|
+
if (correlatesToAcceptedPrompt) {
|
|
3374
|
+
turn.terminalCorrelation.evidence.set("current-assistant", turn.activitySequence + 1);
|
|
3052
3375
|
}
|
|
3053
|
-
if (turn.steersInFlight > 0) {
|
|
3054
|
-
turn.deferredAgentEnd = event;
|
|
3055
|
-
return;
|
|
3056
|
-
}
|
|
3057
|
-
this.beginTerminalization(turn, event);
|
|
3058
|
-
return;
|
|
3059
3376
|
}
|
|
3060
3377
|
if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
|
|
3061
|
-
if (event.type === "message_end" && event.message.role === "user") {
|
|
3062
|
-
this.markAgentEvidence(turn);
|
|
3063
|
-
this.projectUserEcho(turn, event.message);
|
|
3064
|
-
return;
|
|
3065
|
-
}
|
|
3066
3378
|
this.projector.project(event, turn.turnId);
|
|
3379
|
+
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
3067
3380
|
}
|
|
3068
3381
|
|
|
3069
3382
|
private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
|
|
@@ -3134,52 +3447,10 @@ export class OmpProviderSession {
|
|
|
3134
3447
|
pending.bufferedEchoes.push(...turn.userEchoes.splice(0));
|
|
3135
3448
|
return;
|
|
3136
3449
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
if (
|
|
3142
|
-
this.closed ||
|
|
3143
|
-
turn.terminal ||
|
|
3144
|
-
this.activeTurn !== turn ||
|
|
3145
|
-
turn.pendingUsers[0] !== pending
|
|
3146
|
-
) {
|
|
3147
|
-
return;
|
|
3148
|
-
}
|
|
3149
|
-
if (retainedBytes(messages, MAX_UNCLAIMED_BRANCH_BYTES) === Number.POSITIVE_INFINITY) {
|
|
3150
|
-
this.quarantineBranchEntries();
|
|
3151
|
-
return;
|
|
3152
|
-
}
|
|
3153
|
-
const unseen: Array<{ entryId: string; text: string }> = [];
|
|
3154
|
-
for (const branchMessage of messages) {
|
|
3155
|
-
if (!this.seenEntryIds.has(branchMessage.entryId)) unseen.push(branchMessage);
|
|
3156
|
-
}
|
|
3157
|
-
if (!this.branchWatermarkValid) {
|
|
3158
|
-
this.unclaimedBranchEntries.length = 0;
|
|
3159
|
-
this.branchWatermarkValid = true;
|
|
3160
|
-
} else if (
|
|
3161
|
-
unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES - this.unclaimedBranchEntries.length &&
|
|
3162
|
-
retainedBytes(this.unclaimedBranchEntries, MAX_UNCLAIMED_BRANCH_BYTES) +
|
|
3163
|
-
retainedBytes(unseen, MAX_UNCLAIMED_BRANCH_BYTES) <=
|
|
3164
|
-
MAX_UNCLAIMED_BRANCH_BYTES
|
|
3165
|
-
) {
|
|
3166
|
-
this.unclaimedBranchEntries.push(...unseen);
|
|
3167
|
-
} else {
|
|
3168
|
-
this.quarantineBranchEntries();
|
|
3169
|
-
}
|
|
3170
|
-
for (const branchMessage of messages) this.seenEntryIds.add(branchMessage.entryId);
|
|
3171
|
-
resolvedId = this.claimUnclaimedBranchEntry(pending.text);
|
|
3172
|
-
} catch {
|
|
3173
|
-
if (
|
|
3174
|
-
this.closed ||
|
|
3175
|
-
turn.terminal ||
|
|
3176
|
-
this.activeTurn !== turn ||
|
|
3177
|
-
turn.pendingUsers[0] !== pending
|
|
3178
|
-
) {
|
|
3179
|
-
return;
|
|
3180
|
-
}
|
|
3181
|
-
this.quarantineBranchEntries();
|
|
3182
|
-
}
|
|
3450
|
+
const observedSequence = turn.activitySequence;
|
|
3451
|
+
let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
3452
|
+
if (!resolvedId && (await this.refreshBranchEntries(turn, pending))) {
|
|
3453
|
+
resolvedId = this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
3183
3454
|
}
|
|
3184
3455
|
if (
|
|
3185
3456
|
this.closed ||
|
|
@@ -3192,18 +3463,94 @@ export class OmpProviderSession {
|
|
|
3192
3463
|
turn.userEchoes.shift();
|
|
3193
3464
|
if (!resolvedId) return;
|
|
3194
3465
|
turn.pendingUsers.shift();
|
|
3195
|
-
this.publishCorrelatedUser(turn, pending, resolvedId);
|
|
3466
|
+
this.publishCorrelatedUser(turn, pending, resolvedId, observedSequence);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
private async refreshBranchEntries(turn: ActiveTurn, pending: PendingUser): Promise<boolean> {
|
|
3470
|
+
const runtime = this.runtime;
|
|
3471
|
+
try {
|
|
3472
|
+
const messages = await runtime.getBranchMessages();
|
|
3473
|
+
if (
|
|
3474
|
+
this.closed ||
|
|
3475
|
+
turn.terminal ||
|
|
3476
|
+
this.activeTurn !== turn ||
|
|
3477
|
+
turn.pendingUsers[0] !== pending ||
|
|
3478
|
+
turn.generation !== this.generation ||
|
|
3479
|
+
runtime !== this.runtime
|
|
3480
|
+
) {
|
|
3481
|
+
return false;
|
|
3482
|
+
}
|
|
3483
|
+
if (
|
|
3484
|
+
messages.length > MAX_UNCLAIMED_BRANCH_ENTRIES ||
|
|
3485
|
+
retainedBytes(messages, MAX_UNCLAIMED_BRANCH_BYTES) === Number.POSITIVE_INFINITY
|
|
3486
|
+
) {
|
|
3487
|
+
this.quarantineBranchEntries();
|
|
3488
|
+
return false;
|
|
3489
|
+
}
|
|
3490
|
+
const unseen: Array<{ entryId: string; text: string }> = [];
|
|
3491
|
+
const snapshotIds = new Set<string>();
|
|
3492
|
+
for (const message of messages) {
|
|
3493
|
+
if (snapshotIds.has(message.entryId)) {
|
|
3494
|
+
this.quarantineBranchEntries();
|
|
3495
|
+
return false;
|
|
3496
|
+
}
|
|
3497
|
+
snapshotIds.add(message.entryId);
|
|
3498
|
+
if (!this.branchEntryIds.has(message.entryId)) unseen.push(message);
|
|
3499
|
+
}
|
|
3500
|
+
if (!this.branchWatermarkValid) {
|
|
3501
|
+
this.unclaimedBranchEntries.length = 0;
|
|
3502
|
+
this.branchWatermarkValid = true;
|
|
3503
|
+
} else if (
|
|
3504
|
+
unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES - this.unclaimedBranchEntries.length &&
|
|
3505
|
+
this.branchEntryIds.size + unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES &&
|
|
3506
|
+
retainedBytes(this.unclaimedBranchEntries, MAX_UNCLAIMED_BRANCH_BYTES) +
|
|
3507
|
+
retainedBytes(unseen, MAX_UNCLAIMED_BRANCH_BYTES) <=
|
|
3508
|
+
MAX_UNCLAIMED_BRANCH_BYTES
|
|
3509
|
+
) {
|
|
3510
|
+
this.unclaimedBranchEntries.push(...unseen);
|
|
3511
|
+
} else {
|
|
3512
|
+
this.quarantineBranchEntries();
|
|
3513
|
+
return false;
|
|
3514
|
+
}
|
|
3515
|
+
for (const message of messages) {
|
|
3516
|
+
this.branchEntryIds.add(message.entryId);
|
|
3517
|
+
this.seenEntryIds.add(message.entryId);
|
|
3518
|
+
}
|
|
3519
|
+
return true;
|
|
3520
|
+
} catch {
|
|
3521
|
+
if (
|
|
3522
|
+
!this.closed &&
|
|
3523
|
+
!turn.terminal &&
|
|
3524
|
+
this.activeTurn === turn &&
|
|
3525
|
+
turn.pendingUsers[0] === pending &&
|
|
3526
|
+
turn.generation === this.generation &&
|
|
3527
|
+
runtime === this.runtime
|
|
3528
|
+
) {
|
|
3529
|
+
this.quarantineBranchEntries();
|
|
3530
|
+
}
|
|
3531
|
+
return false;
|
|
3196
3532
|
}
|
|
3197
3533
|
}
|
|
3198
3534
|
|
|
3199
|
-
private claimUnclaimedBranchEntry(text: string): string | undefined {
|
|
3535
|
+
private claimUnclaimedBranchEntry(turn: ActiveTurn, text: string): string | undefined {
|
|
3200
3536
|
const index = this.unclaimedBranchEntries.findIndex((entry) => entry.text === text);
|
|
3201
3537
|
if (index < 0) return undefined;
|
|
3538
|
+
let matches = 0;
|
|
3539
|
+
let expected = 0;
|
|
3540
|
+
for (const entry of this.unclaimedBranchEntries) if (entry.text === text) matches += 1;
|
|
3541
|
+
for (const pending of turn.pendingUsers) {
|
|
3542
|
+
if (pending.accepted && pending.text === text) expected += 1;
|
|
3543
|
+
}
|
|
3544
|
+
if (matches > expected) {
|
|
3545
|
+
this.quarantineBranchEntries();
|
|
3546
|
+
return undefined;
|
|
3547
|
+
}
|
|
3202
3548
|
return this.unclaimedBranchEntries.splice(index, 1)[0]?.entryId;
|
|
3203
3549
|
}
|
|
3204
3550
|
|
|
3205
3551
|
private quarantineBranchEntries(): void {
|
|
3206
3552
|
this.unclaimedBranchEntries.length = 0;
|
|
3553
|
+
this.branchEntryIds.clear();
|
|
3207
3554
|
this.branchWatermarkValid = false;
|
|
3208
3555
|
}
|
|
3209
3556
|
|
|
@@ -3950,23 +4297,27 @@ export class OmpProviderSession {
|
|
|
3950
4297
|
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
3951
4298
|
}
|
|
3952
4299
|
|
|
3953
|
-
private
|
|
3954
|
-
const turn = this.activeTurn;
|
|
3955
|
-
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4300
|
+
private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
|
|
3956
4301
|
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
3957
4302
|
pending.turnId === turn.turnId;
|
|
3958
|
-
|
|
4303
|
+
return (
|
|
4304
|
+
turn.awaitingPermissionEvidence ||
|
|
3959
4305
|
[...this.pendingPermissions.values()].some(ownsTurn) ||
|
|
3960
4306
|
[...this.inFlightPermissions.values()].some(ownsTurn) ||
|
|
3961
4307
|
[...this.pendingToolPermissions.values()].some(ownsTurn) ||
|
|
3962
|
-
[...this.inFlightToolPermissions.values()].some(ownsTurn)
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4308
|
+
[...this.inFlightToolPermissions.values()].some(ownsTurn) ||
|
|
4309
|
+
turn.activeToolCallIds.size > 0 ||
|
|
4310
|
+
turn.steersInFlight > 0 ||
|
|
4311
|
+
(includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
|
|
4315
|
+
private reevaluateDeferredPermissionTerminal(): void {
|
|
4316
|
+
const turn = this.activeTurn;
|
|
4317
|
+
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4318
|
+
if (this.hasTerminalConflict(turn)) return;
|
|
3969
4319
|
const deferred = turn.deferredAgentEnd;
|
|
4320
|
+
if (deferred.confidence === "ambiguous") return;
|
|
3970
4321
|
turn.deferredAgentEnd = undefined;
|
|
3971
4322
|
this.beginTerminalization(turn, deferred);
|
|
3972
4323
|
}
|
|
@@ -3982,12 +4333,23 @@ export class OmpProviderSession {
|
|
|
3982
4333
|
return this.slashCommands.has(commandName);
|
|
3983
4334
|
}
|
|
3984
4335
|
|
|
3985
|
-
private publishCorrelatedUser(
|
|
4336
|
+
private publishCorrelatedUser(
|
|
4337
|
+
turn: ActiveTurn,
|
|
4338
|
+
pending: PendingUser,
|
|
4339
|
+
entryId: string | undefined,
|
|
4340
|
+
observedSequence: number,
|
|
4341
|
+
): void {
|
|
3986
4342
|
if (entryId) {
|
|
3987
4343
|
if (this.emittedEntryIds.has(entryId)) return;
|
|
3988
4344
|
this.seenEntryIds.add(entryId);
|
|
3989
4345
|
this.emittedEntryIds.add(entryId);
|
|
3990
|
-
|
|
4346
|
+
turn.terminalCorrelation.evidence.set("fresh-native-user", observedSequence);
|
|
4347
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4348
|
+
if (this.branchWatermarkValid && !this.branchEntryIds.has(entryId)) {
|
|
4349
|
+
if (this.branchEntryIds.size >= MAX_UNCLAIMED_BRANCH_ENTRIES)
|
|
4350
|
+
this.quarantineBranchEntries();
|
|
4351
|
+
else this.branchEntryIds.add(entryId);
|
|
4352
|
+
}
|
|
3991
4353
|
const unclaimedIndex = this.unclaimedBranchEntries.findIndex(
|
|
3992
4354
|
(entry) => entry.entryId === entryId,
|
|
3993
4355
|
);
|
|
@@ -4002,12 +4364,7 @@ export class OmpProviderSession {
|
|
|
4002
4364
|
turn.activitySequence += 1;
|
|
4003
4365
|
turn.localOnlyEligible = false;
|
|
4004
4366
|
this.cancelLocalOnlyCompletion(turn);
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
private markTerminalOwnershipEvidence(turn: ActiveTurn): void {
|
|
4009
|
-
turn.terminalOwnershipEvidence = true;
|
|
4010
|
-
this.cancelTerminalOwnershipTimeout(turn);
|
|
4367
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4011
4368
|
}
|
|
4012
4369
|
|
|
4013
4370
|
private scheduleLocalOnlyCompletion(turn: ActiveTurn): void {
|
|
@@ -4033,32 +4390,68 @@ export class OmpProviderSession {
|
|
|
4033
4390
|
turn.localOnlyTimer = undefined;
|
|
4034
4391
|
}
|
|
4035
4392
|
|
|
4036
|
-
private
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4393
|
+
private refreshAmbiguousTerminal(turn: ActiveTurn): void {
|
|
4394
|
+
const candidate = turn.deferredAgentEnd;
|
|
4395
|
+
if (candidate?.confidence !== "ambiguous") return;
|
|
4396
|
+
const resolved = classifyTerminalCandidate(turn, candidate.event, candidate.arrivalSequence);
|
|
4397
|
+
if (resolved.confidence !== "ambiguous") {
|
|
4398
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4399
|
+
turn.deferredAgentEnd = undefined;
|
|
4400
|
+
if (turn.terminalizing || this.hasTerminalConflict(turn, false)) {
|
|
4401
|
+
turn.deferredAgentEnd = resolved;
|
|
4402
|
+
} else {
|
|
4403
|
+
this.beginTerminalization(turn, resolved);
|
|
4404
|
+
}
|
|
4042
4405
|
return;
|
|
4043
4406
|
}
|
|
4044
|
-
turn.
|
|
4045
|
-
turn.
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4407
|
+
if (turn.activitySequence > candidate.arrivalSequence) {
|
|
4408
|
+
turn.deferredAgentEnd = undefined;
|
|
4409
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
private deferAmbiguousTerminal(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4414
|
+
if (turn.agentInvoked === false && turn.localOnlyEligible) return;
|
|
4415
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4416
|
+
turn.deferredAgentEnd = candidate;
|
|
4417
|
+
turn.ambiguousTerminalTimer = this.scheduler.set(() => {
|
|
4418
|
+
turn.ambiguousTerminalTimer = undefined;
|
|
4419
|
+
void this.settleAmbiguousTerminal(turn, candidate);
|
|
4055
4420
|
}, AGENT_END_STATE_TIMEOUT_MS);
|
|
4056
4421
|
}
|
|
4057
4422
|
|
|
4058
|
-
private
|
|
4059
|
-
if (turn.
|
|
4060
|
-
this.scheduler.clear(turn.
|
|
4061
|
-
turn.
|
|
4423
|
+
private cancelAmbiguousTerminal(turn: ActiveTurn): void {
|
|
4424
|
+
if (turn.ambiguousTerminalTimer === undefined) return;
|
|
4425
|
+
this.scheduler.clear(turn.ambiguousTerminalTimer);
|
|
4426
|
+
turn.ambiguousTerminalTimer = undefined;
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4429
|
+
private async settleAmbiguousTerminal(
|
|
4430
|
+
turn: ActiveTurn,
|
|
4431
|
+
candidate: TerminalCandidate,
|
|
4432
|
+
): Promise<void> {
|
|
4433
|
+
if (
|
|
4434
|
+
this.closed ||
|
|
4435
|
+
turn.terminal ||
|
|
4436
|
+
this.activeTurn !== turn ||
|
|
4437
|
+
turn.deferredAgentEnd !== candidate
|
|
4438
|
+
) {
|
|
4439
|
+
return;
|
|
4440
|
+
}
|
|
4441
|
+
await Promise.allSettled(turn.userLookups);
|
|
4442
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4443
|
+
if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
|
|
4444
|
+
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4445
|
+
if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
|
|
4446
|
+
if (!state) {
|
|
4447
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4448
|
+
return;
|
|
4449
|
+
}
|
|
4450
|
+
turn.deferredAgentEnd = undefined;
|
|
4451
|
+
if (state.isStreaming || state.isCompacting) return;
|
|
4452
|
+
await this.finishTurn(turn, "failed", {
|
|
4453
|
+
message: "OMP unkeyed agent_end could not be correlated to the current prompt",
|
|
4454
|
+
});
|
|
4062
4455
|
}
|
|
4063
4456
|
|
|
4064
4457
|
private async completeLocalOnlyTurn(turn: ActiveTurn): Promise<void> {
|
|
@@ -4084,15 +4477,44 @@ export class OmpProviderSession {
|
|
|
4084
4477
|
await this.finishTurn(turn, "completed", undefined, false, false, true);
|
|
4085
4478
|
}
|
|
4086
4479
|
|
|
4087
|
-
private
|
|
4088
|
-
turn
|
|
4089
|
-
|
|
4090
|
-
|
|
4480
|
+
private resetAgentEndProbe(turn: ActiveTurn): void {
|
|
4481
|
+
turn.agentEndPending = false;
|
|
4482
|
+
turn.terminalizing = false;
|
|
4483
|
+
if (turn.agentEndRetryTimer !== undefined) {
|
|
4484
|
+
this.scheduler.clear(turn.agentEndRetryTimer);
|
|
4485
|
+
turn.agentEndRetryTimer = undefined;
|
|
4486
|
+
}
|
|
4487
|
+
if (turn.agentEndDeadlineTimer !== undefined) {
|
|
4488
|
+
this.scheduler.clear(turn.agentEndDeadlineTimer);
|
|
4489
|
+
turn.agentEndDeadlineTimer = undefined;
|
|
4490
|
+
}
|
|
4491
|
+
}
|
|
4492
|
+
private ignoreActiveTerminalCandidate(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4493
|
+
const replacement =
|
|
4494
|
+
turn.deferredAgentEnd && turn.deferredAgentEnd !== candidate
|
|
4495
|
+
? turn.deferredAgentEnd
|
|
4496
|
+
: undefined;
|
|
4497
|
+
this.resetAgentEndProbe(turn);
|
|
4498
|
+
turn.deferredAgentEnd = replacement;
|
|
4499
|
+
if (replacement && replacement.confidence !== "ambiguous") {
|
|
4500
|
+
turn.deferredAgentEnd = undefined;
|
|
4501
|
+
this.beginTerminalization(turn, replacement);
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
this.pollUsage(turn);
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4507
|
+
private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4091
4508
|
if (turn.terminal || turn.terminalizing || turn.agentEndPending || this.activeTurn !== turn) {
|
|
4092
4509
|
return;
|
|
4093
4510
|
}
|
|
4094
|
-
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn,
|
|
4511
|
+
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4095
4512
|
turn.agentEndPending = true;
|
|
4513
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
4514
|
+
turn.deferredAgentEnd = undefined;
|
|
4515
|
+
}
|
|
4516
|
+
turn.terminalizing = true;
|
|
4517
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4096
4518
|
turn.usageSampleFloor = this.usageSequence + 1;
|
|
4097
4519
|
if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
|
|
4098
4520
|
this.usageSample = null;
|
|
@@ -4101,19 +4523,22 @@ export class OmpProviderSession {
|
|
|
4101
4523
|
turn.agentEndDeadlineTimer = this.scheduler.set(() => {
|
|
4102
4524
|
turn.agentEndDeadlineTimer = undefined;
|
|
4103
4525
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4104
|
-
if (
|
|
4105
|
-
|
|
4526
|
+
if (
|
|
4527
|
+
candidate.confidence === "keyed" ||
|
|
4528
|
+
(candidate.confidence === "initial-turn" && !turn.userEchoObserved)
|
|
4529
|
+
) {
|
|
4530
|
+
void this.completeAgentEnd(turn, candidate.event);
|
|
4106
4531
|
} else {
|
|
4107
|
-
|
|
4532
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4108
4533
|
}
|
|
4109
4534
|
}, AGENT_END_SETTLE_MS);
|
|
4110
|
-
this.finishFromAgentEnd(turn,
|
|
4535
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4111
4536
|
}
|
|
4112
4537
|
|
|
4113
4538
|
private resumeAfterFailedSteer(turn: ActiveTurn): void {
|
|
4114
4539
|
if (turn.terminal || this.activeTurn !== turn || turn.steersInFlight > 0) return;
|
|
4115
4540
|
const deferred = turn.deferredAgentEnd;
|
|
4116
|
-
if (deferred) {
|
|
4541
|
+
if (deferred && deferred.confidence !== "ambiguous" && !this.hasTerminalConflict(turn)) {
|
|
4117
4542
|
turn.deferredAgentEnd = undefined;
|
|
4118
4543
|
this.beginTerminalization(turn, deferred);
|
|
4119
4544
|
return;
|
|
@@ -4122,13 +4547,11 @@ export class OmpProviderSession {
|
|
|
4122
4547
|
this.scheduleLocalOnlyCompletion(turn);
|
|
4123
4548
|
}
|
|
4124
4549
|
}
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4128
|
-
): boolean {
|
|
4550
|
+
|
|
4551
|
+
private deferAgentEndForSubsessions(turn: ActiveTurn, candidate: TerminalCandidate): boolean {
|
|
4129
4552
|
if (!this.subsessions?.hasActiveChildren()) return false;
|
|
4130
4553
|
turn.terminalizing = false;
|
|
4131
|
-
turn.deferredAgentEnd =
|
|
4554
|
+
turn.deferredAgentEnd = candidate;
|
|
4132
4555
|
void this.subsessions.reconcile(this.runtime).catch(() => {
|
|
4133
4556
|
if (!turn.terminal && this.activeTurn === turn) {
|
|
4134
4557
|
this.handleRuntimeFailure("OMP subagent reconciliation failed");
|
|
@@ -4139,31 +4562,22 @@ export class OmpProviderSession {
|
|
|
4139
4562
|
|
|
4140
4563
|
private resumeDeferredAgentEnd(): void {
|
|
4141
4564
|
const turn = this.activeTurn;
|
|
4142
|
-
if (
|
|
4143
|
-
!turn ||
|
|
4144
|
-
turn.terminal ||
|
|
4145
|
-
turn.terminalizing ||
|
|
4146
|
-
turn.steersInFlight > 0 ||
|
|
4147
|
-
this.subsessions?.hasActiveChildren()
|
|
4148
|
-
) {
|
|
4565
|
+
if (!turn || turn.terminal || turn.terminalizing || this.hasTerminalConflict(turn)) {
|
|
4149
4566
|
return;
|
|
4150
4567
|
}
|
|
4151
|
-
const
|
|
4152
|
-
if (!
|
|
4568
|
+
const candidate = turn.deferredAgentEnd;
|
|
4569
|
+
if (!candidate || candidate.confidence === "ambiguous") return;
|
|
4153
4570
|
turn.deferredAgentEnd = undefined;
|
|
4154
|
-
this.beginTerminalization(turn,
|
|
4571
|
+
this.beginTerminalization(turn, candidate);
|
|
4155
4572
|
}
|
|
4156
4573
|
|
|
4157
|
-
private finishFromAgentEnd(
|
|
4158
|
-
turn: ActiveTurn,
|
|
4159
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4160
|
-
): void {
|
|
4574
|
+
private finishFromAgentEnd(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4161
4575
|
if (!turn.agentEndPending || turn.terminal) return;
|
|
4162
4576
|
if (turn.agentEndCheck) {
|
|
4163
|
-
turn.deferredAgentEnd =
|
|
4577
|
+
turn.deferredAgentEnd = candidate;
|
|
4164
4578
|
return;
|
|
4165
4579
|
}
|
|
4166
|
-
const check = this.checkAgentEndState(turn,
|
|
4580
|
+
const check = this.checkAgentEndState(turn, candidate);
|
|
4167
4581
|
turn.agentEndCheck = check;
|
|
4168
4582
|
void check.finally(() => {
|
|
4169
4583
|
if (turn.agentEndCheck !== check) return;
|
|
@@ -4175,10 +4589,7 @@ export class OmpProviderSession {
|
|
|
4175
4589
|
});
|
|
4176
4590
|
}
|
|
4177
4591
|
|
|
4178
|
-
private async checkAgentEndState(
|
|
4179
|
-
turn: ActiveTurn,
|
|
4180
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4181
|
-
): Promise<void> {
|
|
4592
|
+
private async checkAgentEndState(turn: ActiveTurn, candidate: TerminalCandidate): Promise<void> {
|
|
4182
4593
|
await Promise.allSettled(turn.userLookups);
|
|
4183
4594
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4184
4595
|
while (turn.userEchoes.length > 0) {
|
|
@@ -4187,37 +4598,42 @@ export class OmpProviderSession {
|
|
|
4187
4598
|
await Promise.allSettled(turn.userLookups);
|
|
4188
4599
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4189
4600
|
}
|
|
4601
|
+
if (
|
|
4602
|
+
!turn.interrupted &&
|
|
4603
|
+
candidate.confidence !== "keyed" &&
|
|
4604
|
+
this.hasTerminalConflict(turn, false)
|
|
4605
|
+
) {
|
|
4606
|
+
this.resetAgentEndProbe(turn);
|
|
4607
|
+
turn.deferredAgentEnd = candidate;
|
|
4608
|
+
this.pollUsage(turn);
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4190
4611
|
if (turn.interrupted) {
|
|
4191
|
-
await this.completeAgentEnd(turn, event);
|
|
4612
|
+
await this.completeAgentEnd(turn, candidate.event);
|
|
4192
4613
|
return;
|
|
4193
4614
|
}
|
|
4194
4615
|
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4195
4616
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4196
4617
|
if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
|
|
4197
|
-
turn
|
|
4198
|
-
if (this.deferAgentEndForSubsessions(turn,
|
|
4618
|
+
this.resetAgentEndProbe(turn);
|
|
4619
|
+
if (this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4620
|
+
}
|
|
4621
|
+
if (
|
|
4622
|
+
!turn.interrupted &&
|
|
4623
|
+
candidate.confidence !== "keyed" &&
|
|
4624
|
+
this.hasTerminalConflict(turn, false)
|
|
4625
|
+
) {
|
|
4626
|
+
this.resetAgentEndProbe(turn);
|
|
4627
|
+
turn.deferredAgentEnd = candidate;
|
|
4628
|
+
this.pollUsage(turn);
|
|
4629
|
+
return;
|
|
4199
4630
|
}
|
|
4200
4631
|
if (state) {
|
|
4201
4632
|
if (state.isStreaming || state.isCompacting) {
|
|
4202
|
-
|
|
4203
|
-
const message = "OMP agent_end arrived while the native runtime remained active";
|
|
4204
|
-
this.invalidateRuntime(message);
|
|
4205
|
-
await this.finishTurn(turn, "failed", { message }, true, true);
|
|
4206
|
-
return;
|
|
4207
|
-
}
|
|
4208
|
-
turn.agentEndPending = false;
|
|
4209
|
-
turn.terminalizing = false;
|
|
4210
|
-
turn.deferredAgentEnd = undefined;
|
|
4211
|
-
return;
|
|
4212
|
-
}
|
|
4213
|
-
if (!turn.terminalOwnershipEvidence && turn.terminalOwnershipRequired) {
|
|
4214
|
-
turn.agentEndPending = false;
|
|
4215
|
-
turn.terminalizing = false;
|
|
4216
|
-
turn.deferredAgentEnd = undefined;
|
|
4217
|
-
this.scheduleTerminalOwnershipTimeout(turn);
|
|
4633
|
+
this.ignoreActiveTerminalCandidate(turn, candidate);
|
|
4218
4634
|
return;
|
|
4219
4635
|
}
|
|
4220
|
-
await this.completeAgentEnd(turn, event);
|
|
4636
|
+
await this.completeAgentEnd(turn, candidate.event, true);
|
|
4221
4637
|
return;
|
|
4222
4638
|
}
|
|
4223
4639
|
if (turn.userEchoObserved) {
|
|
@@ -4227,7 +4643,7 @@ export class OmpProviderSession {
|
|
|
4227
4643
|
if (turn.agentEndRetryTimer === undefined) {
|
|
4228
4644
|
turn.agentEndRetryTimer = this.scheduler.set(() => {
|
|
4229
4645
|
turn.agentEndRetryTimer = undefined;
|
|
4230
|
-
this.finishFromAgentEnd(turn,
|
|
4646
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4231
4647
|
}, USAGE_POLL_MS);
|
|
4232
4648
|
}
|
|
4233
4649
|
}
|
|
@@ -4235,17 +4651,56 @@ export class OmpProviderSession {
|
|
|
4235
4651
|
private async completeAgentEnd(
|
|
4236
4652
|
turn: ActiveTurn,
|
|
4237
4653
|
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4238
|
-
|
|
4654
|
+
providerIdle = false,
|
|
4239
4655
|
): Promise<void> {
|
|
4240
4656
|
if (turn.terminal || this.activeTurn !== turn) return;
|
|
4241
|
-
|
|
4242
|
-
if (
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4657
|
+
let outcome = turn.interrupted ? ("canceled" as const) : terminalOutcome(event, turn);
|
|
4658
|
+
if (!outcome && providerIdle && event.messageCount !== undefined) {
|
|
4659
|
+
const runtime = this.runtime;
|
|
4660
|
+
const history = await this.readRuntimeHistoryWithTimeout(runtime);
|
|
4661
|
+
if (
|
|
4662
|
+
history &&
|
|
4663
|
+
history.length <= MAX_REPLAY_MESSAGES &&
|
|
4664
|
+
this.isCurrentRuntime(runtime, turn.generation) &&
|
|
4665
|
+
!turn.terminal &&
|
|
4666
|
+
!turn.interrupted &&
|
|
4667
|
+
this.activeTurn === turn
|
|
4668
|
+
) {
|
|
4669
|
+
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4670
|
+
if (
|
|
4671
|
+
turn.terminal ||
|
|
4672
|
+
this.activeTurn !== turn ||
|
|
4673
|
+
!this.isCurrentRuntime(runtime, turn.generation)
|
|
4674
|
+
)
|
|
4675
|
+
return;
|
|
4676
|
+
if (!turn.interrupted && state && (state.isStreaming || state.isCompacting)) {
|
|
4677
|
+
this.ignoreActiveTerminalCandidate(turn, {
|
|
4678
|
+
event,
|
|
4679
|
+
arrivalSequence: turn.activitySequence,
|
|
4680
|
+
confidence: event.requestId === undefined ? "ordered-legacy" : "keyed",
|
|
4681
|
+
});
|
|
4682
|
+
return;
|
|
4683
|
+
}
|
|
4684
|
+
if (state && !state.isStreaming && !state.isCompacting) {
|
|
4685
|
+
outcome = historyTerminalOutcome(history, event.messageCount, turn, event.messages ?? []);
|
|
4686
|
+
}
|
|
4687
|
+
}
|
|
4688
|
+
}
|
|
4689
|
+
if (turn.terminal || this.activeTurn !== turn) return;
|
|
4690
|
+
if (turn.interrupted || outcome === "canceled") {
|
|
4691
|
+
this.subsessions?.terminalize("canceled");
|
|
4692
|
+
await this.finishTurn(turn, "canceled");
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
if (outcome === "completed") {
|
|
4696
|
+
await this.finishTurn(turn, "completed");
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
const error =
|
|
4700
|
+
outcome === "failed" ? "OMP assistant turn failed" : unknownTerminalOutcomeError(event, turn);
|
|
4701
|
+
this.subsessions?.terminalize("failed");
|
|
4702
|
+
await this.finishTurn(turn, "failed", { message: error });
|
|
4247
4703
|
}
|
|
4248
|
-
|
|
4249
4704
|
private publishPendingUsers(turn: ActiveTurn): void {
|
|
4250
4705
|
for (const pending of turn.pendingUsers.splice(0)) {
|
|
4251
4706
|
for (const echo of pending.bufferedEchoes) {
|
|
@@ -4283,6 +4738,18 @@ export class OmpProviderSession {
|
|
|
4283
4738
|
pending.bufferedEchoes.length = 0;
|
|
4284
4739
|
}
|
|
4285
4740
|
|
|
4741
|
+
private settleUnstartedTurn(turn: ActiveTurn): void {
|
|
4742
|
+
if (turn.started || turn.terminal) return;
|
|
4743
|
+
turn.steerReady.resolve();
|
|
4744
|
+
turn.starting = false;
|
|
4745
|
+
turn.terminal = true;
|
|
4746
|
+
this.stopUsagePoll(turn);
|
|
4747
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4748
|
+
this.resolveTurnPermissions(turn.turnId);
|
|
4749
|
+
this.projector.finishTurn(turn.turnId);
|
|
4750
|
+
if (this.activeTurn === turn) this.activeTurn = null;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4286
4753
|
private publishPromptResult(
|
|
4287
4754
|
turn: ActiveTurn,
|
|
4288
4755
|
result: Extract<ProviderEvent, { type: "session.prompt_result" }>["result"],
|
|
@@ -4308,7 +4775,7 @@ export class OmpProviderSession {
|
|
|
4308
4775
|
private finishTurn(
|
|
4309
4776
|
turn: ActiveTurn,
|
|
4310
4777
|
state: "completed" | "failed" | "canceled",
|
|
4311
|
-
error?:
|
|
4778
|
+
error?: ProviderError,
|
|
4312
4779
|
usageSampled = false,
|
|
4313
4780
|
override = false,
|
|
4314
4781
|
preserveCompactions = false,
|
|
@@ -4332,7 +4799,7 @@ export class OmpProviderSession {
|
|
|
4332
4799
|
turn.manualCompactionPending = false;
|
|
4333
4800
|
turn.agentEndPending = false;
|
|
4334
4801
|
this.cancelLocalOnlyCompletion(turn);
|
|
4335
|
-
this.
|
|
4802
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4336
4803
|
this.stopUsagePoll(turn);
|
|
4337
4804
|
if (turn.manualCompactionDeadlineTimer !== undefined) {
|
|
4338
4805
|
this.scheduler.clear(turn.manualCompactionDeadlineTimer);
|