@omercnet/paseo-omp 0.2.1 → 0.3.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/CHANGELOG.md +26 -0
- package/README.md +25 -13
- package/SUPPORT.md +6 -2
- 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 +213 -9
- package/server/provider/host-tools.ts +71 -0
- package/server/provider/omp-rpc.ts +82 -15
- package/server/provider/profile-providers.ts +249 -0
- package/server/provider/registration.ts +11 -0
- package/server/provider/session-descriptors.ts +306 -1
- package/server/provider/session.ts +704 -249
- package/server/provider/subsessions.ts +4 -1
- package/server/provider/timeline-projector.ts +70 -33
- 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
|
-
|
|
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;
|
|
670
|
+
}
|
|
671
|
+
if (event.messageCount === 0) return "completed";
|
|
672
|
+
if (
|
|
673
|
+
turn.lastCompletedAssistantOutcome !== undefined &&
|
|
674
|
+
(event.messageCount === undefined || turn.completedMessageCount >= event.messageCount)
|
|
675
|
+
) {
|
|
676
|
+
return turn.lastCompletedAssistantOutcome;
|
|
614
677
|
}
|
|
615
|
-
|
|
616
|
-
|
|
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 {
|
|
617
693
|
if (
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
694
|
+
messages.length < declaredCount ||
|
|
695
|
+
!turn.streamedMessageIdentityComplete ||
|
|
696
|
+
turn.streamedMessageEntryIds.length === 0
|
|
621
697
|
) {
|
|
622
|
-
return
|
|
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;
|
|
623
717
|
}
|
|
624
|
-
|
|
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,48 @@ 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
|
+
} catch (error) {
|
|
1596
|
+
if (replay.signal.aborted) throw error;
|
|
1597
|
+
this.emit({
|
|
1598
|
+
type: "timeline.item",
|
|
1599
|
+
sessionId: this.id,
|
|
1600
|
+
item: {
|
|
1601
|
+
id: "omp:replay-incomplete",
|
|
1602
|
+
type: "error",
|
|
1603
|
+
message:
|
|
1604
|
+
"OMP could not read its complete persisted transcript; displayed history may be incomplete.",
|
|
1605
|
+
},
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1404
1608
|
}
|
|
1405
|
-
this.
|
|
1609
|
+
messages ??= await waitForReplay(this.runtime.getMessages(), replay.signal);
|
|
1610
|
+
this.quarantineBranchEntries();
|
|
1406
1611
|
for (const message of messages) {
|
|
1407
1612
|
replay.signal.throwIfAborted();
|
|
1408
1613
|
const entryId = nativeEntryId(message);
|
|
1409
1614
|
if (entryId) this.seenEntryIds.add(entryId);
|
|
1410
1615
|
this.projector.projectReplayMessage(message);
|
|
1411
1616
|
}
|
|
1412
|
-
this.branchWatermarkValid = true;
|
|
1413
1617
|
this.projector.finishReplay();
|
|
1414
1618
|
await this.subsessions?.replay(messages, this.runtime, this.runtimeFactory, replay.signal);
|
|
1415
1619
|
replay.signal.throwIfAborted();
|
|
@@ -1705,7 +1909,7 @@ export class OmpProviderSession {
|
|
|
1705
1909
|
input.prompt.clientMessageId,
|
|
1706
1910
|
payload.text,
|
|
1707
1911
|
this.generation,
|
|
1708
|
-
this.runtimeTurnCompleted,
|
|
1912
|
+
this.runtimeTurnCompleted ? "ordered-legacy" : "initial-turn",
|
|
1709
1913
|
slashCommandName(payload.text) === "compact",
|
|
1710
1914
|
);
|
|
1711
1915
|
this.activeTurn = turn;
|
|
@@ -1741,9 +1945,32 @@ export class OmpProviderSession {
|
|
|
1741
1945
|
}, COMPACTION_MAX_WAIT_MS);
|
|
1742
1946
|
return;
|
|
1743
1947
|
}
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
|
|
1948
|
+
const ownershipPending = turn.pendingUsers[0];
|
|
1949
|
+
if (
|
|
1950
|
+
turn.terminalCorrelation.policy === "ordered-legacy" &&
|
|
1951
|
+
!this.branchWatermarkValid &&
|
|
1952
|
+
ownershipPending
|
|
1953
|
+
) {
|
|
1954
|
+
await this.refreshBranchEntries(turn, ownershipPending);
|
|
1955
|
+
if (
|
|
1956
|
+
this.closed ||
|
|
1957
|
+
turn.terminal ||
|
|
1958
|
+
this.activeTurn !== turn ||
|
|
1959
|
+
turn.generation !== this.generation
|
|
1960
|
+
) {
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
const acknowledgement = await runtime.prompt(
|
|
1965
|
+
payload.text,
|
|
1966
|
+
payload.images,
|
|
1967
|
+
() => {
|
|
1968
|
+
turn.promptAcceptedEventIndex ??= turn.bufferedEvents.length;
|
|
1969
|
+
},
|
|
1970
|
+
(requestId) => {
|
|
1971
|
+
turn.nativeRequestId = requestId;
|
|
1972
|
+
},
|
|
1973
|
+
);
|
|
1747
1974
|
if (this.closed || turn.terminal) return;
|
|
1748
1975
|
turn.nativeRequestId = acknowledgement.requestId;
|
|
1749
1976
|
this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
|
|
@@ -1761,13 +1988,10 @@ export class OmpProviderSession {
|
|
|
1761
1988
|
0,
|
|
1762
1989
|
turn.promptAcceptedEventIndex ?? bufferedEvents.length,
|
|
1763
1990
|
);
|
|
1764
|
-
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event);
|
|
1991
|
+
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event, false);
|
|
1765
1992
|
this.projector.acceptLiveTurn(turn.turnId);
|
|
1766
1993
|
for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
|
|
1767
1994
|
turn.replayingBufferedEvents = false;
|
|
1768
|
-
if (acknowledgement.agentInvoked === true && turn.bufferedTerminalOwnershipEvidence) {
|
|
1769
|
-
this.markTerminalOwnershipEvidence(turn);
|
|
1770
|
-
}
|
|
1771
1995
|
if (
|
|
1772
1996
|
acknowledgement.agentInvoked === false &&
|
|
1773
1997
|
turn.agentInvoked !== true &&
|
|
@@ -1915,11 +2139,26 @@ export class OmpProviderSession {
|
|
|
1915
2139
|
text: string,
|
|
1916
2140
|
invoke: () => Promise<void>,
|
|
1917
2141
|
): Promise<void> {
|
|
1918
|
-
|
|
2142
|
+
if (this.activeTurn) {
|
|
2143
|
+
throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
|
|
2144
|
+
}
|
|
2145
|
+
const turn = createActiveTurn(clientMessageId, text, this.generation, "native-command");
|
|
1919
2146
|
this.activeTurn = turn;
|
|
1920
2147
|
try {
|
|
1921
2148
|
await invoke();
|
|
1922
|
-
if (this.closed || turn.terminal || this.activeTurn !== turn)
|
|
2149
|
+
if (this.closed || turn.terminal || this.activeTurn !== turn) {
|
|
2150
|
+
if (!turn.terminal) {
|
|
2151
|
+
this.publishPendingUsers(turn);
|
|
2152
|
+
this.publishPromptResult(turn, {
|
|
2153
|
+
type: "failed",
|
|
2154
|
+
error: {
|
|
2155
|
+
message: this.closed ? "OMP session is closed" : "OMP command lost turn ownership",
|
|
2156
|
+
},
|
|
2157
|
+
});
|
|
2158
|
+
this.settleUnstartedTurn(turn);
|
|
2159
|
+
}
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
1923
2162
|
this.publishPromptResult(turn, { type: "turn", turnId: turn.turnId });
|
|
1924
2163
|
turn.starting = false;
|
|
1925
2164
|
turn.acknowledged = true;
|
|
@@ -1930,16 +2169,13 @@ export class OmpProviderSession {
|
|
|
1930
2169
|
for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
|
|
1931
2170
|
turn.replayingBufferedEvents = false;
|
|
1932
2171
|
} catch (error) {
|
|
1933
|
-
if (turn.terminal
|
|
2172
|
+
if (turn.terminal) return;
|
|
2173
|
+
const ownsTurn = this.activeTurn === turn;
|
|
1934
2174
|
const failure = providerError(error, "OMP command failed");
|
|
1935
2175
|
this.publishPendingUsers(turn);
|
|
1936
2176
|
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;
|
|
2177
|
+
this.settleUnstartedTurn(turn);
|
|
2178
|
+
if (ownsTurn) this.imageMaterializer.clear();
|
|
1943
2179
|
}
|
|
1944
2180
|
}
|
|
1945
2181
|
|
|
@@ -2040,6 +2276,14 @@ export class OmpProviderSession {
|
|
|
2040
2276
|
this.lifetime.abort(new Error("OMP provider connection closed"));
|
|
2041
2277
|
this.unsubscribe();
|
|
2042
2278
|
this.unsubscribe = () => {};
|
|
2279
|
+
const turn = this.activeTurn;
|
|
2280
|
+
if (turn && !turn.started) {
|
|
2281
|
+
this.publishPromptResult(turn, {
|
|
2282
|
+
type: "failed",
|
|
2283
|
+
error: { message: "OMP session closed before the prompt was accepted" },
|
|
2284
|
+
});
|
|
2285
|
+
this.settleUnstartedTurn(turn);
|
|
2286
|
+
}
|
|
2043
2287
|
}
|
|
2044
2288
|
async configure(input: SessionConfigureInput): Promise<void> {
|
|
2045
2289
|
if (this.revertInFlight) {
|
|
@@ -2296,6 +2540,23 @@ export class OmpProviderSession {
|
|
|
2296
2540
|
}
|
|
2297
2541
|
}
|
|
2298
2542
|
|
|
2543
|
+
private async readRuntimeHistoryWithTimeout(
|
|
2544
|
+
runtime: OmpRuntimeSession,
|
|
2545
|
+
): Promise<OmpMessage[] | undefined> {
|
|
2546
|
+
if (!runtime.canReplayHistory) return undefined;
|
|
2547
|
+
const historyRequest = runtime.getMessages();
|
|
2548
|
+
void historyRequest.catch(() => undefined);
|
|
2549
|
+
const timeout = Promise.withResolvers<null>();
|
|
2550
|
+
const timer = this.scheduler.set(() => timeout.resolve(null), AGENT_END_HISTORY_TIMEOUT_MS);
|
|
2551
|
+
try {
|
|
2552
|
+
return (await Promise.race([historyRequest, timeout.promise])) ?? undefined;
|
|
2553
|
+
} catch {
|
|
2554
|
+
return undefined;
|
|
2555
|
+
} finally {
|
|
2556
|
+
this.scheduler.clear(timer);
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2299
2560
|
private publishCommittedConfig(
|
|
2300
2561
|
state: OmpSessionState,
|
|
2301
2562
|
runtime: OmpRuntimeSession,
|
|
@@ -2499,12 +2760,8 @@ export class OmpProviderSession {
|
|
|
2499
2760
|
});
|
|
2500
2761
|
if (turn.started) await this.finishTurn(turn, "canceled", undefined, true, true);
|
|
2501
2762
|
else {
|
|
2502
|
-
|
|
2503
|
-
turn.terminal = true;
|
|
2504
|
-
this.stopUsagePoll(turn);
|
|
2763
|
+
this.settleUnstartedTurn(turn);
|
|
2505
2764
|
this.finishCompaction("canceled");
|
|
2506
|
-
this.projector.finishTurn(turn.turnId);
|
|
2507
|
-
if (this.activeTurn === turn) this.activeTurn = null;
|
|
2508
2765
|
}
|
|
2509
2766
|
}
|
|
2510
2767
|
this.imageMaterializer.clear();
|
|
@@ -2783,6 +3040,7 @@ export class OmpProviderSession {
|
|
|
2783
3040
|
return;
|
|
2784
3041
|
}
|
|
2785
3042
|
turn.localOnlyDisabled = true;
|
|
3043
|
+
this.cancelAmbiguousTerminal(turn);
|
|
2786
3044
|
turn.deferredAgentEnd = undefined;
|
|
2787
3045
|
this.acceptPendingUser(turn, pending);
|
|
2788
3046
|
this.emit({
|
|
@@ -2905,6 +3163,13 @@ export class OmpProviderSession {
|
|
|
2905
3163
|
}
|
|
2906
3164
|
return;
|
|
2907
3165
|
}
|
|
3166
|
+
if (
|
|
3167
|
+
event.type === "agent_end" &&
|
|
3168
|
+
event.requestId !== undefined &&
|
|
3169
|
+
event.requestId !== turn.nativeRequestId
|
|
3170
|
+
) {
|
|
3171
|
+
return;
|
|
3172
|
+
}
|
|
2908
3173
|
if (turn.starting) {
|
|
2909
3174
|
if (
|
|
2910
3175
|
turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
|
|
@@ -2927,7 +3192,11 @@ export class OmpProviderSession {
|
|
|
2927
3192
|
this.handleTurnEvent(turn, event);
|
|
2928
3193
|
}
|
|
2929
3194
|
|
|
2930
|
-
private handleTurnEvent(
|
|
3195
|
+
private handleTurnEvent(
|
|
3196
|
+
turn: ActiveTurn,
|
|
3197
|
+
event: OmpRpcEvent,
|
|
3198
|
+
correlatesToAcceptedPrompt = true,
|
|
3199
|
+
): void {
|
|
2931
3200
|
if (
|
|
2932
3201
|
turn.generation !== this.generation ||
|
|
2933
3202
|
turn.terminal ||
|
|
@@ -2936,16 +3205,32 @@ export class OmpProviderSession {
|
|
|
2936
3205
|
) {
|
|
2937
3206
|
return;
|
|
2938
3207
|
}
|
|
2939
|
-
if (
|
|
3208
|
+
if (
|
|
3209
|
+
event.type === "agent_end" &&
|
|
3210
|
+
event.requestId !== undefined &&
|
|
3211
|
+
event.requestId !== turn.nativeRequestId
|
|
3212
|
+
) {
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
if (event.type === "message_end" && correlatesToAcceptedPrompt) {
|
|
3216
|
+
const entryId = nativeEntryId(event.message);
|
|
3217
|
+
if (!entryId || turn.streamedMessageEntryIds.length >= MAX_AGENT_END_CORRELATION_MESSAGES) {
|
|
3218
|
+
turn.streamedMessageIdentityComplete = false;
|
|
3219
|
+
} else {
|
|
3220
|
+
turn.streamedMessageEntryIds.push(entryId);
|
|
3221
|
+
}
|
|
2940
3222
|
turn.completedMessageCount += 1;
|
|
2941
3223
|
if (event.message.role === "assistant") {
|
|
2942
|
-
turn.
|
|
2943
|
-
|
|
3224
|
+
turn.lastCompletedAssistantOutcome = assistantTerminalOutcome(event.message);
|
|
3225
|
+
turn.lastCompletedAssistantEntryId = entryId;
|
|
2944
3226
|
}
|
|
2945
3227
|
}
|
|
2946
3228
|
if (event.type === "prompt_error") {
|
|
2947
3229
|
if (event.id !== turn.nativeRequestId) return;
|
|
2948
|
-
const error = {
|
|
3230
|
+
const error: ProviderError = {
|
|
3231
|
+
message: this.dataFilter.text(event.error, 4_096),
|
|
3232
|
+
...(event.code ? { code: this.dataFilter.text(event.code, 256) } : {}),
|
|
3233
|
+
};
|
|
2949
3234
|
this.publishPendingUsers(turn);
|
|
2950
3235
|
void this.finishTurn(turn, "failed", error);
|
|
2951
3236
|
return;
|
|
@@ -2953,15 +3238,19 @@ export class OmpProviderSession {
|
|
|
2953
3238
|
if (event.type === "prompt_result") {
|
|
2954
3239
|
if (!event.id || event.id !== turn.nativeRequestId) return;
|
|
2955
3240
|
if (event.agentInvoked) {
|
|
3241
|
+
// A request-keyed prompt result proves dispatch, but an unkeyed terminal still needs
|
|
3242
|
+
// ordered native user and assistant evidence from this accepted prompt.
|
|
2956
3243
|
this.markAgentEvidence(turn);
|
|
2957
|
-
if (turn.replayingBufferedEvents) turn.bufferedTerminalOwnershipEvidence = true;
|
|
2958
|
-
else this.markTerminalOwnershipEvidence(turn);
|
|
2959
3244
|
return;
|
|
2960
3245
|
}
|
|
2961
3246
|
if (turn.localOnlyDisabled || turn.steersInFlight > 0) return;
|
|
2962
3247
|
if (!turn.nativeActivity && !turn.awaitingPermissionEvidence) {
|
|
2963
3248
|
turn.agentInvoked = false;
|
|
2964
3249
|
turn.localOnlyEligible = true;
|
|
3250
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
3251
|
+
turn.deferredAgentEnd = undefined;
|
|
3252
|
+
this.cancelAmbiguousTerminal(turn);
|
|
3253
|
+
}
|
|
2965
3254
|
this.scheduleLocalOnlyCompletion(turn);
|
|
2966
3255
|
}
|
|
2967
3256
|
return;
|
|
@@ -3009,8 +3298,10 @@ export class OmpProviderSession {
|
|
|
3009
3298
|
return;
|
|
3010
3299
|
}
|
|
3011
3300
|
if (event.type === "agent_end" && turn.manualCompactionPending) return;
|
|
3012
|
-
if (event.type === "
|
|
3013
|
-
|
|
3301
|
+
if (event.type === "tool_execution_start") turn.activeToolCallIds.add(event.toolCallId);
|
|
3302
|
+
if (event.type === "tool_execution_end") {
|
|
3303
|
+
turn.activeToolCallIds.delete(event.toolCallId);
|
|
3304
|
+
if (event.toolName === "ask_user") this.pendingFreeformSelection = null;
|
|
3014
3305
|
}
|
|
3015
3306
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
3016
3307
|
try {
|
|
@@ -3020,6 +3311,37 @@ export class OmpProviderSession {
|
|
|
3020
3311
|
return;
|
|
3021
3312
|
}
|
|
3022
3313
|
}
|
|
3314
|
+
if (event.type === "agent_end") {
|
|
3315
|
+
if (event.isTerminal === false) {
|
|
3316
|
+
turn.completedMessageCount = 0;
|
|
3317
|
+
turn.streamedMessageEntryIds.length = 0;
|
|
3318
|
+
turn.streamedMessageIdentityComplete = true;
|
|
3319
|
+
turn.lastCompletedAssistantOutcome = undefined;
|
|
3320
|
+
turn.lastCompletedAssistantEntryId = undefined;
|
|
3321
|
+
return;
|
|
3322
|
+
}
|
|
3323
|
+
const candidate: TerminalCandidate = turn.interrupted
|
|
3324
|
+
? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
|
|
3325
|
+
: classifyTerminalCandidate(turn, event);
|
|
3326
|
+
if (candidate.confidence === "ambiguous") {
|
|
3327
|
+
this.deferAmbiguousTerminal(turn, candidate);
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
turn.nativeActivity = true;
|
|
3331
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
3332
|
+
if (
|
|
3333
|
+
(!turn.interrupted &&
|
|
3334
|
+
candidate.confidence !== "keyed" &&
|
|
3335
|
+
this.hasTerminalConflict(turn, false)) ||
|
|
3336
|
+
turn.steersInFlight > 0 ||
|
|
3337
|
+
turn.terminalizing
|
|
3338
|
+
) {
|
|
3339
|
+
turn.deferredAgentEnd = candidate;
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3342
|
+
this.beginTerminalization(turn, candidate);
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3023
3345
|
if (isNativeTurnActivity(event)) {
|
|
3024
3346
|
turn.nativeActivity = true;
|
|
3025
3347
|
this.cancelLocalOnlyCompletion(turn);
|
|
@@ -3036,34 +3358,13 @@ export class OmpProviderSession {
|
|
|
3036
3358
|
event.message.role === "assistant"
|
|
3037
3359
|
) {
|
|
3038
3360
|
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;
|
|
3361
|
+
if (correlatesToAcceptedPrompt) {
|
|
3362
|
+
turn.terminalCorrelation.evidence.set("current-assistant", turn.activitySequence + 1);
|
|
3046
3363
|
}
|
|
3047
|
-
if (hasAssistantEvidence) turn.awaitingPermissionEvidence = false;
|
|
3048
|
-
if (event.isTerminal === false) return;
|
|
3049
|
-
if (turn.terminalizing) {
|
|
3050
|
-
turn.deferredAgentEnd = event;
|
|
3051
|
-
return;
|
|
3052
|
-
}
|
|
3053
|
-
if (turn.steersInFlight > 0) {
|
|
3054
|
-
turn.deferredAgentEnd = event;
|
|
3055
|
-
return;
|
|
3056
|
-
}
|
|
3057
|
-
this.beginTerminalization(turn, event);
|
|
3058
|
-
return;
|
|
3059
3364
|
}
|
|
3060
3365
|
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
3366
|
this.projector.project(event, turn.turnId);
|
|
3367
|
+
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
3067
3368
|
}
|
|
3068
3369
|
|
|
3069
3370
|
private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
|
|
@@ -3134,52 +3435,10 @@ export class OmpProviderSession {
|
|
|
3134
3435
|
pending.bufferedEchoes.push(...turn.userEchoes.splice(0));
|
|
3135
3436
|
return;
|
|
3136
3437
|
}
|
|
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
|
-
}
|
|
3438
|
+
const observedSequence = turn.activitySequence;
|
|
3439
|
+
let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
3440
|
+
if (!resolvedId && (await this.refreshBranchEntries(turn, pending))) {
|
|
3441
|
+
resolvedId = this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
3183
3442
|
}
|
|
3184
3443
|
if (
|
|
3185
3444
|
this.closed ||
|
|
@@ -3192,18 +3451,94 @@ export class OmpProviderSession {
|
|
|
3192
3451
|
turn.userEchoes.shift();
|
|
3193
3452
|
if (!resolvedId) return;
|
|
3194
3453
|
turn.pendingUsers.shift();
|
|
3195
|
-
this.publishCorrelatedUser(turn, pending, resolvedId);
|
|
3454
|
+
this.publishCorrelatedUser(turn, pending, resolvedId, observedSequence);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
private async refreshBranchEntries(turn: ActiveTurn, pending: PendingUser): Promise<boolean> {
|
|
3458
|
+
const runtime = this.runtime;
|
|
3459
|
+
try {
|
|
3460
|
+
const messages = await runtime.getBranchMessages();
|
|
3461
|
+
if (
|
|
3462
|
+
this.closed ||
|
|
3463
|
+
turn.terminal ||
|
|
3464
|
+
this.activeTurn !== turn ||
|
|
3465
|
+
turn.pendingUsers[0] !== pending ||
|
|
3466
|
+
turn.generation !== this.generation ||
|
|
3467
|
+
runtime !== this.runtime
|
|
3468
|
+
) {
|
|
3469
|
+
return false;
|
|
3470
|
+
}
|
|
3471
|
+
if (
|
|
3472
|
+
messages.length > MAX_UNCLAIMED_BRANCH_ENTRIES ||
|
|
3473
|
+
retainedBytes(messages, MAX_UNCLAIMED_BRANCH_BYTES) === Number.POSITIVE_INFINITY
|
|
3474
|
+
) {
|
|
3475
|
+
this.quarantineBranchEntries();
|
|
3476
|
+
return false;
|
|
3477
|
+
}
|
|
3478
|
+
const unseen: Array<{ entryId: string; text: string }> = [];
|
|
3479
|
+
const snapshotIds = new Set<string>();
|
|
3480
|
+
for (const message of messages) {
|
|
3481
|
+
if (snapshotIds.has(message.entryId)) {
|
|
3482
|
+
this.quarantineBranchEntries();
|
|
3483
|
+
return false;
|
|
3484
|
+
}
|
|
3485
|
+
snapshotIds.add(message.entryId);
|
|
3486
|
+
if (!this.branchEntryIds.has(message.entryId)) unseen.push(message);
|
|
3487
|
+
}
|
|
3488
|
+
if (!this.branchWatermarkValid) {
|
|
3489
|
+
this.unclaimedBranchEntries.length = 0;
|
|
3490
|
+
this.branchWatermarkValid = true;
|
|
3491
|
+
} else if (
|
|
3492
|
+
unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES - this.unclaimedBranchEntries.length &&
|
|
3493
|
+
this.branchEntryIds.size + unseen.length <= MAX_UNCLAIMED_BRANCH_ENTRIES &&
|
|
3494
|
+
retainedBytes(this.unclaimedBranchEntries, MAX_UNCLAIMED_BRANCH_BYTES) +
|
|
3495
|
+
retainedBytes(unseen, MAX_UNCLAIMED_BRANCH_BYTES) <=
|
|
3496
|
+
MAX_UNCLAIMED_BRANCH_BYTES
|
|
3497
|
+
) {
|
|
3498
|
+
this.unclaimedBranchEntries.push(...unseen);
|
|
3499
|
+
} else {
|
|
3500
|
+
this.quarantineBranchEntries();
|
|
3501
|
+
return false;
|
|
3502
|
+
}
|
|
3503
|
+
for (const message of messages) {
|
|
3504
|
+
this.branchEntryIds.add(message.entryId);
|
|
3505
|
+
this.seenEntryIds.add(message.entryId);
|
|
3506
|
+
}
|
|
3507
|
+
return true;
|
|
3508
|
+
} catch {
|
|
3509
|
+
if (
|
|
3510
|
+
!this.closed &&
|
|
3511
|
+
!turn.terminal &&
|
|
3512
|
+
this.activeTurn === turn &&
|
|
3513
|
+
turn.pendingUsers[0] === pending &&
|
|
3514
|
+
turn.generation === this.generation &&
|
|
3515
|
+
runtime === this.runtime
|
|
3516
|
+
) {
|
|
3517
|
+
this.quarantineBranchEntries();
|
|
3518
|
+
}
|
|
3519
|
+
return false;
|
|
3196
3520
|
}
|
|
3197
3521
|
}
|
|
3198
3522
|
|
|
3199
|
-
private claimUnclaimedBranchEntry(text: string): string | undefined {
|
|
3523
|
+
private claimUnclaimedBranchEntry(turn: ActiveTurn, text: string): string | undefined {
|
|
3200
3524
|
const index = this.unclaimedBranchEntries.findIndex((entry) => entry.text === text);
|
|
3201
3525
|
if (index < 0) return undefined;
|
|
3526
|
+
let matches = 0;
|
|
3527
|
+
let expected = 0;
|
|
3528
|
+
for (const entry of this.unclaimedBranchEntries) if (entry.text === text) matches += 1;
|
|
3529
|
+
for (const pending of turn.pendingUsers) {
|
|
3530
|
+
if (pending.accepted && pending.text === text) expected += 1;
|
|
3531
|
+
}
|
|
3532
|
+
if (matches > expected) {
|
|
3533
|
+
this.quarantineBranchEntries();
|
|
3534
|
+
return undefined;
|
|
3535
|
+
}
|
|
3202
3536
|
return this.unclaimedBranchEntries.splice(index, 1)[0]?.entryId;
|
|
3203
3537
|
}
|
|
3204
3538
|
|
|
3205
3539
|
private quarantineBranchEntries(): void {
|
|
3206
3540
|
this.unclaimedBranchEntries.length = 0;
|
|
3541
|
+
this.branchEntryIds.clear();
|
|
3207
3542
|
this.branchWatermarkValid = false;
|
|
3208
3543
|
}
|
|
3209
3544
|
|
|
@@ -3950,23 +4285,27 @@ export class OmpProviderSession {
|
|
|
3950
4285
|
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
3951
4286
|
}
|
|
3952
4287
|
|
|
3953
|
-
private
|
|
3954
|
-
const turn = this.activeTurn;
|
|
3955
|
-
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4288
|
+
private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
|
|
3956
4289
|
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
3957
4290
|
pending.turnId === turn.turnId;
|
|
3958
|
-
|
|
4291
|
+
return (
|
|
4292
|
+
turn.awaitingPermissionEvidence ||
|
|
3959
4293
|
[...this.pendingPermissions.values()].some(ownsTurn) ||
|
|
3960
4294
|
[...this.inFlightPermissions.values()].some(ownsTurn) ||
|
|
3961
4295
|
[...this.pendingToolPermissions.values()].some(ownsTurn) ||
|
|
3962
|
-
[...this.inFlightToolPermissions.values()].some(ownsTurn)
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4296
|
+
[...this.inFlightToolPermissions.values()].some(ownsTurn) ||
|
|
4297
|
+
turn.activeToolCallIds.size > 0 ||
|
|
4298
|
+
turn.steersInFlight > 0 ||
|
|
4299
|
+
(includeChildren && Boolean(this.subsessions?.hasActiveChildren()))
|
|
4300
|
+
);
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
private reevaluateDeferredPermissionTerminal(): void {
|
|
4304
|
+
const turn = this.activeTurn;
|
|
4305
|
+
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4306
|
+
if (this.hasTerminalConflict(turn)) return;
|
|
3969
4307
|
const deferred = turn.deferredAgentEnd;
|
|
4308
|
+
if (deferred.confidence === "ambiguous") return;
|
|
3970
4309
|
turn.deferredAgentEnd = undefined;
|
|
3971
4310
|
this.beginTerminalization(turn, deferred);
|
|
3972
4311
|
}
|
|
@@ -3982,12 +4321,23 @@ export class OmpProviderSession {
|
|
|
3982
4321
|
return this.slashCommands.has(commandName);
|
|
3983
4322
|
}
|
|
3984
4323
|
|
|
3985
|
-
private publishCorrelatedUser(
|
|
4324
|
+
private publishCorrelatedUser(
|
|
4325
|
+
turn: ActiveTurn,
|
|
4326
|
+
pending: PendingUser,
|
|
4327
|
+
entryId: string | undefined,
|
|
4328
|
+
observedSequence: number,
|
|
4329
|
+
): void {
|
|
3986
4330
|
if (entryId) {
|
|
3987
4331
|
if (this.emittedEntryIds.has(entryId)) return;
|
|
3988
4332
|
this.seenEntryIds.add(entryId);
|
|
3989
4333
|
this.emittedEntryIds.add(entryId);
|
|
3990
|
-
|
|
4334
|
+
turn.terminalCorrelation.evidence.set("fresh-native-user", observedSequence);
|
|
4335
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4336
|
+
if (this.branchWatermarkValid && !this.branchEntryIds.has(entryId)) {
|
|
4337
|
+
if (this.branchEntryIds.size >= MAX_UNCLAIMED_BRANCH_ENTRIES)
|
|
4338
|
+
this.quarantineBranchEntries();
|
|
4339
|
+
else this.branchEntryIds.add(entryId);
|
|
4340
|
+
}
|
|
3991
4341
|
const unclaimedIndex = this.unclaimedBranchEntries.findIndex(
|
|
3992
4342
|
(entry) => entry.entryId === entryId,
|
|
3993
4343
|
);
|
|
@@ -4002,12 +4352,7 @@ export class OmpProviderSession {
|
|
|
4002
4352
|
turn.activitySequence += 1;
|
|
4003
4353
|
turn.localOnlyEligible = false;
|
|
4004
4354
|
this.cancelLocalOnlyCompletion(turn);
|
|
4005
|
-
|
|
4006
|
-
}
|
|
4007
|
-
|
|
4008
|
-
private markTerminalOwnershipEvidence(turn: ActiveTurn): void {
|
|
4009
|
-
turn.terminalOwnershipEvidence = true;
|
|
4010
|
-
this.cancelTerminalOwnershipTimeout(turn);
|
|
4355
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4011
4356
|
}
|
|
4012
4357
|
|
|
4013
4358
|
private scheduleLocalOnlyCompletion(turn: ActiveTurn): void {
|
|
@@ -4033,32 +4378,68 @@ export class OmpProviderSession {
|
|
|
4033
4378
|
turn.localOnlyTimer = undefined;
|
|
4034
4379
|
}
|
|
4035
4380
|
|
|
4036
|
-
private
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4381
|
+
private refreshAmbiguousTerminal(turn: ActiveTurn): void {
|
|
4382
|
+
const candidate = turn.deferredAgentEnd;
|
|
4383
|
+
if (candidate?.confidence !== "ambiguous") return;
|
|
4384
|
+
const resolved = classifyTerminalCandidate(turn, candidate.event, candidate.arrivalSequence);
|
|
4385
|
+
if (resolved.confidence !== "ambiguous") {
|
|
4386
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4387
|
+
turn.deferredAgentEnd = undefined;
|
|
4388
|
+
if (turn.terminalizing || this.hasTerminalConflict(turn, false)) {
|
|
4389
|
+
turn.deferredAgentEnd = resolved;
|
|
4390
|
+
} else {
|
|
4391
|
+
this.beginTerminalization(turn, resolved);
|
|
4392
|
+
}
|
|
4042
4393
|
return;
|
|
4043
4394
|
}
|
|
4044
|
-
turn.
|
|
4045
|
-
turn.
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4395
|
+
if (turn.activitySequence > candidate.arrivalSequence) {
|
|
4396
|
+
turn.deferredAgentEnd = undefined;
|
|
4397
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
private deferAmbiguousTerminal(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4402
|
+
if (turn.agentInvoked === false && turn.localOnlyEligible) return;
|
|
4403
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4404
|
+
turn.deferredAgentEnd = candidate;
|
|
4405
|
+
turn.ambiguousTerminalTimer = this.scheduler.set(() => {
|
|
4406
|
+
turn.ambiguousTerminalTimer = undefined;
|
|
4407
|
+
void this.settleAmbiguousTerminal(turn, candidate);
|
|
4055
4408
|
}, AGENT_END_STATE_TIMEOUT_MS);
|
|
4056
4409
|
}
|
|
4057
4410
|
|
|
4058
|
-
private
|
|
4059
|
-
if (turn.
|
|
4060
|
-
this.scheduler.clear(turn.
|
|
4061
|
-
turn.
|
|
4411
|
+
private cancelAmbiguousTerminal(turn: ActiveTurn): void {
|
|
4412
|
+
if (turn.ambiguousTerminalTimer === undefined) return;
|
|
4413
|
+
this.scheduler.clear(turn.ambiguousTerminalTimer);
|
|
4414
|
+
turn.ambiguousTerminalTimer = undefined;
|
|
4415
|
+
}
|
|
4416
|
+
|
|
4417
|
+
private async settleAmbiguousTerminal(
|
|
4418
|
+
turn: ActiveTurn,
|
|
4419
|
+
candidate: TerminalCandidate,
|
|
4420
|
+
): Promise<void> {
|
|
4421
|
+
if (
|
|
4422
|
+
this.closed ||
|
|
4423
|
+
turn.terminal ||
|
|
4424
|
+
this.activeTurn !== turn ||
|
|
4425
|
+
turn.deferredAgentEnd !== candidate
|
|
4426
|
+
) {
|
|
4427
|
+
return;
|
|
4428
|
+
}
|
|
4429
|
+
await Promise.allSettled(turn.userLookups);
|
|
4430
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4431
|
+
if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
|
|
4432
|
+
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4433
|
+
if (turn.terminal || this.activeTurn !== turn || turn.deferredAgentEnd !== candidate) return;
|
|
4434
|
+
if (!state) {
|
|
4435
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4438
|
+
turn.deferredAgentEnd = undefined;
|
|
4439
|
+
if (state.isStreaming || state.isCompacting) return;
|
|
4440
|
+
await this.finishTurn(turn, "failed", {
|
|
4441
|
+
message: "OMP unkeyed agent_end could not be correlated to the current prompt",
|
|
4442
|
+
});
|
|
4062
4443
|
}
|
|
4063
4444
|
|
|
4064
4445
|
private async completeLocalOnlyTurn(turn: ActiveTurn): Promise<void> {
|
|
@@ -4084,15 +4465,44 @@ export class OmpProviderSession {
|
|
|
4084
4465
|
await this.finishTurn(turn, "completed", undefined, false, false, true);
|
|
4085
4466
|
}
|
|
4086
4467
|
|
|
4087
|
-
private
|
|
4088
|
-
turn
|
|
4089
|
-
|
|
4090
|
-
|
|
4468
|
+
private resetAgentEndProbe(turn: ActiveTurn): void {
|
|
4469
|
+
turn.agentEndPending = false;
|
|
4470
|
+
turn.terminalizing = false;
|
|
4471
|
+
if (turn.agentEndRetryTimer !== undefined) {
|
|
4472
|
+
this.scheduler.clear(turn.agentEndRetryTimer);
|
|
4473
|
+
turn.agentEndRetryTimer = undefined;
|
|
4474
|
+
}
|
|
4475
|
+
if (turn.agentEndDeadlineTimer !== undefined) {
|
|
4476
|
+
this.scheduler.clear(turn.agentEndDeadlineTimer);
|
|
4477
|
+
turn.agentEndDeadlineTimer = undefined;
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
private ignoreActiveTerminalCandidate(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4481
|
+
const replacement =
|
|
4482
|
+
turn.deferredAgentEnd && turn.deferredAgentEnd !== candidate
|
|
4483
|
+
? turn.deferredAgentEnd
|
|
4484
|
+
: undefined;
|
|
4485
|
+
this.resetAgentEndProbe(turn);
|
|
4486
|
+
turn.deferredAgentEnd = replacement;
|
|
4487
|
+
if (replacement && replacement.confidence !== "ambiguous") {
|
|
4488
|
+
turn.deferredAgentEnd = undefined;
|
|
4489
|
+
this.beginTerminalization(turn, replacement);
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
this.pollUsage(turn);
|
|
4493
|
+
}
|
|
4494
|
+
|
|
4495
|
+
private beginTerminalization(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4091
4496
|
if (turn.terminal || turn.terminalizing || turn.agentEndPending || this.activeTurn !== turn) {
|
|
4092
4497
|
return;
|
|
4093
4498
|
}
|
|
4094
|
-
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn,
|
|
4499
|
+
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4095
4500
|
turn.agentEndPending = true;
|
|
4501
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
4502
|
+
turn.deferredAgentEnd = undefined;
|
|
4503
|
+
}
|
|
4504
|
+
turn.terminalizing = true;
|
|
4505
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4096
4506
|
turn.usageSampleFloor = this.usageSequence + 1;
|
|
4097
4507
|
if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
|
|
4098
4508
|
this.usageSample = null;
|
|
@@ -4101,19 +4511,22 @@ export class OmpProviderSession {
|
|
|
4101
4511
|
turn.agentEndDeadlineTimer = this.scheduler.set(() => {
|
|
4102
4512
|
turn.agentEndDeadlineTimer = undefined;
|
|
4103
4513
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4104
|
-
if (
|
|
4105
|
-
|
|
4514
|
+
if (
|
|
4515
|
+
candidate.confidence === "keyed" ||
|
|
4516
|
+
(candidate.confidence === "initial-turn" && !turn.userEchoObserved)
|
|
4517
|
+
) {
|
|
4518
|
+
void this.completeAgentEnd(turn, candidate.event);
|
|
4106
4519
|
} else {
|
|
4107
|
-
|
|
4520
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4108
4521
|
}
|
|
4109
4522
|
}, AGENT_END_SETTLE_MS);
|
|
4110
|
-
this.finishFromAgentEnd(turn,
|
|
4523
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4111
4524
|
}
|
|
4112
4525
|
|
|
4113
4526
|
private resumeAfterFailedSteer(turn: ActiveTurn): void {
|
|
4114
4527
|
if (turn.terminal || this.activeTurn !== turn || turn.steersInFlight > 0) return;
|
|
4115
4528
|
const deferred = turn.deferredAgentEnd;
|
|
4116
|
-
if (deferred) {
|
|
4529
|
+
if (deferred && deferred.confidence !== "ambiguous" && !this.hasTerminalConflict(turn)) {
|
|
4117
4530
|
turn.deferredAgentEnd = undefined;
|
|
4118
4531
|
this.beginTerminalization(turn, deferred);
|
|
4119
4532
|
return;
|
|
@@ -4122,13 +4535,11 @@ export class OmpProviderSession {
|
|
|
4122
4535
|
this.scheduleLocalOnlyCompletion(turn);
|
|
4123
4536
|
}
|
|
4124
4537
|
}
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4128
|
-
): boolean {
|
|
4538
|
+
|
|
4539
|
+
private deferAgentEndForSubsessions(turn: ActiveTurn, candidate: TerminalCandidate): boolean {
|
|
4129
4540
|
if (!this.subsessions?.hasActiveChildren()) return false;
|
|
4130
4541
|
turn.terminalizing = false;
|
|
4131
|
-
turn.deferredAgentEnd =
|
|
4542
|
+
turn.deferredAgentEnd = candidate;
|
|
4132
4543
|
void this.subsessions.reconcile(this.runtime).catch(() => {
|
|
4133
4544
|
if (!turn.terminal && this.activeTurn === turn) {
|
|
4134
4545
|
this.handleRuntimeFailure("OMP subagent reconciliation failed");
|
|
@@ -4139,31 +4550,22 @@ export class OmpProviderSession {
|
|
|
4139
4550
|
|
|
4140
4551
|
private resumeDeferredAgentEnd(): void {
|
|
4141
4552
|
const turn = this.activeTurn;
|
|
4142
|
-
if (
|
|
4143
|
-
!turn ||
|
|
4144
|
-
turn.terminal ||
|
|
4145
|
-
turn.terminalizing ||
|
|
4146
|
-
turn.steersInFlight > 0 ||
|
|
4147
|
-
this.subsessions?.hasActiveChildren()
|
|
4148
|
-
) {
|
|
4553
|
+
if (!turn || turn.terminal || turn.terminalizing || this.hasTerminalConflict(turn)) {
|
|
4149
4554
|
return;
|
|
4150
4555
|
}
|
|
4151
|
-
const
|
|
4152
|
-
if (!
|
|
4556
|
+
const candidate = turn.deferredAgentEnd;
|
|
4557
|
+
if (!candidate || candidate.confidence === "ambiguous") return;
|
|
4153
4558
|
turn.deferredAgentEnd = undefined;
|
|
4154
|
-
this.beginTerminalization(turn,
|
|
4559
|
+
this.beginTerminalization(turn, candidate);
|
|
4155
4560
|
}
|
|
4156
4561
|
|
|
4157
|
-
private finishFromAgentEnd(
|
|
4158
|
-
turn: ActiveTurn,
|
|
4159
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4160
|
-
): void {
|
|
4562
|
+
private finishFromAgentEnd(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4161
4563
|
if (!turn.agentEndPending || turn.terminal) return;
|
|
4162
4564
|
if (turn.agentEndCheck) {
|
|
4163
|
-
turn.deferredAgentEnd =
|
|
4565
|
+
turn.deferredAgentEnd = candidate;
|
|
4164
4566
|
return;
|
|
4165
4567
|
}
|
|
4166
|
-
const check = this.checkAgentEndState(turn,
|
|
4568
|
+
const check = this.checkAgentEndState(turn, candidate);
|
|
4167
4569
|
turn.agentEndCheck = check;
|
|
4168
4570
|
void check.finally(() => {
|
|
4169
4571
|
if (turn.agentEndCheck !== check) return;
|
|
@@ -4175,10 +4577,7 @@ export class OmpProviderSession {
|
|
|
4175
4577
|
});
|
|
4176
4578
|
}
|
|
4177
4579
|
|
|
4178
|
-
private async checkAgentEndState(
|
|
4179
|
-
turn: ActiveTurn,
|
|
4180
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4181
|
-
): Promise<void> {
|
|
4580
|
+
private async checkAgentEndState(turn: ActiveTurn, candidate: TerminalCandidate): Promise<void> {
|
|
4182
4581
|
await Promise.allSettled(turn.userLookups);
|
|
4183
4582
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4184
4583
|
while (turn.userEchoes.length > 0) {
|
|
@@ -4187,37 +4586,42 @@ export class OmpProviderSession {
|
|
|
4187
4586
|
await Promise.allSettled(turn.userLookups);
|
|
4188
4587
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4189
4588
|
}
|
|
4589
|
+
if (
|
|
4590
|
+
!turn.interrupted &&
|
|
4591
|
+
candidate.confidence !== "keyed" &&
|
|
4592
|
+
this.hasTerminalConflict(turn, false)
|
|
4593
|
+
) {
|
|
4594
|
+
this.resetAgentEndProbe(turn);
|
|
4595
|
+
turn.deferredAgentEnd = candidate;
|
|
4596
|
+
this.pollUsage(turn);
|
|
4597
|
+
return;
|
|
4598
|
+
}
|
|
4190
4599
|
if (turn.interrupted) {
|
|
4191
|
-
await this.completeAgentEnd(turn, event);
|
|
4600
|
+
await this.completeAgentEnd(turn, candidate.event);
|
|
4192
4601
|
return;
|
|
4193
4602
|
}
|
|
4194
4603
|
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4195
4604
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4196
4605
|
if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
|
|
4197
|
-
turn
|
|
4198
|
-
if (this.deferAgentEndForSubsessions(turn,
|
|
4606
|
+
this.resetAgentEndProbe(turn);
|
|
4607
|
+
if (this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4608
|
+
}
|
|
4609
|
+
if (
|
|
4610
|
+
!turn.interrupted &&
|
|
4611
|
+
candidate.confidence !== "keyed" &&
|
|
4612
|
+
this.hasTerminalConflict(turn, false)
|
|
4613
|
+
) {
|
|
4614
|
+
this.resetAgentEndProbe(turn);
|
|
4615
|
+
turn.deferredAgentEnd = candidate;
|
|
4616
|
+
this.pollUsage(turn);
|
|
4617
|
+
return;
|
|
4199
4618
|
}
|
|
4200
4619
|
if (state) {
|
|
4201
4620
|
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);
|
|
4621
|
+
this.ignoreActiveTerminalCandidate(turn, candidate);
|
|
4218
4622
|
return;
|
|
4219
4623
|
}
|
|
4220
|
-
await this.completeAgentEnd(turn, event);
|
|
4624
|
+
await this.completeAgentEnd(turn, candidate.event, true);
|
|
4221
4625
|
return;
|
|
4222
4626
|
}
|
|
4223
4627
|
if (turn.userEchoObserved) {
|
|
@@ -4227,7 +4631,7 @@ export class OmpProviderSession {
|
|
|
4227
4631
|
if (turn.agentEndRetryTimer === undefined) {
|
|
4228
4632
|
turn.agentEndRetryTimer = this.scheduler.set(() => {
|
|
4229
4633
|
turn.agentEndRetryTimer = undefined;
|
|
4230
|
-
this.finishFromAgentEnd(turn,
|
|
4634
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4231
4635
|
}, USAGE_POLL_MS);
|
|
4232
4636
|
}
|
|
4233
4637
|
}
|
|
@@ -4235,17 +4639,56 @@ export class OmpProviderSession {
|
|
|
4235
4639
|
private async completeAgentEnd(
|
|
4236
4640
|
turn: ActiveTurn,
|
|
4237
4641
|
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4238
|
-
|
|
4642
|
+
providerIdle = false,
|
|
4239
4643
|
): Promise<void> {
|
|
4240
4644
|
if (turn.terminal || this.activeTurn !== turn) return;
|
|
4241
|
-
|
|
4242
|
-
if (
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4645
|
+
let outcome = turn.interrupted ? ("canceled" as const) : terminalOutcome(event, turn);
|
|
4646
|
+
if (!outcome && providerIdle && event.messageCount !== undefined) {
|
|
4647
|
+
const runtime = this.runtime;
|
|
4648
|
+
const history = await this.readRuntimeHistoryWithTimeout(runtime);
|
|
4649
|
+
if (
|
|
4650
|
+
history &&
|
|
4651
|
+
history.length <= MAX_REPLAY_MESSAGES &&
|
|
4652
|
+
this.isCurrentRuntime(runtime, turn.generation) &&
|
|
4653
|
+
!turn.terminal &&
|
|
4654
|
+
!turn.interrupted &&
|
|
4655
|
+
this.activeTurn === turn
|
|
4656
|
+
) {
|
|
4657
|
+
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4658
|
+
if (
|
|
4659
|
+
turn.terminal ||
|
|
4660
|
+
this.activeTurn !== turn ||
|
|
4661
|
+
!this.isCurrentRuntime(runtime, turn.generation)
|
|
4662
|
+
)
|
|
4663
|
+
return;
|
|
4664
|
+
if (!turn.interrupted && state && (state.isStreaming || state.isCompacting)) {
|
|
4665
|
+
this.ignoreActiveTerminalCandidate(turn, {
|
|
4666
|
+
event,
|
|
4667
|
+
arrivalSequence: turn.activitySequence,
|
|
4668
|
+
confidence: event.requestId === undefined ? "ordered-legacy" : "keyed",
|
|
4669
|
+
});
|
|
4670
|
+
return;
|
|
4671
|
+
}
|
|
4672
|
+
if (state && !state.isStreaming && !state.isCompacting) {
|
|
4673
|
+
outcome = historyTerminalOutcome(history, event.messageCount, turn, event.messages ?? []);
|
|
4674
|
+
}
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
if (turn.terminal || this.activeTurn !== turn) return;
|
|
4678
|
+
if (turn.interrupted || outcome === "canceled") {
|
|
4679
|
+
this.subsessions?.terminalize("canceled");
|
|
4680
|
+
await this.finishTurn(turn, "canceled");
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
if (outcome === "completed") {
|
|
4684
|
+
await this.finishTurn(turn, "completed");
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
const error =
|
|
4688
|
+
outcome === "failed" ? "OMP assistant turn failed" : unknownTerminalOutcomeError(event, turn);
|
|
4689
|
+
this.subsessions?.terminalize("failed");
|
|
4690
|
+
await this.finishTurn(turn, "failed", { message: error });
|
|
4247
4691
|
}
|
|
4248
|
-
|
|
4249
4692
|
private publishPendingUsers(turn: ActiveTurn): void {
|
|
4250
4693
|
for (const pending of turn.pendingUsers.splice(0)) {
|
|
4251
4694
|
for (const echo of pending.bufferedEchoes) {
|
|
@@ -4283,6 +4726,18 @@ export class OmpProviderSession {
|
|
|
4283
4726
|
pending.bufferedEchoes.length = 0;
|
|
4284
4727
|
}
|
|
4285
4728
|
|
|
4729
|
+
private settleUnstartedTurn(turn: ActiveTurn): void {
|
|
4730
|
+
if (turn.started || turn.terminal) return;
|
|
4731
|
+
turn.steerReady.resolve();
|
|
4732
|
+
turn.starting = false;
|
|
4733
|
+
turn.terminal = true;
|
|
4734
|
+
this.stopUsagePoll(turn);
|
|
4735
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4736
|
+
this.resolveTurnPermissions(turn.turnId);
|
|
4737
|
+
this.projector.finishTurn(turn.turnId);
|
|
4738
|
+
if (this.activeTurn === turn) this.activeTurn = null;
|
|
4739
|
+
}
|
|
4740
|
+
|
|
4286
4741
|
private publishPromptResult(
|
|
4287
4742
|
turn: ActiveTurn,
|
|
4288
4743
|
result: Extract<ProviderEvent, { type: "session.prompt_result" }>["result"],
|
|
@@ -4308,7 +4763,7 @@ export class OmpProviderSession {
|
|
|
4308
4763
|
private finishTurn(
|
|
4309
4764
|
turn: ActiveTurn,
|
|
4310
4765
|
state: "completed" | "failed" | "canceled",
|
|
4311
|
-
error?:
|
|
4766
|
+
error?: ProviderError,
|
|
4312
4767
|
usageSampled = false,
|
|
4313
4768
|
override = false,
|
|
4314
4769
|
preserveCompactions = false,
|
|
@@ -4332,7 +4787,7 @@ export class OmpProviderSession {
|
|
|
4332
4787
|
turn.manualCompactionPending = false;
|
|
4333
4788
|
turn.agentEndPending = false;
|
|
4334
4789
|
this.cancelLocalOnlyCompletion(turn);
|
|
4335
|
-
this.
|
|
4790
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4336
4791
|
this.stopUsagePoll(turn);
|
|
4337
4792
|
if (turn.manualCompactionDeadlineTimer !== undefined) {
|
|
4338
4793
|
this.scheduler.clear(turn.manualCompactionDeadlineTimer);
|