@omercnet/paseo-omp 0.2.1-next.88.1 → 0.2.1-next.89.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/README.md +2 -2
- package/SUPPORT.md +3 -2
- package/TESTING.md +2 -2
- package/docs/core-provider-issue-audit.md +2 -1
- package/package.json +1 -1
- package/server/provider/session.ts +279 -169
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Scoring is deliberately mechanical so releases remain comparable:
|
|
|
49
49
|
|
|
50
50
|
| SDK capability | Completeness | Support and gap |
|
|
51
51
|
| --- | ---: | --- |
|
|
52
|
-
| `prompt.message` | **100%** | Text, structured attachments, optimistic correlation, streaming, and terminal outcomes map to native OMP prompts. |
|
|
52
|
+
| `prompt.message` | **100%** | Text, structured attachments, optimistic correlation, streaming, and terminal outcomes map to native OMP prompts. Matching terminal request IDs remain authoritative; released OMP builds without them use the ordered compatibility policy described below. |
|
|
53
53
|
| `prompt.command` | **100%** | Native command discovery plus structured `compact`, `autocompact`, `handoff`, `steer`, and `follow-up` dispatch. Individual commands can still fail when their documented OMP prerequisites are absent. |
|
|
54
54
|
| `prompt.image` | **100%** | Native image blocks for capable models; private bounded temporary files with cleanup for text-only models. |
|
|
55
55
|
| `prompt.output_schema` | **0%** | Not advertised. OMP has no equivalent structured-output contract; `outputSchema` fails visibly. |
|
|
@@ -97,7 +97,7 @@ The plugin always remains a separate provider. It registers `omp-plugin` and `om
|
|
|
97
97
|
|
|
98
98
|
Existing agents whose provider is `omp` remain owned by the bundled provider. New plugin agents persist under their selected `omp-plugin` or `omp-plugin-<profile>` identity with the plugin's versioned opaque handle. The plugin can list and import OMP-native sessions through its own provider flow, but it performs no implicit conversion of bundled-provider records.
|
|
99
99
|
|
|
100
|
-
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above.
|
|
100
|
+
OMP `18.1.15` is the oldest version tested end to end. The direct provider's hard compatibility gate is `rpc-ui` protocol v2: metadata-free legacy ready frames and v1-only runtimes are rejected before a provider session opens because they cannot support the advertised persistence and conversation-rewind capabilities. Typed tool approvals remain capability-gated and fall back as described above. For terminal completion, a matching `agent_end.requestId` is authoritative and mismatches are ignored before state changes. Released OMP 18.2.x builds that omit that field use an ordered fallback requiring a fresh correlated native user entry followed by current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. Ambiguous events while OMP is active are ignored; unresolved ambiguity after confirmed idle fails only the Paseo turn. This trades a bounded residual same-agent stale-event risk for compatibility with released OMP instead of terminating and lazily restarting the runtime after every later prompt. Paseo clients, including mobile clients inside reconnect grace, do not own or terminate the daemon-managed OMP session.
|
|
101
101
|
|
|
102
102
|
Install, update, disable, or remove `paseo-omp` independently of the bundled provider. Verify the provider snapshot contains `omp-plugin` after installation; a bundled `omp` entry may remain present and is not modified by this plugin.
|
|
103
103
|
|
package/SUPPORT.md
CHANGED
|
@@ -26,8 +26,8 @@ Do not put credentials, private repository paths, session transcripts, or unreda
|
|
|
26
26
|
- `omp` and `omp-plugin` are independent provider identities. Agents, provider settings, and persisted handles do not migrate automatically between them.
|
|
27
27
|
- Do not open the same underlying OMP session concurrently through both providers. Reservation tracking is provider-local and cannot coordinate ownership with Paseo's bundled adapter.
|
|
28
28
|
- OMP 18.1.15 does not advertise typed tool approvals. The plugin uses its bounded generic permission fallback until both peers negotiate `typedToolApprovals: 1`.
|
|
29
|
-
- OMP releases
|
|
30
|
-
- Timeline correlation rebuilds an invalid watermark from a complete pre-prompt `get_branch_messages` snapshot, not replayed model context or an evicting identity cache. Snapshots are limited to 1,024 entries and 4 MiB; duplicate IDs, surplus exact-text matches, unavailable history, and exceeded bounds leave users uncorrelated rather than claiming an old entry. Repeated accepted prompts within a turn consume matching branch occurrences in order.
|
|
29
|
+
- OMP releases through 18.2.x may omit the originating prompt `requestId` from `agent_end`. A matching ID is authoritative when present, and a mismatch is discarded before changing turn state. For an unkeyed later-turn terminal, the plugin uses a bounded ordered fallback: the accepted prompt must not be known local-only, a fresh native user entry must correlate to it, current-turn assistant activity must follow that entry, OMP must report idle and non-compacting, and no permission, tool, steer, or child-session work may remain. Ambiguous candidates are ignored while OMP is active; once OMP is confirmed idle, unresolved ambiguity fails only the Paseo turn and keeps the OMP process available. This deliberately accepts a residual same-agent risk: a sufficiently delayed unkeyed event with indistinguishable ordered evidence can still be misattributed. It is safer than accepting any idle `agent_end`, while remaining usable with released OMP builds that cannot provide request identity.
|
|
30
|
+
- Timeline correlation rebuilds an invalid watermark from a complete pre-prompt `get_branch_messages` snapshot, not replayed model context or an evicting identity cache. Snapshots are limited to 1,024 entries and 4 MiB; duplicate IDs, surplus exact-text matches, unavailable history, and exceeded bounds leave users uncorrelated rather than claiming an old entry. Repeated accepted prompts within a turn consume matching branch occurrences in order.
|
|
31
31
|
- Configured MCP servers are supported and bridged into OMP. Paseo's own orchestration tools appear under their native names when the daemon's **Enable Paseo tools** / `daemon.mcp.injectIntoAgents` setting is enabled; other MCP servers remain namespaced. Exact Paseo `toolPolicy` preapproval cannot be represented by OMP `set_host_tools` and therefore fails session startup closed. `disallowedTools` applies only to recognized native OMP built-ins; unknown names are rejected and MCP tools are not silently filtered through it.
|
|
32
32
|
- `qwen2.5:0.5b` is provided only for free exploratory inference. It may ignore exact-output instructions and is not a deterministic protocol or tool-use oracle; use `canary-mock/Deterministic Canary` for assertions.
|
|
33
33
|
- The deterministic mock does not implement OMP's compaction-summary contract, so `/compact` reports `OMP compaction failed` in the canary; compaction remains covered by protocol fixtures.
|
|
@@ -35,6 +35,7 @@ Do not put credentials, private repository paths, session transcripts, or unreda
|
|
|
35
35
|
- On official Paseo 0.8.0, requesting a live approval-mode change that the plugin rejects can trigger the daemon's unhandled-rejection restart path. Create a new `full`, `write`, or `ask` session instead of changing mode in place.
|
|
36
36
|
- Native Fast mode and a first-class plan mode are not exposed. `/handoff` is implemented but is not reproducible in the controlled canary without its native OMP workflow prerequisites.
|
|
37
37
|
- Terminal-started OMP sessions are discoverable and importable but are not registered automatically through a terminal hook.
|
|
38
|
+
- Paseo clients do not own OMP sessions. Mobile or desktop disconnects remain subject to Paseo's reconnect grace while the daemon-owned provider and OMP child continue independently; reconnecting can reveal an existing turn but does not transfer or sever runtime ownership.
|
|
38
39
|
- OMP tool output reaching RPC stdout is strictly parsed and bounded, but channel purity ultimately depends on OMP keeping non-protocol output off stdout.
|
|
39
40
|
|
|
40
41
|
The deduplicated comparison with every known OMP report in `getpaseo/paseo` is maintained in [docs/core-provider-issue-audit.md](docs/core-provider-issue-audit.md).
|
package/TESTING.md
CHANGED
|
@@ -32,7 +32,7 @@ Classifications:
|
|
|
32
32
|
| Image prompts | **Equivalent** | Valid native image models receive image blocks; text-only models receive private content-addressed local files with an aggregate cap and turn/session/failure cleanup. The file path is valid because the direct provider and OMP child share the daemon host. |
|
|
33
33
|
| Structured attachments | **Equivalent** | Forge change requests/issues, legacy GitHub forms, text, reviews, and uploaded files render to bounded OMP prompt text; regression in `tests/provider.test.ts`. |
|
|
34
34
|
| Optimistic message correlation | **Equivalent** | Native entry lookup, repeated-text occurrence correlation, steering correlation, replay-boundary dedupe, and exactly-one `session.prompt_result` regressions. Bounded branch snapshots rebuild incomplete or evicted replay watermarks; duplicate IDs, surplus matching entries, unavailable snapshots, and count/byte overflow retain local user-message fallback instead of claiming an old entry. |
|
|
35
|
-
| Terminal
|
|
35
|
+
| Terminal correlation on later turns | **Equivalent with legacy fallback** | A matching `agent_end.requestId` is authoritative and a mismatch is discarded before turn state changes. Released OMP builds that omit the field use ordered evidence from the accepted prompt: a fresh branch-correlated native user entry, later current-turn assistant activity, an idle non-compacting runtime, and no conflicting permission, tool, steer, or child-session work. `prompt_result.agentInvoked: false` remains local-only. Active ambiguous terminals are ignored; confirmed-idle ambiguity fails only the Paseo turn without killing the OMP process. Tests cover three sequential legacy prompts, stale terminals before evidence and while active, repeated text, local-only results, keyed mismatch/match, and degraded state/history. The remaining risk is same-agent misattribution when a delayed unkeyed event is indistinguishable from this ordered evidence. |
|
|
36
36
|
| Streaming assistant text | **Equivalent** | `OmpTimelineProjector` publishes stable complete snapshots with frame coalescing and bounded retained bytes. |
|
|
37
37
|
| Streaming reasoning | **Equivalent** | Indexed thinking blocks map to stable `reasoning` items and share stream bounds. |
|
|
38
38
|
| `contentIndex` ordering | **Equivalent** | Stable 0→1→0 updates, sparse-index rejection, and 64-block bounds are tested in `tests/provider.test.ts`. |
|
|
@@ -103,7 +103,7 @@ npm run test:integration:canary
|
|
|
103
103
|
|
|
104
104
|
Set `PASEO_CANARY_OMP_VERSION` to exercise another pinned release. The local runner knows the SHA-256 values for every version in the compatibility matrix; an unlisted version requires explicit `PASEO_CANARY_OMP_SHA256_AMD64` and `PASEO_CANARY_OMP_SHA256_ARM64` values. It allocates isolated loopback ports, builds a version-specific image, waits for both services to become healthy, runs the deterministic provider scenarios, prints container logs on failure, and always removes its containers, volumes, and image namespace.
|
|
105
105
|
|
|
106
|
-
The Linux real-OMP CI matrix downloads checksummed `omp-linux-x64` assets for OMP 17.2.15, 17.3.4, 18.0.11, 18.1.10, 18.1.15, 18.1.22, and 18.2.0, verifies each binary's pinned GitHub release SHA-256, and runs `PASEO_OMP_REAL_E2E=1 PASEO_OMP_VERSION=<version> npm test -- tests/provider.real.e2e.test.ts`. The historical entries are the four pre-floor releases with more than 4,000 downloads shown by npm for the seven days ending 2026-09-15; they are compatibility regression probes, not a support commitment. The remaining entries cover the supported floor, newest patch in that minor line, and current release. The test uses the real OMP binary and a local deterministic OpenAI-compatible model endpoint,
|
|
106
|
+
The Linux real-OMP CI matrix downloads checksummed `omp-linux-x64` assets for OMP 17.2.15, 17.3.4, 18.0.11, 18.1.10, 18.1.15, 18.1.22, and 18.2.0, verifies each binary's pinned GitHub release SHA-256, and runs `PASEO_OMP_REAL_E2E=1 PASEO_OMP_VERSION=<version> npm test -- tests/provider.real.e2e.test.ts`. The historical entries are the four pre-floor releases with more than 4,000 downloads shown by npm for the seven days ending 2026-09-15; they are compatibility regression probes, not a support commitment. The remaining entries cover the supported floor, newest patch in that minor line, and current release. The test uses the real OMP binary and a local deterministic OpenAI-compatible model endpoint, including three sequential prompts on one native session, a Bash tool, and oversized-image transport.
|
|
107
107
|
|
|
108
108
|
The controlled canary in `canary/compose.yml` builds this plugin into that official Paseo image and installs checksummed OMP binaries for `amd64` and `arm64`. `canary/smoke.ts` passed catalog/mode discovery, text and image prompts, Bash, a configured stdio MCP tool, permission allow/deny/cancel, steering, interruption, session listing/import/resume, subagents, conversation rewind followed by another turn, Hub process visibility, usage, and every plugin RPC. Browser verification loaded the global OMP health/configuration surface and the agent-scoped Hub, Memory, and Sessions controls. The default mock returned `CANARY_MOCK_OK`, `CANARY_TOOL_OK`, and `CANARY_MCP_OK`; the optional Ollama profile pulled `qwen2.5:0.5b` and completed turns without paid credentials. The harness records the canary-only `/compact` and `/handoff` failures instead of masking them.
|
|
109
109
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Core OMP provider issue audit
|
|
2
2
|
|
|
3
|
-
This audit compares reports in `getpaseo/paseo` with the community `omp-plugin` provider. It was refreshed on 2026-09-
|
|
3
|
+
This audit compares reports in `getpaseo/paseo` with the community `omp-plugin` provider. It was refreshed on 2026-09-18 from GitHub issue titles and bodies containing `OMP`, `oh-my-pi`, or `rpc-ui`, OMP-related pull requests, and materially equivalent Pi/RPC reports. GitHub Discussions were also inspected; the repository had no OMP-related discussion.
|
|
4
4
|
|
|
5
5
|
Issue and pull-request pairs are consolidated by root cause. A closed upstream issue does not prove the plugin implements the behavior, and an open upstream issue does not imply the plugin is affected.
|
|
6
6
|
|
|
@@ -20,6 +20,7 @@ Status meanings:
|
|
|
20
20
|
| [#2260](https://github.com/getpaseo/paseo/issues/2260), [PR #2261](https://github.com/getpaseo/paseo/pull/2261) | **Verified** | Incomplete or compacted `agent_end` frames use complete streamed `message_end` evidence or bounded history correlated by entry ID, with idle state confirmed before and after retrieval. Success, failure, native cancellation, and concurrent interrupts are preserved; missing, unavailable, non-correlatable, or conflicting terminal evidence fails closed with content-free count diagnostics. |
|
|
21
21
|
| [#2281](https://github.com/getpaseo/paseo/issues/2281), [PR #2282](https://github.com/getpaseo/paseo/pull/2282) | **Verified** | Local-only prompt results and structured commands have explicit terminal ownership. |
|
|
22
22
|
| [#3654](https://github.com/getpaseo/paseo/issues/3654), [#3998](https://github.com/getpaseo/paseo/issues/3998), [PR #3667](https://github.com/getpaseo/paseo/pull/3667) | **Verified** | Post-`agent_end` state reconciliation is bounded. Stale or unavailable `get_state` cannot leave a turn running forever. |
|
|
23
|
+
| Upstream OMP [PR #12331](https://github.com/can1357/oh-my-pi/pull/12331) and released OMP 18.2.x unkeyed `agent_end` frames | **Mitigated** | Request-keyed terminals use exact matching. For released binaries that omit the key, the plugin accepts only ordered current-prompt evidence: a fresh branch-correlated user entry followed by assistant activity, confirmed native idle/non-compacting state, and no active permission, tool, steer, or child work. Active or pre-evidence stale candidates are ignored. Confirmed-idle ambiguity fails only the Paseo turn, not the OMP runtime. The remaining same-agent stale-event misattribution risk is explicit and bounded; waiting for the upstream protocol fix would make every later turn unusable on published releases. Client reconnects, including mobile reconnect grace, are host transport behavior and do not own the daemon-managed provider session. |
|
|
23
24
|
| [#3999](https://github.com/getpaseo/paseo/issues/3999), [#4000](https://github.com/getpaseo/paseo/issues/4000), [#4039](https://github.com/getpaseo/paseo/issues/4039), [PR #3772](https://github.com/getpaseo/paseo/pull/3772), [PR #4217](https://github.com/getpaseo/paseo/pull/4217) | **Verified** | `prompt.steer` is negotiated and implemented with expected-turn checks, acknowledgement ordering, duplicate correlation, and interrupt/terminal race coverage. |
|
|
24
25
|
| Shared RPC cancellation [#3540](https://github.com/getpaseo/paseo/issues/3540), Pi [#3749](https://github.com/getpaseo/paseo/issues/3749) | **Verified in provider** | Native abort, exactly-one terminal event, permission cleanup, descendant cleanup, and uncertain-cleanup quarantine are tested. UI keyboard delivery remains host-owned. |
|
|
25
26
|
| [#3218](https://github.com/getpaseo/paseo/issues/3218) | **Verified at provider boundary** | Session close owns and awaits OMP process cleanup. Whether every archive UI route invokes provider close is a host concern. |
|
package/package.json
CHANGED
|
@@ -228,6 +228,24 @@ type PendingUser = {
|
|
|
228
228
|
fallbackOnFinish: boolean;
|
|
229
229
|
bufferedEchoes: OmpMessage[];
|
|
230
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
|
+
};
|
|
231
249
|
|
|
232
250
|
type ActiveTurn = {
|
|
233
251
|
turnId: string;
|
|
@@ -245,10 +263,8 @@ type ActiveTurn = {
|
|
|
245
263
|
awaitingPermissionEvidence: boolean;
|
|
246
264
|
activitySequence: number;
|
|
247
265
|
acknowledged: boolean;
|
|
248
|
-
|
|
249
|
-
terminalOwnershipRequired: boolean;
|
|
266
|
+
terminalCorrelation: TerminalCorrelation;
|
|
250
267
|
replayingBufferedEvents: boolean;
|
|
251
|
-
bufferedTerminalOwnershipEvidence: boolean;
|
|
252
268
|
agentInvoked?: boolean;
|
|
253
269
|
nativeRequestId?: string;
|
|
254
270
|
promptAcceptedEventIndex?: number;
|
|
@@ -263,14 +279,15 @@ type ActiveTurn = {
|
|
|
263
279
|
agentEndRetryTimer?: unknown;
|
|
264
280
|
agentEndDeadlineTimer?: unknown;
|
|
265
281
|
agentEndCheck?: Promise<void>;
|
|
266
|
-
|
|
282
|
+
ambiguousTerminalTimer?: unknown;
|
|
267
283
|
terminalizing: boolean;
|
|
268
284
|
terminalization?: Promise<void>;
|
|
269
285
|
terminalOutcome?: TurnOutcome;
|
|
270
286
|
terminalWake?: VoidDeferred;
|
|
271
287
|
steerReady: VoidDeferred;
|
|
272
288
|
steersInFlight: number;
|
|
273
|
-
deferredAgentEnd?:
|
|
289
|
+
deferredAgentEnd?: TerminalCandidate;
|
|
290
|
+
activeToolCallIds: Set<string>;
|
|
274
291
|
bufferedEvents: OmpRpcEvent[];
|
|
275
292
|
pendingUsers: PendingUser[];
|
|
276
293
|
userEchoes: OmpMessage[];
|
|
@@ -784,7 +801,7 @@ function createActiveTurn(
|
|
|
784
801
|
clientMessageId: string,
|
|
785
802
|
text: string,
|
|
786
803
|
generation: number,
|
|
787
|
-
|
|
804
|
+
terminalPolicy: TerminalCorrelation["policy"],
|
|
788
805
|
manualCompaction = false,
|
|
789
806
|
): ActiveTurn {
|
|
790
807
|
return {
|
|
@@ -809,11 +826,10 @@ function createActiveTurn(
|
|
|
809
826
|
manualCompaction,
|
|
810
827
|
activitySequence: 0,
|
|
811
828
|
acknowledged: false,
|
|
812
|
-
|
|
829
|
+
terminalCorrelation: { policy: terminalPolicy, evidence: new Map() },
|
|
813
830
|
replayingBufferedEvents: false,
|
|
814
|
-
bufferedTerminalOwnershipEvidence: false,
|
|
815
|
-
terminalOwnershipRequired,
|
|
816
831
|
steersInFlight: 0,
|
|
832
|
+
activeToolCallIds: new Set(),
|
|
817
833
|
steerReady: Promise.withResolvers<void>(),
|
|
818
834
|
userCorrelationActive: false,
|
|
819
835
|
userLookups: new Set(),
|
|
@@ -833,6 +849,35 @@ function createActiveTurn(
|
|
|
833
849
|
],
|
|
834
850
|
};
|
|
835
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
|
+
}
|
|
836
881
|
|
|
837
882
|
export class OmpProviderSession {
|
|
838
883
|
readonly id: string;
|
|
@@ -1864,7 +1909,7 @@ export class OmpProviderSession {
|
|
|
1864
1909
|
input.prompt.clientMessageId,
|
|
1865
1910
|
payload.text,
|
|
1866
1911
|
this.generation,
|
|
1867
|
-
this.runtimeTurnCompleted,
|
|
1912
|
+
this.runtimeTurnCompleted ? "ordered-legacy" : "initial-turn",
|
|
1868
1913
|
slashCommandName(payload.text) === "compact",
|
|
1869
1914
|
);
|
|
1870
1915
|
this.activeTurn = turn;
|
|
@@ -1901,7 +1946,11 @@ export class OmpProviderSession {
|
|
|
1901
1946
|
return;
|
|
1902
1947
|
}
|
|
1903
1948
|
const ownershipPending = turn.pendingUsers[0];
|
|
1904
|
-
if (
|
|
1949
|
+
if (
|
|
1950
|
+
turn.terminalCorrelation.policy === "ordered-legacy" &&
|
|
1951
|
+
!this.branchWatermarkValid &&
|
|
1952
|
+
ownershipPending
|
|
1953
|
+
) {
|
|
1905
1954
|
await this.refreshBranchEntries(turn, ownershipPending);
|
|
1906
1955
|
if (
|
|
1907
1956
|
this.closed ||
|
|
@@ -1939,13 +1988,10 @@ export class OmpProviderSession {
|
|
|
1939
1988
|
0,
|
|
1940
1989
|
turn.promptAcceptedEventIndex ?? bufferedEvents.length,
|
|
1941
1990
|
);
|
|
1942
|
-
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event);
|
|
1991
|
+
for (const event of preAcceptanceEvents) this.handleTurnEvent(turn, event, false);
|
|
1943
1992
|
this.projector.acceptLiveTurn(turn.turnId);
|
|
1944
1993
|
for (const event of bufferedEvents) this.handleTurnEvent(turn, event);
|
|
1945
1994
|
turn.replayingBufferedEvents = false;
|
|
1946
|
-
if (acknowledgement.agentInvoked === true && turn.bufferedTerminalOwnershipEvidence) {
|
|
1947
|
-
this.markTerminalOwnershipEvidence(turn);
|
|
1948
|
-
}
|
|
1949
1995
|
if (
|
|
1950
1996
|
acknowledgement.agentInvoked === false &&
|
|
1951
1997
|
turn.agentInvoked !== true &&
|
|
@@ -2096,7 +2142,7 @@ export class OmpProviderSession {
|
|
|
2096
2142
|
if (this.activeTurn) {
|
|
2097
2143
|
throw new OmpPublicError("OMP already has an active turn; send this message as a steer");
|
|
2098
2144
|
}
|
|
2099
|
-
const turn = createActiveTurn(clientMessageId, text, this.generation,
|
|
2145
|
+
const turn = createActiveTurn(clientMessageId, text, this.generation, "native-command");
|
|
2100
2146
|
this.activeTurn = turn;
|
|
2101
2147
|
try {
|
|
2102
2148
|
await invoke();
|
|
@@ -2994,6 +3040,7 @@ export class OmpProviderSession {
|
|
|
2994
3040
|
return;
|
|
2995
3041
|
}
|
|
2996
3042
|
turn.localOnlyDisabled = true;
|
|
3043
|
+
this.cancelAmbiguousTerminal(turn);
|
|
2997
3044
|
turn.deferredAgentEnd = undefined;
|
|
2998
3045
|
this.acceptPendingUser(turn, pending);
|
|
2999
3046
|
this.emit({
|
|
@@ -3145,7 +3192,11 @@ export class OmpProviderSession {
|
|
|
3145
3192
|
this.handleTurnEvent(turn, event);
|
|
3146
3193
|
}
|
|
3147
3194
|
|
|
3148
|
-
private handleTurnEvent(
|
|
3195
|
+
private handleTurnEvent(
|
|
3196
|
+
turn: ActiveTurn,
|
|
3197
|
+
event: OmpRpcEvent,
|
|
3198
|
+
correlatesToAcceptedPrompt = true,
|
|
3199
|
+
): void {
|
|
3149
3200
|
if (
|
|
3150
3201
|
turn.generation !== this.generation ||
|
|
3151
3202
|
turn.terminal ||
|
|
@@ -3161,7 +3212,7 @@ export class OmpProviderSession {
|
|
|
3161
3212
|
) {
|
|
3162
3213
|
return;
|
|
3163
3214
|
}
|
|
3164
|
-
if (event.type === "message_end") {
|
|
3215
|
+
if (event.type === "message_end" && correlatesToAcceptedPrompt) {
|
|
3165
3216
|
const entryId = nativeEntryId(event.message);
|
|
3166
3217
|
if (!entryId || turn.streamedMessageEntryIds.length >= MAX_AGENT_END_CORRELATION_MESSAGES) {
|
|
3167
3218
|
turn.streamedMessageIdentityComplete = false;
|
|
@@ -3187,15 +3238,19 @@ export class OmpProviderSession {
|
|
|
3187
3238
|
if (event.type === "prompt_result") {
|
|
3188
3239
|
if (!event.id || event.id !== turn.nativeRequestId) return;
|
|
3189
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.
|
|
3190
3243
|
this.markAgentEvidence(turn);
|
|
3191
|
-
if (turn.replayingBufferedEvents) turn.bufferedTerminalOwnershipEvidence = true;
|
|
3192
|
-
else this.markTerminalOwnershipEvidence(turn);
|
|
3193
3244
|
return;
|
|
3194
3245
|
}
|
|
3195
3246
|
if (turn.localOnlyDisabled || turn.steersInFlight > 0) return;
|
|
3196
3247
|
if (!turn.nativeActivity && !turn.awaitingPermissionEvidence) {
|
|
3197
3248
|
turn.agentInvoked = false;
|
|
3198
3249
|
turn.localOnlyEligible = true;
|
|
3250
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
3251
|
+
turn.deferredAgentEnd = undefined;
|
|
3252
|
+
this.cancelAmbiguousTerminal(turn);
|
|
3253
|
+
}
|
|
3199
3254
|
this.scheduleLocalOnlyCompletion(turn);
|
|
3200
3255
|
}
|
|
3201
3256
|
return;
|
|
@@ -3243,8 +3298,10 @@ export class OmpProviderSession {
|
|
|
3243
3298
|
return;
|
|
3244
3299
|
}
|
|
3245
3300
|
if (event.type === "agent_end" && turn.manualCompactionPending) return;
|
|
3246
|
-
if (event.type === "
|
|
3247
|
-
|
|
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;
|
|
3248
3305
|
}
|
|
3249
3306
|
if (event.type === "tool_execution_start" || event.type === "tool_execution_end") {
|
|
3250
3307
|
try {
|
|
@@ -3254,23 +3311,6 @@ export class OmpProviderSession {
|
|
|
3254
3311
|
return;
|
|
3255
3312
|
}
|
|
3256
3313
|
}
|
|
3257
|
-
if (isNativeTurnActivity(event)) {
|
|
3258
|
-
turn.nativeActivity = true;
|
|
3259
|
-
this.cancelLocalOnlyCompletion(turn);
|
|
3260
|
-
}
|
|
3261
|
-
if (event.type === "message_end" && event.message.role === "user") {
|
|
3262
|
-
this.markAgentEvidence(turn);
|
|
3263
|
-
this.projectUserEcho(turn, event.message);
|
|
3264
|
-
return;
|
|
3265
|
-
}
|
|
3266
|
-
if (
|
|
3267
|
-
(event.type === "message_start" ||
|
|
3268
|
-
event.type === "message_update" ||
|
|
3269
|
-
event.type === "message_end") &&
|
|
3270
|
-
event.message.role === "assistant"
|
|
3271
|
-
) {
|
|
3272
|
-
turn.awaitingPermissionEvidence = false;
|
|
3273
|
-
}
|
|
3274
3314
|
if (event.type === "agent_end") {
|
|
3275
3315
|
if (event.isTerminal === false) {
|
|
3276
3316
|
turn.completedMessageCount = 0;
|
|
@@ -3280,41 +3320,51 @@ export class OmpProviderSession {
|
|
|
3280
3320
|
turn.lastCompletedAssistantEntryId = undefined;
|
|
3281
3321
|
return;
|
|
3282
3322
|
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
!(turn.replayingBufferedEvents && turn.bufferedTerminalOwnershipEvidence)
|
|
3289
|
-
) {
|
|
3290
|
-
this.scheduleTerminalOwnershipTimeout(turn);
|
|
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);
|
|
3291
3328
|
return;
|
|
3292
3329
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
if (
|
|
3296
|
-
turn.
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
}
|
|
3304
|
-
if (turn.steersInFlight > 0) {
|
|
3305
|
-
turn.deferredAgentEnd = event;
|
|
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;
|
|
3306
3340
|
return;
|
|
3307
3341
|
}
|
|
3308
|
-
this.beginTerminalization(turn,
|
|
3342
|
+
this.beginTerminalization(turn, candidate);
|
|
3309
3343
|
return;
|
|
3310
3344
|
}
|
|
3311
|
-
if (isNativeTurnActivity(event))
|
|
3345
|
+
if (isNativeTurnActivity(event)) {
|
|
3346
|
+
turn.nativeActivity = true;
|
|
3347
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
3348
|
+
}
|
|
3312
3349
|
if (event.type === "message_end" && event.message.role === "user") {
|
|
3313
3350
|
this.markAgentEvidence(turn);
|
|
3314
3351
|
this.projectUserEcho(turn, event.message);
|
|
3315
3352
|
return;
|
|
3316
3353
|
}
|
|
3354
|
+
if (
|
|
3355
|
+
(event.type === "message_start" ||
|
|
3356
|
+
event.type === "message_update" ||
|
|
3357
|
+
event.type === "message_end") &&
|
|
3358
|
+
event.message.role === "assistant"
|
|
3359
|
+
) {
|
|
3360
|
+
turn.awaitingPermissionEvidence = false;
|
|
3361
|
+
if (correlatesToAcceptedPrompt) {
|
|
3362
|
+
turn.terminalCorrelation.evidence.set("current-assistant", turn.activitySequence + 1);
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
if (isNativeTurnActivity(event)) this.markAgentEvidence(turn);
|
|
3317
3366
|
this.projector.project(event, turn.turnId);
|
|
3367
|
+
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
3318
3368
|
}
|
|
3319
3369
|
|
|
3320
3370
|
private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
|
|
@@ -3385,6 +3435,7 @@ export class OmpProviderSession {
|
|
|
3385
3435
|
pending.bufferedEchoes.push(...turn.userEchoes.splice(0));
|
|
3386
3436
|
return;
|
|
3387
3437
|
}
|
|
3438
|
+
const observedSequence = turn.activitySequence;
|
|
3388
3439
|
let resolvedId = entryId ?? this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
3389
3440
|
if (!resolvedId && (await this.refreshBranchEntries(turn, pending))) {
|
|
3390
3441
|
resolvedId = this.claimUnclaimedBranchEntry(turn, pending.text);
|
|
@@ -3400,7 +3451,7 @@ export class OmpProviderSession {
|
|
|
3400
3451
|
turn.userEchoes.shift();
|
|
3401
3452
|
if (!resolvedId) return;
|
|
3402
3453
|
turn.pendingUsers.shift();
|
|
3403
|
-
this.publishCorrelatedUser(turn, pending, resolvedId);
|
|
3454
|
+
this.publishCorrelatedUser(turn, pending, resolvedId, observedSequence);
|
|
3404
3455
|
}
|
|
3405
3456
|
}
|
|
3406
3457
|
private async refreshBranchEntries(turn: ActiveTurn, pending: PendingUser): Promise<boolean> {
|
|
@@ -4234,23 +4285,27 @@ export class OmpProviderSession {
|
|
|
4234
4285
|
return this.activeTurn?.turnId === pending.turnId && !this.activeTurn.terminal;
|
|
4235
4286
|
}
|
|
4236
4287
|
|
|
4237
|
-
private
|
|
4238
|
-
const turn = this.activeTurn;
|
|
4239
|
-
if (!turn?.deferredAgentEnd || turn.terminal || turn.terminalizing) return;
|
|
4288
|
+
private hasTerminalConflict(turn: ActiveTurn, includeChildren = true): boolean {
|
|
4240
4289
|
const ownsTurn = (pending: PendingPermission | PendingToolPermission) =>
|
|
4241
4290
|
pending.turnId === turn.turnId;
|
|
4242
|
-
|
|
4291
|
+
return (
|
|
4292
|
+
turn.awaitingPermissionEvidence ||
|
|
4243
4293
|
[...this.pendingPermissions.values()].some(ownsTurn) ||
|
|
4244
4294
|
[...this.inFlightPermissions.values()].some(ownsTurn) ||
|
|
4245
4295
|
[...this.pendingToolPermissions.values()].some(ownsTurn) ||
|
|
4246
|
-
[...this.inFlightToolPermissions.values()].some(ownsTurn)
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
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;
|
|
4253
4307
|
const deferred = turn.deferredAgentEnd;
|
|
4308
|
+
if (deferred.confidence === "ambiguous") return;
|
|
4254
4309
|
turn.deferredAgentEnd = undefined;
|
|
4255
4310
|
this.beginTerminalization(turn, deferred);
|
|
4256
4311
|
}
|
|
@@ -4266,12 +4321,18 @@ export class OmpProviderSession {
|
|
|
4266
4321
|
return this.slashCommands.has(commandName);
|
|
4267
4322
|
}
|
|
4268
4323
|
|
|
4269
|
-
private publishCorrelatedUser(
|
|
4324
|
+
private publishCorrelatedUser(
|
|
4325
|
+
turn: ActiveTurn,
|
|
4326
|
+
pending: PendingUser,
|
|
4327
|
+
entryId: string | undefined,
|
|
4328
|
+
observedSequence: number,
|
|
4329
|
+
): void {
|
|
4270
4330
|
if (entryId) {
|
|
4271
4331
|
if (this.emittedEntryIds.has(entryId)) return;
|
|
4272
4332
|
this.seenEntryIds.add(entryId);
|
|
4273
4333
|
this.emittedEntryIds.add(entryId);
|
|
4274
|
-
|
|
4334
|
+
turn.terminalCorrelation.evidence.set("fresh-native-user", observedSequence);
|
|
4335
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4275
4336
|
if (this.branchWatermarkValid && !this.branchEntryIds.has(entryId)) {
|
|
4276
4337
|
if (this.branchEntryIds.size >= MAX_UNCLAIMED_BRANCH_ENTRIES)
|
|
4277
4338
|
this.quarantineBranchEntries();
|
|
@@ -4291,12 +4352,7 @@ export class OmpProviderSession {
|
|
|
4291
4352
|
turn.activitySequence += 1;
|
|
4292
4353
|
turn.localOnlyEligible = false;
|
|
4293
4354
|
this.cancelLocalOnlyCompletion(turn);
|
|
4294
|
-
|
|
4295
|
-
}
|
|
4296
|
-
|
|
4297
|
-
private markTerminalOwnershipEvidence(turn: ActiveTurn): void {
|
|
4298
|
-
turn.terminalOwnershipEvidence = true;
|
|
4299
|
-
this.cancelTerminalOwnershipTimeout(turn);
|
|
4355
|
+
this.refreshAmbiguousTerminal(turn);
|
|
4300
4356
|
}
|
|
4301
4357
|
|
|
4302
4358
|
private scheduleLocalOnlyCompletion(turn: ActiveTurn): void {
|
|
@@ -4322,32 +4378,68 @@ export class OmpProviderSession {
|
|
|
4322
4378
|
turn.localOnlyTimer = undefined;
|
|
4323
4379
|
}
|
|
4324
4380
|
|
|
4325
|
-
private
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
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
|
+
}
|
|
4331
4393
|
return;
|
|
4332
4394
|
}
|
|
4333
|
-
turn.
|
|
4334
|
-
turn.
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
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);
|
|
4344
4408
|
}, AGENT_END_STATE_TIMEOUT_MS);
|
|
4345
4409
|
}
|
|
4346
4410
|
|
|
4347
|
-
private
|
|
4348
|
-
if (turn.
|
|
4349
|
-
this.scheduler.clear(turn.
|
|
4350
|
-
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
|
+
});
|
|
4351
4443
|
}
|
|
4352
4444
|
|
|
4353
4445
|
private async completeLocalOnlyTurn(turn: ActiveTurn): Promise<void> {
|
|
@@ -4373,15 +4465,44 @@ export class OmpProviderSession {
|
|
|
4373
4465
|
await this.finishTurn(turn, "completed", undefined, false, false, true);
|
|
4374
4466
|
}
|
|
4375
4467
|
|
|
4376
|
-
private
|
|
4377
|
-
turn
|
|
4378
|
-
|
|
4379
|
-
|
|
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 {
|
|
4380
4496
|
if (turn.terminal || turn.terminalizing || turn.agentEndPending || this.activeTurn !== turn) {
|
|
4381
4497
|
return;
|
|
4382
4498
|
}
|
|
4383
|
-
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn,
|
|
4499
|
+
if (!turn.interrupted && this.deferAgentEndForSubsessions(turn, candidate)) return;
|
|
4384
4500
|
turn.agentEndPending = true;
|
|
4501
|
+
if (turn.deferredAgentEnd?.confidence === "ambiguous") {
|
|
4502
|
+
turn.deferredAgentEnd = undefined;
|
|
4503
|
+
}
|
|
4504
|
+
turn.terminalizing = true;
|
|
4505
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4385
4506
|
turn.usageSampleFloor = this.usageSequence + 1;
|
|
4386
4507
|
if (this.usageSample?.turn === turn && this.usageSample.sequence < turn.usageSampleFloor) {
|
|
4387
4508
|
this.usageSample = null;
|
|
@@ -4391,21 +4512,21 @@ export class OmpProviderSession {
|
|
|
4391
4512
|
turn.agentEndDeadlineTimer = undefined;
|
|
4392
4513
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4393
4514
|
if (
|
|
4394
|
-
|
|
4395
|
-
(
|
|
4515
|
+
candidate.confidence === "keyed" ||
|
|
4516
|
+
(candidate.confidence === "initial-turn" && !turn.userEchoObserved)
|
|
4396
4517
|
) {
|
|
4397
|
-
this.
|
|
4518
|
+
void this.completeAgentEnd(turn, candidate.event);
|
|
4398
4519
|
} else {
|
|
4399
|
-
|
|
4520
|
+
this.handleRuntimeFailure("OMP agent_end state could not be confirmed");
|
|
4400
4521
|
}
|
|
4401
4522
|
}, AGENT_END_SETTLE_MS);
|
|
4402
|
-
this.finishFromAgentEnd(turn,
|
|
4523
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4403
4524
|
}
|
|
4404
4525
|
|
|
4405
4526
|
private resumeAfterFailedSteer(turn: ActiveTurn): void {
|
|
4406
4527
|
if (turn.terminal || this.activeTurn !== turn || turn.steersInFlight > 0) return;
|
|
4407
4528
|
const deferred = turn.deferredAgentEnd;
|
|
4408
|
-
if (deferred) {
|
|
4529
|
+
if (deferred && deferred.confidence !== "ambiguous" && !this.hasTerminalConflict(turn)) {
|
|
4409
4530
|
turn.deferredAgentEnd = undefined;
|
|
4410
4531
|
this.beginTerminalization(turn, deferred);
|
|
4411
4532
|
return;
|
|
@@ -4414,13 +4535,11 @@ export class OmpProviderSession {
|
|
|
4414
4535
|
this.scheduleLocalOnlyCompletion(turn);
|
|
4415
4536
|
}
|
|
4416
4537
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4420
|
-
): boolean {
|
|
4538
|
+
|
|
4539
|
+
private deferAgentEndForSubsessions(turn: ActiveTurn, candidate: TerminalCandidate): boolean {
|
|
4421
4540
|
if (!this.subsessions?.hasActiveChildren()) return false;
|
|
4422
4541
|
turn.terminalizing = false;
|
|
4423
|
-
turn.deferredAgentEnd =
|
|
4542
|
+
turn.deferredAgentEnd = candidate;
|
|
4424
4543
|
void this.subsessions.reconcile(this.runtime).catch(() => {
|
|
4425
4544
|
if (!turn.terminal && this.activeTurn === turn) {
|
|
4426
4545
|
this.handleRuntimeFailure("OMP subagent reconciliation failed");
|
|
@@ -4431,31 +4550,22 @@ export class OmpProviderSession {
|
|
|
4431
4550
|
|
|
4432
4551
|
private resumeDeferredAgentEnd(): void {
|
|
4433
4552
|
const turn = this.activeTurn;
|
|
4434
|
-
if (
|
|
4435
|
-
!turn ||
|
|
4436
|
-
turn.terminal ||
|
|
4437
|
-
turn.terminalizing ||
|
|
4438
|
-
turn.steersInFlight > 0 ||
|
|
4439
|
-
this.subsessions?.hasActiveChildren()
|
|
4440
|
-
) {
|
|
4553
|
+
if (!turn || turn.terminal || turn.terminalizing || this.hasTerminalConflict(turn)) {
|
|
4441
4554
|
return;
|
|
4442
4555
|
}
|
|
4443
|
-
const
|
|
4444
|
-
if (!
|
|
4556
|
+
const candidate = turn.deferredAgentEnd;
|
|
4557
|
+
if (!candidate || candidate.confidence === "ambiguous") return;
|
|
4445
4558
|
turn.deferredAgentEnd = undefined;
|
|
4446
|
-
this.beginTerminalization(turn,
|
|
4559
|
+
this.beginTerminalization(turn, candidate);
|
|
4447
4560
|
}
|
|
4448
4561
|
|
|
4449
|
-
private finishFromAgentEnd(
|
|
4450
|
-
turn: ActiveTurn,
|
|
4451
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4452
|
-
): void {
|
|
4562
|
+
private finishFromAgentEnd(turn: ActiveTurn, candidate: TerminalCandidate): void {
|
|
4453
4563
|
if (!turn.agentEndPending || turn.terminal) return;
|
|
4454
4564
|
if (turn.agentEndCheck) {
|
|
4455
|
-
turn.deferredAgentEnd =
|
|
4565
|
+
turn.deferredAgentEnd = candidate;
|
|
4456
4566
|
return;
|
|
4457
4567
|
}
|
|
4458
|
-
const check = this.checkAgentEndState(turn,
|
|
4568
|
+
const check = this.checkAgentEndState(turn, candidate);
|
|
4459
4569
|
turn.agentEndCheck = check;
|
|
4460
4570
|
void check.finally(() => {
|
|
4461
4571
|
if (turn.agentEndCheck !== check) return;
|
|
@@ -4467,13 +4577,7 @@ export class OmpProviderSession {
|
|
|
4467
4577
|
});
|
|
4468
4578
|
}
|
|
4469
4579
|
|
|
4470
|
-
private async checkAgentEndState(
|
|
4471
|
-
turn: ActiveTurn,
|
|
4472
|
-
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
4473
|
-
): Promise<void> {
|
|
4474
|
-
// Evidence arriving during this check cannot authorize an older terminal frame.
|
|
4475
|
-
const ownershipObserved =
|
|
4476
|
-
turn.terminalOwnershipEvidence || turn.bufferedTerminalOwnershipEvidence;
|
|
4580
|
+
private async checkAgentEndState(turn: ActiveTurn, candidate: TerminalCandidate): Promise<void> {
|
|
4477
4581
|
await Promise.allSettled(turn.userLookups);
|
|
4478
4582
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4479
4583
|
while (turn.userEchoes.length > 0) {
|
|
@@ -4482,40 +4586,42 @@ export class OmpProviderSession {
|
|
|
4482
4586
|
await Promise.allSettled(turn.userLookups);
|
|
4483
4587
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4484
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
|
+
}
|
|
4485
4599
|
if (turn.interrupted) {
|
|
4486
|
-
await this.completeAgentEnd(turn, event);
|
|
4600
|
+
await this.completeAgentEnd(turn, candidate.event);
|
|
4487
4601
|
return;
|
|
4488
4602
|
}
|
|
4489
4603
|
const state = await this.boundedTerminalState(turn, FINAL_USAGE_WAIT_MS);
|
|
4490
4604
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
4491
4605
|
if (!turn.interrupted && this.subsessions?.hasActiveChildren()) {
|
|
4492
|
-
turn
|
|
4493
|
-
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;
|
|
4494
4618
|
}
|
|
4495
4619
|
if (state) {
|
|
4496
4620
|
if (state.isStreaming || state.isCompacting) {
|
|
4497
|
-
|
|
4498
|
-
const message = "OMP agent_end arrived while the native runtime remained active";
|
|
4499
|
-
this.invalidateRuntime(message);
|
|
4500
|
-
await this.finishTurn(turn, "failed", { message }, true, true);
|
|
4501
|
-
return;
|
|
4502
|
-
}
|
|
4503
|
-
turn.agentEndPending = false;
|
|
4504
|
-
turn.terminalizing = false;
|
|
4505
|
-
turn.deferredAgentEnd = undefined;
|
|
4506
|
-
return;
|
|
4507
|
-
}
|
|
4508
|
-
if (
|
|
4509
|
-
turn.terminalOwnershipRequired &&
|
|
4510
|
-
(!ownershipObserved || !turn.terminalOwnershipEvidence)
|
|
4511
|
-
) {
|
|
4512
|
-
turn.agentEndPending = false;
|
|
4513
|
-
turn.terminalizing = false;
|
|
4514
|
-
turn.deferredAgentEnd = undefined;
|
|
4515
|
-
this.scheduleTerminalOwnershipTimeout(turn);
|
|
4621
|
+
this.ignoreActiveTerminalCandidate(turn, candidate);
|
|
4516
4622
|
return;
|
|
4517
4623
|
}
|
|
4518
|
-
await this.completeAgentEnd(turn, event, true);
|
|
4624
|
+
await this.completeAgentEnd(turn, candidate.event, true);
|
|
4519
4625
|
return;
|
|
4520
4626
|
}
|
|
4521
4627
|
if (turn.userEchoObserved) {
|
|
@@ -4525,7 +4631,7 @@ export class OmpProviderSession {
|
|
|
4525
4631
|
if (turn.agentEndRetryTimer === undefined) {
|
|
4526
4632
|
turn.agentEndRetryTimer = this.scheduler.set(() => {
|
|
4527
4633
|
turn.agentEndRetryTimer = undefined;
|
|
4528
|
-
this.finishFromAgentEnd(turn,
|
|
4634
|
+
this.finishFromAgentEnd(turn, candidate);
|
|
4529
4635
|
}, USAGE_POLL_MS);
|
|
4530
4636
|
}
|
|
4531
4637
|
}
|
|
@@ -4556,8 +4662,11 @@ export class OmpProviderSession {
|
|
|
4556
4662
|
)
|
|
4557
4663
|
return;
|
|
4558
4664
|
if (!turn.interrupted && state && (state.isStreaming || state.isCompacting)) {
|
|
4559
|
-
turn
|
|
4560
|
-
|
|
4665
|
+
this.ignoreActiveTerminalCandidate(turn, {
|
|
4666
|
+
event,
|
|
4667
|
+
arrivalSequence: turn.activitySequence,
|
|
4668
|
+
confidence: event.requestId === undefined ? "ordered-legacy" : "keyed",
|
|
4669
|
+
});
|
|
4561
4670
|
return;
|
|
4562
4671
|
}
|
|
4563
4672
|
if (state && !state.isStreaming && !state.isCompacting) {
|
|
@@ -4623,6 +4732,7 @@ export class OmpProviderSession {
|
|
|
4623
4732
|
turn.starting = false;
|
|
4624
4733
|
turn.terminal = true;
|
|
4625
4734
|
this.stopUsagePoll(turn);
|
|
4735
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4626
4736
|
this.resolveTurnPermissions(turn.turnId);
|
|
4627
4737
|
this.projector.finishTurn(turn.turnId);
|
|
4628
4738
|
if (this.activeTurn === turn) this.activeTurn = null;
|
|
@@ -4677,7 +4787,7 @@ export class OmpProviderSession {
|
|
|
4677
4787
|
turn.manualCompactionPending = false;
|
|
4678
4788
|
turn.agentEndPending = false;
|
|
4679
4789
|
this.cancelLocalOnlyCompletion(turn);
|
|
4680
|
-
this.
|
|
4790
|
+
this.cancelAmbiguousTerminal(turn);
|
|
4681
4791
|
this.stopUsagePoll(turn);
|
|
4682
4792
|
if (turn.manualCompactionDeadlineTimer !== undefined) {
|
|
4683
4793
|
this.scheduler.clear(turn.manualCompactionDeadlineTimer);
|