@omercnet/paseo-omp 5.1.1 → 5.2.0-next.362.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/OVERVIEW.md +1 -0
- package/README.md +2 -2
- package/client/provider-image.tsx +127 -14
- package/client/provider-launch-settings.tsx +46 -2
- package/client/subagents-state.ts +108 -0
- package/client/subagents-strip.tsx +391 -0
- package/index.client.tsx +9 -0
- package/index.server.ts +25 -0
- package/npm-shrinkwrap.json +9 -9
- package/package.json +6 -9
- package/scripts/prepare-dependencies.mjs +7 -10
- package/server/mcp-browser.ts +3 -1
- package/server/package-version.ts +1 -1
- package/server/provider/connection.ts +43 -8
- package/server/provider/omp-rpc-process.ts +9 -9
- package/server/provider/omp-rpc-protocol.ts +40 -2
- package/server/provider/omp-rpc-transport.ts +5 -2
- package/server/provider/omp-rpc.ts +25 -0
- package/server/provider/registration.ts +18 -0
- package/server/provider/session-compaction.ts +29 -25
- package/server/provider/session-descriptors.ts +36 -30
- package/server/provider/session-events.ts +1 -0
- package/server/provider/session-history.ts +2 -0
- package/server/provider/session-limit.ts +32 -0
- package/server/provider/session-terminal.ts +52 -1
- package/server/provider/session.ts +255 -67
- package/server/provider/subsessions.ts +109 -7
- package/server/provider/timeline-projector.ts +41 -26
- package/server/subagents.ts +97 -0
- package/shared/provider-launch-settings.ts +4 -0
- package/shared/subagents.ts +60 -0
|
@@ -6,10 +6,12 @@ import type {
|
|
|
6
6
|
ProviderInput,
|
|
7
7
|
ProviderSessionConfig,
|
|
8
8
|
} from "@getpaseo/plugin/server/provider";
|
|
9
|
+
import { isLiveOmpSubagentStatus } from "../../shared/subagents";
|
|
9
10
|
import type {
|
|
10
11
|
OmpOperationalFailure,
|
|
11
12
|
OmpOperationalFailureReporter,
|
|
12
13
|
} from "../operational-failure-diagnostics";
|
|
14
|
+
import { type OmpSubagentControls, OmpSubagentNotRunningError } from "../subagents";
|
|
13
15
|
import {
|
|
14
16
|
mapOmpModels,
|
|
15
17
|
nativeOmpModelId,
|
|
@@ -73,15 +75,18 @@ import {
|
|
|
73
75
|
assistantTerminalOutcome,
|
|
74
76
|
classifyTerminalCandidate,
|
|
75
77
|
createActiveTurn,
|
|
78
|
+
createNativeRunTurn,
|
|
79
|
+
type HeldAgentEnd,
|
|
76
80
|
historyTerminalOutcome,
|
|
77
81
|
nativeEntryId,
|
|
78
82
|
type PendingUser,
|
|
79
83
|
type TerminalCandidate,
|
|
84
|
+
takeRunEvidence,
|
|
80
85
|
terminalOutcome,
|
|
81
86
|
unknownTerminalOutcomeError,
|
|
82
87
|
} from "./session-terminal";
|
|
83
88
|
import { OmpSessionUsage } from "./session-usage";
|
|
84
|
-
import { OmpSubsessionProjector } from "./subsessions";
|
|
89
|
+
import { OmpSubsessionProjector, ompSubagentHandle } from "./subsessions";
|
|
85
90
|
import {
|
|
86
91
|
defaultOmpTimelineScheduler,
|
|
87
92
|
OmpTimelineProjector,
|
|
@@ -223,8 +228,9 @@ export async function authorizeNativeSession(
|
|
|
223
228
|
sessionId: string,
|
|
224
229
|
cwd: string,
|
|
225
230
|
sessionDir?: string,
|
|
231
|
+
signal?: AbortSignal,
|
|
226
232
|
): Promise<OmpSessionDescriptor> {
|
|
227
|
-
const matches = await runtime.listSessions({ sessionId, cwd, limit: 2, sessionDir });
|
|
233
|
+
const matches = await runtime.listSessions({ sessionId, cwd, limit: 2, sessionDir, signal });
|
|
228
234
|
const descriptor = matches[0];
|
|
229
235
|
if (matches.length !== 1 || descriptor?.id !== sessionId) {
|
|
230
236
|
throw new OmpPublicError("OMP session could not be resolved in this workspace");
|
|
@@ -295,7 +301,7 @@ async function settleSessionCleanup(promises: readonly Promise<void>[]): Promise
|
|
|
295
301
|
throw new AggregateError(failures, "OMP session initialization cleanup failed");
|
|
296
302
|
}
|
|
297
303
|
}
|
|
298
|
-
export class OmpProviderSession implements OmpOpenedSession {
|
|
304
|
+
export class OmpProviderSession implements OmpOpenedSession, OmpSubagentControls {
|
|
299
305
|
readonly id: string;
|
|
300
306
|
readonly cwd: string;
|
|
301
307
|
|
|
@@ -330,6 +336,8 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
330
336
|
private configRevision = 0;
|
|
331
337
|
private recoveryUsesNativeConfig = false;
|
|
332
338
|
private activeAbort: PendingAbort | null = null;
|
|
339
|
+
/** Frames of a run that started while the previous terminal was being confirmed or published. */
|
|
340
|
+
private runStartBacklog: OmpRpcEvent[] | null = null;
|
|
333
341
|
private readonly usage: OmpSessionUsage;
|
|
334
342
|
private readonly compaction: OmpSessionCompaction;
|
|
335
343
|
private revertInFlight = false;
|
|
@@ -460,6 +468,36 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
460
468
|
this.projector.setBrowserAuthorizationIssuer(issue);
|
|
461
469
|
}
|
|
462
470
|
|
|
471
|
+
async cancelSubagent(handle: string): Promise<boolean> {
|
|
472
|
+
const target = await this.liveSubagent(handle);
|
|
473
|
+
return target ? await target.runtime.cancelSubagent(target.nativeId) : false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async steerSubagent(handle: string, message: string): Promise<void> {
|
|
477
|
+
const target = await this.liveSubagent(handle);
|
|
478
|
+
if (!target) throw new OmpSubagentNotRunningError();
|
|
479
|
+
await target.runtime.steerSubagent(target.nativeId, message);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The native ID and runtime of a subagent this session's current runtime lists as running, or
|
|
484
|
+
* `null`. OMP repeats the check; this scopes the request to this session and answers exactly.
|
|
485
|
+
*/
|
|
486
|
+
private async liveSubagent(
|
|
487
|
+
handle: string,
|
|
488
|
+
): Promise<{ runtime: OmpRuntimeSession; nativeId: string } | null> {
|
|
489
|
+
if (this.closed || this.runtimeDead) return null;
|
|
490
|
+
const runtime = this.runtime;
|
|
491
|
+
const snapshots = await runtime.getSubagents();
|
|
492
|
+
if (runtime !== this.runtime || this.closed || this.runtimeDead) return null;
|
|
493
|
+
const live = snapshots.find(
|
|
494
|
+
(snapshot) =>
|
|
495
|
+
ompSubagentHandle(this.id, snapshot.id) === handle &&
|
|
496
|
+
isLiveOmpSubagentStatus(snapshot.status),
|
|
497
|
+
);
|
|
498
|
+
return live ? { runtime, nativeId: live.id } : null;
|
|
499
|
+
}
|
|
500
|
+
|
|
463
501
|
private readonly imageMaterializer = new OmpImageMaterializer();
|
|
464
502
|
static async open(
|
|
465
503
|
input: SessionOpenInput,
|
|
@@ -505,6 +543,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
505
543
|
resumeSessionId,
|
|
506
544
|
input.config.cwd,
|
|
507
545
|
normalizedConfig.sessionDir,
|
|
546
|
+
signal,
|
|
508
547
|
)
|
|
509
548
|
: undefined;
|
|
510
549
|
validateOmpHostToolConfig(effectiveConfig);
|
|
@@ -1458,33 +1497,46 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
1458
1497
|
} catch (error) {
|
|
1459
1498
|
if (this.closed || this.runtimeDead || turn.generation !== this.generation) return;
|
|
1460
1499
|
turn.manualCompactionPending = false;
|
|
1500
|
+
// OMP 18.13.3+ reports the outcome in manual_compaction_end before rejecting `compact`.
|
|
1501
|
+
const outcome = turn.manualCompactionOutcome;
|
|
1502
|
+
if (outcome === "skipped") {
|
|
1503
|
+
await this.finishTurn(turn, turn.interrupted ? "canceled" : "completed");
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1461
1506
|
const raw = providerError(error, "OMP compaction failed");
|
|
1462
1507
|
const failure = { message: this.dataFilter.text(raw.message, 4_096) };
|
|
1463
|
-
const state = turn.interrupted ? "canceled" : "failed";
|
|
1508
|
+
const state = turn.interrupted || outcome === "canceled" ? "canceled" : "failed";
|
|
1464
1509
|
this.compaction.finish(state, { message: failure.message });
|
|
1465
1510
|
await this.finishTurn(turn, state, state === "failed" ? failure : undefined, true, true);
|
|
1466
1511
|
}
|
|
1467
1512
|
}
|
|
1468
1513
|
|
|
1469
1514
|
async canStopRuntime(): Promise<boolean> {
|
|
1470
|
-
|
|
1515
|
+
if (this.hasLocalRuntimeWork()) return false;
|
|
1516
|
+
const state = await this.runtime.getState();
|
|
1517
|
+
// OMP can start a run of its own while the state lookup is pending.
|
|
1518
|
+
if (this.hasLocalRuntimeWork()) return false;
|
|
1519
|
+
// OMP builds that predate these fields report neither, which keeps the runtime alive.
|
|
1520
|
+
return (
|
|
1521
|
+
state.isSettled === true &&
|
|
1522
|
+
state.hasPendingAsyncWork === false &&
|
|
1523
|
+
!state.isStreaming &&
|
|
1524
|
+
!state.isCompacting
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
private hasLocalRuntimeWork(): boolean {
|
|
1529
|
+
return (
|
|
1471
1530
|
this.closed ||
|
|
1472
1531
|
this.runtimeDead !== null ||
|
|
1473
1532
|
this.recoveryPromise !== null ||
|
|
1474
1533
|
this.activeTurn !== null ||
|
|
1534
|
+
this.runStartBacklog !== null ||
|
|
1475
1535
|
this.activeAbort !== null ||
|
|
1476
1536
|
this.revertInFlight ||
|
|
1477
1537
|
this.configMutationInFlight ||
|
|
1478
1538
|
this.permissions.hasPending() ||
|
|
1479
|
-
Boolean(this.subsessions?.hasActiveChildren())
|
|
1480
|
-
if (hasLocalWork) return false;
|
|
1481
|
-
const state = await this.runtime.getState();
|
|
1482
|
-
// OMP builds that predate these fields report neither, which keeps the runtime alive.
|
|
1483
|
-
return (
|
|
1484
|
-
state.isSettled === true &&
|
|
1485
|
-
state.hasPendingAsyncWork === false &&
|
|
1486
|
-
!state.isStreaming &&
|
|
1487
|
-
!state.isCompacting
|
|
1539
|
+
Boolean(this.subsessions?.hasActiveChildren())
|
|
1488
1540
|
);
|
|
1489
1541
|
}
|
|
1490
1542
|
|
|
@@ -2259,6 +2311,17 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2259
2311
|
|
|
2260
2312
|
private handleRuntimeEvent(event: OmpRpcEvent): void {
|
|
2261
2313
|
if (this.closed) return;
|
|
2314
|
+
if (this.runStartBacklog) {
|
|
2315
|
+
this.bufferEvent(this.runStartBacklog, event);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
// A run that starts while the previous terminal is being confirmed or published waits for it,
|
|
2319
|
+
// so neither the old turn's outcome nor the new run's frames are lost.
|
|
2320
|
+
const pendingTerminal = this.activeTurn?.terminalization ?? this.activeTurn?.agentEndCheck;
|
|
2321
|
+
if (event.type === "agent_start" && pendingTerminal) {
|
|
2322
|
+
this.deferRunStart(event, pendingTerminal);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2262
2325
|
if (
|
|
2263
2326
|
event.type === "subagent_lifecycle" ||
|
|
2264
2327
|
event.type === "subagent_progress" ||
|
|
@@ -2336,7 +2399,8 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2336
2399
|
event.type === "ttsr_triggered" ||
|
|
2337
2400
|
event.type === "cache_warming_start" ||
|
|
2338
2401
|
event.type === "cache_warming_end" ||
|
|
2339
|
-
event.type === "queue_update"
|
|
2402
|
+
event.type === "queue_update" ||
|
|
2403
|
+
event.type === "session_info_update"
|
|
2340
2404
|
) {
|
|
2341
2405
|
return;
|
|
2342
2406
|
}
|
|
@@ -2353,7 +2417,8 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2353
2417
|
event.type === "auto_retry_end" ||
|
|
2354
2418
|
event.type === "compaction_start" ||
|
|
2355
2419
|
event.type === "compaction_end" ||
|
|
2356
|
-
event.type === "advisor_yielded"
|
|
2420
|
+
event.type === "advisor_yielded" ||
|
|
2421
|
+
event.type === "extension_error"
|
|
2357
2422
|
) {
|
|
2358
2423
|
this.projector.projectPassive(event);
|
|
2359
2424
|
return;
|
|
@@ -2367,13 +2432,23 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2367
2432
|
this.scheduleCommittedConfigRefresh();
|
|
2368
2433
|
return;
|
|
2369
2434
|
}
|
|
2370
|
-
if (
|
|
2435
|
+
if (
|
|
2436
|
+
event.type === "model_changed" ||
|
|
2437
|
+
event.type === "thinking_level_changed" ||
|
|
2438
|
+
event.type === "config_update"
|
|
2439
|
+
) {
|
|
2371
2440
|
this.scheduleCommittedConfigRefresh();
|
|
2372
2441
|
return;
|
|
2373
2442
|
}
|
|
2374
|
-
const turn =
|
|
2443
|
+
const turn =
|
|
2444
|
+
this.activeTurn ?? (event.type === "agent_start" ? this.openNativeRunTurn() : null);
|
|
2375
2445
|
if (!turn) {
|
|
2376
|
-
if (
|
|
2446
|
+
if (
|
|
2447
|
+
event.type === "auto_compaction_start" ||
|
|
2448
|
+
event.type === "auto_compaction_end" ||
|
|
2449
|
+
event.type === "manual_compaction_start" ||
|
|
2450
|
+
event.type === "manual_compaction_end"
|
|
2451
|
+
) {
|
|
2377
2452
|
this.projector.projectPassive(event);
|
|
2378
2453
|
}
|
|
2379
2454
|
return;
|
|
@@ -2386,27 +2461,69 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2386
2461
|
return;
|
|
2387
2462
|
}
|
|
2388
2463
|
if (turn.starting) {
|
|
2389
|
-
|
|
2390
|
-
turn.bufferedEvents.length >= MAX_BUFFERED_TURN_EVENTS ||
|
|
2391
|
-
retainedBytes(turn.bufferedEvents, MAX_BUFFERED_TURN_BYTES) +
|
|
2392
|
-
boundedJsonBytes(
|
|
2393
|
-
event,
|
|
2394
|
-
MAX_BUFFERED_TURN_BYTES,
|
|
2395
|
-
MAX_BUFFERED_VALUE_ITEMS,
|
|
2396
|
-
MAX_BUFFERED_TURN_BYTES,
|
|
2397
|
-
MAX_BUFFERED_VALUE_NODES,
|
|
2398
|
-
) >
|
|
2399
|
-
MAX_BUFFERED_TURN_BYTES
|
|
2400
|
-
) {
|
|
2401
|
-
this.handleRuntimeFailure();
|
|
2402
|
-
return;
|
|
2403
|
-
}
|
|
2404
|
-
turn.bufferedEvents.push(event);
|
|
2464
|
+
this.bufferEvent(turn.bufferedEvents, event);
|
|
2405
2465
|
return;
|
|
2406
2466
|
}
|
|
2407
2467
|
this.handleTurnEvent(turn, event);
|
|
2408
2468
|
}
|
|
2409
2469
|
|
|
2470
|
+
private bufferEvent(buffer: OmpRpcEvent[], event: OmpRpcEvent): void {
|
|
2471
|
+
if (
|
|
2472
|
+
buffer.length >= MAX_BUFFERED_TURN_EVENTS ||
|
|
2473
|
+
retainedBytes(buffer, MAX_BUFFERED_TURN_BYTES) +
|
|
2474
|
+
boundedJsonBytes(
|
|
2475
|
+
event,
|
|
2476
|
+
MAX_BUFFERED_TURN_BYTES,
|
|
2477
|
+
MAX_BUFFERED_VALUE_ITEMS,
|
|
2478
|
+
MAX_BUFFERED_TURN_BYTES,
|
|
2479
|
+
MAX_BUFFERED_VALUE_NODES,
|
|
2480
|
+
) >
|
|
2481
|
+
MAX_BUFFERED_TURN_BYTES
|
|
2482
|
+
) {
|
|
2483
|
+
this.handleRuntimeFailure();
|
|
2484
|
+
return;
|
|
2485
|
+
}
|
|
2486
|
+
buffer.push(event);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
private deferRunStart(event: OmpRpcEvent, pendingTerminal: Promise<void>): void {
|
|
2490
|
+
const backlog = [event];
|
|
2491
|
+
this.runStartBacklog = backlog;
|
|
2492
|
+
const replay = () => {
|
|
2493
|
+
if (this.runStartBacklog !== backlog) return;
|
|
2494
|
+
this.runStartBacklog = null;
|
|
2495
|
+
for (const buffered of backlog) this.handleRuntimeEvent(buffered);
|
|
2496
|
+
};
|
|
2497
|
+
void pendingTerminal.then(replay, replay);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
private scheduleHeldTurnCheck(turn: ActiveTurn, held: HeldAgentEnd): void {
|
|
2501
|
+
this.scheduler.set(() => this.checkHeldTurn(turn, held), USAGE_POLL_MS);
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
/** Released OMP does not always write `session_settled` once a held run's async work is gone. */
|
|
2505
|
+
private async checkHeldTurn(turn: ActiveTurn, held: HeldAgentEnd): Promise<void> {
|
|
2506
|
+
if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
|
|
2507
|
+
const state = await this.usage.boundedSnapshot(turn, FINAL_USAGE_WAIT_MS);
|
|
2508
|
+
if (turn.heldAgentEnd !== held || this.activeTurn !== turn) return;
|
|
2509
|
+
const settled =
|
|
2510
|
+
state?.isSettled === true &&
|
|
2511
|
+
state.hasPendingAsyncWork === false &&
|
|
2512
|
+
!state.isStreaming &&
|
|
2513
|
+
!state.isCompacting;
|
|
2514
|
+
if (settled) this.handleTurnEvent(turn, { type: "session_settled" });
|
|
2515
|
+
else this.scheduleHeldTurnCheck(turn, held);
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
private openNativeRunTurn(): ActiveTurn {
|
|
2519
|
+
const turn = createNativeRunTurn(this.generation);
|
|
2520
|
+
this.activeTurn = turn;
|
|
2521
|
+
turn.acknowledged = true;
|
|
2522
|
+
this.startTurn(turn);
|
|
2523
|
+
this.projector.acceptLiveTurn(turn.turnId);
|
|
2524
|
+
return turn;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2410
2527
|
private handleTurnEvent(
|
|
2411
2528
|
turn: ActiveTurn,
|
|
2412
2529
|
event: OmpRpcEvent,
|
|
@@ -2476,10 +2593,27 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2476
2593
|
this.compaction.start(turn, "auto", event.action);
|
|
2477
2594
|
return;
|
|
2478
2595
|
}
|
|
2479
|
-
if (event.type === "
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2596
|
+
if (event.type === "manual_compaction_start") {
|
|
2597
|
+
turn.nativeActivity = true;
|
|
2598
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
2599
|
+
// A no-op when this turn's own `compact` call already opened the operation.
|
|
2600
|
+
this.compaction.start(turn, "manual");
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
2603
|
+
if (event.type === "auto_compaction_end" || event.type === "manual_compaction_end") {
|
|
2604
|
+
const outcome = this.compaction.handleEnd(turn, event);
|
|
2605
|
+
if (outcome && turn.manualCompaction) turn.manualCompactionOutcome = outcome;
|
|
2606
|
+
if (outcome) this.usage.schedulePoll(turn, USAGE_REFRESH_MS);
|
|
2607
|
+
return;
|
|
2608
|
+
}
|
|
2609
|
+
if (event.type === "session_settled") {
|
|
2610
|
+
// OMP is quiet, so the held end closes this stretch; a later run opens its own turn.
|
|
2611
|
+
const held = turn.heldAgentEnd;
|
|
2612
|
+
if (!held) return;
|
|
2613
|
+
turn.heldAgentEnd = undefined;
|
|
2614
|
+
Object.assign(turn, held.evidence);
|
|
2615
|
+
// Ownership is judged as of the held end's arrival, never from evidence that came later.
|
|
2616
|
+
this.acceptTerminal(turn, { ...held.event, isTerminal: true }, held.arrivalSequence);
|
|
2483
2617
|
return;
|
|
2484
2618
|
}
|
|
2485
2619
|
if (event.type === "agent_end" && turn.manualCompactionPending) return;
|
|
@@ -2502,35 +2636,30 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2502
2636
|
return;
|
|
2503
2637
|
}
|
|
2504
2638
|
}
|
|
2639
|
+
if (event.type === "agent_start") {
|
|
2640
|
+
turn.heldAgentEnd = undefined;
|
|
2641
|
+
// Evidence is per run; records OMP writes between runs belong to neither.
|
|
2642
|
+
takeRunEvidence(turn);
|
|
2643
|
+
if (turn.agentEndPending && !turn.interrupted) {
|
|
2644
|
+
// A terminal waiting to retry its idle check is stale once another run starts.
|
|
2645
|
+
this.resetAgentEndProbe(turn);
|
|
2646
|
+
turn.deferredAgentEnd = undefined;
|
|
2647
|
+
this.usage.poll(turn);
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2505
2650
|
if (event.type === "agent_end") {
|
|
2506
2651
|
if (event.isTerminal === false) {
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
const candidate: TerminalCandidate = turn.interrupted
|
|
2515
|
-
? { event, confidence: "interrupted", arrivalSequence: turn.activitySequence }
|
|
2516
|
-
: classifyTerminalCandidate(turn, event);
|
|
2517
|
-
if (candidate.confidence === "ambiguous") {
|
|
2518
|
-
this.deferAmbiguousTerminal(turn, candidate);
|
|
2519
|
-
return;
|
|
2520
|
-
}
|
|
2521
|
-
turn.nativeActivity = true;
|
|
2522
|
-
this.cancelLocalOnlyCompletion(turn);
|
|
2523
|
-
if (
|
|
2524
|
-
(!turn.interrupted &&
|
|
2525
|
-
candidate.confidence !== "keyed" &&
|
|
2526
|
-
this.hasTerminalConflict(turn, false)) ||
|
|
2527
|
-
turn.steersInFlight > 0 ||
|
|
2528
|
-
turn.terminalizing
|
|
2529
|
-
) {
|
|
2530
|
-
turn.deferredAgentEnd = candidate;
|
|
2652
|
+
const held = {
|
|
2653
|
+
event,
|
|
2654
|
+
arrivalSequence: turn.activitySequence,
|
|
2655
|
+
evidence: takeRunEvidence(turn),
|
|
2656
|
+
};
|
|
2657
|
+
turn.heldAgentEnd = held;
|
|
2658
|
+
this.scheduleHeldTurnCheck(turn, held);
|
|
2531
2659
|
return;
|
|
2532
2660
|
}
|
|
2533
|
-
|
|
2661
|
+
turn.heldAgentEnd = undefined;
|
|
2662
|
+
this.acceptTerminal(turn, event, turn.activitySequence);
|
|
2534
2663
|
return;
|
|
2535
2664
|
}
|
|
2536
2665
|
if (isNativeTurnActivity(event)) {
|
|
@@ -2563,9 +2692,39 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
2563
2692
|
});
|
|
2564
2693
|
throw error;
|
|
2565
2694
|
}
|
|
2695
|
+
if (event.type === "tool_execution_start" && event.toolName === "task") {
|
|
2696
|
+
this.subsessions?.anchorRootTask(event.toolCallId);
|
|
2697
|
+
}
|
|
2566
2698
|
if (event.type === "tool_execution_end") this.resumeDeferredAgentEnd();
|
|
2567
2699
|
}
|
|
2568
2700
|
|
|
2701
|
+
private acceptTerminal(
|
|
2702
|
+
turn: ActiveTurn,
|
|
2703
|
+
event: Extract<OmpRpcEvent, { type: "agent_end" }>,
|
|
2704
|
+
arrivalSequence: number,
|
|
2705
|
+
): void {
|
|
2706
|
+
const candidate: TerminalCandidate = turn.interrupted
|
|
2707
|
+
? { event, confidence: "interrupted", arrivalSequence }
|
|
2708
|
+
: classifyTerminalCandidate(turn, event, arrivalSequence);
|
|
2709
|
+
if (candidate.confidence === "ambiguous") {
|
|
2710
|
+
this.deferAmbiguousTerminal(turn, candidate);
|
|
2711
|
+
return;
|
|
2712
|
+
}
|
|
2713
|
+
turn.nativeActivity = true;
|
|
2714
|
+
this.cancelLocalOnlyCompletion(turn);
|
|
2715
|
+
if (
|
|
2716
|
+
(!turn.interrupted &&
|
|
2717
|
+
candidate.confidence !== "keyed" &&
|
|
2718
|
+
this.hasTerminalConflict(turn, false)) ||
|
|
2719
|
+
turn.steersInFlight > 0 ||
|
|
2720
|
+
turn.terminalizing
|
|
2721
|
+
) {
|
|
2722
|
+
turn.deferredAgentEnd = candidate;
|
|
2723
|
+
return;
|
|
2724
|
+
}
|
|
2725
|
+
this.beginTerminalization(turn, candidate);
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2569
2728
|
private projectUserEcho(turn: ActiveTurn, message: OmpMessage): void {
|
|
2570
2729
|
turn.userEchoObserved = true;
|
|
2571
2730
|
const entryId = nativeEntryId(message);
|
|
@@ -3033,6 +3192,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3033
3192
|
if (!turn.agentEndPending || turn.terminal || this.activeTurn !== turn) return;
|
|
3034
3193
|
if (
|
|
3035
3194
|
candidate.confidence === "keyed" ||
|
|
3195
|
+
candidate.confidence === "native-run" ||
|
|
3036
3196
|
(candidate.confidence === "initial-turn" && !turn.userEchoObserved)
|
|
3037
3197
|
) {
|
|
3038
3198
|
void this.completeAgentEnd(turn, candidate.event);
|
|
@@ -3146,14 +3306,16 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3146
3306
|
return;
|
|
3147
3307
|
}
|
|
3148
3308
|
if (state) {
|
|
3149
|
-
|
|
3309
|
+
// A run start buffered behind this check proves this end was the last one before it, so a
|
|
3310
|
+
// streaming state belongs to that new run, not to this turn.
|
|
3311
|
+
if ((state.isStreaming || state.isCompacting) && !this.runStartBacklog) {
|
|
3150
3312
|
this.ignoreActiveTerminalCandidate(turn, candidate);
|
|
3151
3313
|
return;
|
|
3152
3314
|
}
|
|
3153
3315
|
await this.completeAgentEnd(turn, candidate.event, true);
|
|
3154
3316
|
return;
|
|
3155
3317
|
}
|
|
3156
|
-
if (turn.userEchoObserved) {
|
|
3318
|
+
if (turn.userEchoObserved && candidate.confidence !== "native-run") {
|
|
3157
3319
|
await this.cancelIndeterminateTerminal(
|
|
3158
3320
|
turn,
|
|
3159
3321
|
"OMP agent_end state could not be confirmed",
|
|
@@ -3389,6 +3551,7 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3389
3551
|
throw error;
|
|
3390
3552
|
}
|
|
3391
3553
|
this.unclaimedBranchEntries.length = 0;
|
|
3554
|
+
turn.terminalEmitAttempted = true;
|
|
3392
3555
|
this.emit({
|
|
3393
3556
|
type: "session.turn",
|
|
3394
3557
|
sessionId: this.id,
|
|
@@ -3398,17 +3561,42 @@ export class OmpProviderSession implements OmpOpenedSession {
|
|
|
3398
3561
|
});
|
|
3399
3562
|
this.runtimeTurnCompleted = true;
|
|
3400
3563
|
if (this.activeTurn === turn) this.activeTurn = null;
|
|
3401
|
-
})();
|
|
3564
|
+
})().catch(() => this.failTerminalization(turn));
|
|
3402
3565
|
turn.terminalization = terminalization;
|
|
3403
3566
|
return terminalization;
|
|
3404
3567
|
}
|
|
3405
3568
|
|
|
3569
|
+
/** A turn whose terminal state could not be published must not keep owning the session. */
|
|
3570
|
+
private failTerminalization(turn: ActiveTurn): void {
|
|
3571
|
+
const message = "OMP turn could not be finalized";
|
|
3572
|
+
turn.terminal = true;
|
|
3573
|
+
if (this.activeTurn === turn) this.activeTurn = null;
|
|
3574
|
+
this.recordOperationalFailure({ category: "terminal-outcome", stage: "failed" });
|
|
3575
|
+
// Once the terminal emit was attempted, earlier listeners may already hold it; a second,
|
|
3576
|
+
// conflicting terminal would be worse than failing the runtime alone.
|
|
3577
|
+
if (!turn.terminalEmitAttempted) {
|
|
3578
|
+
try {
|
|
3579
|
+
this.emit({
|
|
3580
|
+
type: "session.turn",
|
|
3581
|
+
sessionId: this.id,
|
|
3582
|
+
turnId: turn.turnId,
|
|
3583
|
+
state: "failed",
|
|
3584
|
+
error: { message },
|
|
3585
|
+
});
|
|
3586
|
+
} catch {
|
|
3587
|
+
// The listener that failed may fail again; the runtime failure below still applies.
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
this.handleRuntimeFailure(message);
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3406
3593
|
private invalidateRuntime(
|
|
3407
3594
|
message: string,
|
|
3408
3595
|
compactionState: "failed" | "canceled" = "failed",
|
|
3409
3596
|
): void {
|
|
3410
3597
|
this.imageMaterializer.clear();
|
|
3411
3598
|
if (this.closed || this.runtimeDead) return;
|
|
3599
|
+
this.runStartBacklog = null;
|
|
3412
3600
|
this.compaction.resetDiscardedEnds();
|
|
3413
3601
|
this.permissions.resolveAllPermissions();
|
|
3414
3602
|
this.recoveryUsesNativeConfig ||=
|