@ours.network/fleet 1.2.0-nightly.63 → 1.2.0-nightly.64
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 +3 -0
- package/dist/application/conversation-history.js +4 -1
- package/dist/application/conversation-role-observer.d.ts +62 -0
- package/dist/application/conversation-role-observer.js +206 -0
- package/dist/application/session-control.d.ts +31 -17
- package/dist/application/session-control.js +148 -33
- package/dist/build-info.json +4 -4
- package/dist/session/acp.d.ts +8 -1
- package/dist/session/acp.js +97 -43
- package/dist/session/arbiter.d.ts +4 -0
- package/dist/session/arbiter.js +18 -0
- package/dist/session/codex-app-server.d.ts +7 -1
- package/dist/session/codex-app-server.js +60 -9
- package/dist/session/control.d.ts +5 -2
- package/dist/session/control.js +145 -20
- package/dist/session/conversation-observation.d.ts +25 -0
- package/dist/session/conversation-observation.js +100 -0
- package/dist/session/conversation-replay.d.ts +50 -0
- package/dist/session/conversation-replay.js +203 -0
- package/dist/session/conversation-store.d.ts +14 -0
- package/dist/session/conversation-store.js +201 -17
- package/dist/session/conversation-types.d.ts +29 -1
- package/dist/session/types.d.ts +6 -0
- package/dist/web/auth.d.ts +2 -0
- package/dist/web/auth.js +10 -1
- package/dist/web/conversation-stream-codec.d.ts +23 -0
- package/dist/web/conversation-stream-codec.js +110 -0
- package/dist/web/conversation-stream.d.ts +25 -0
- package/dist/web/conversation-stream.js +231 -0
- package/dist/web/runtime.js +12 -2
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +2 -0
- package/docs/conversation-stream.md +176 -0
- package/package.json +2 -1
package/dist/session/acp.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as acp from '@agentclientprotocol/sdk';
|
|
|
4
4
|
import type { CommonPermissions } from '../config.js';
|
|
5
5
|
import type { AcpMcpServer } from '../harness/types.js';
|
|
6
6
|
import { ConversationEventStore } from './conversation-store.js';
|
|
7
|
-
import type { ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
7
|
+
import type { ConversationObservationState, ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
8
8
|
import { type PendingPrompt } from './prompt-batch.js';
|
|
9
9
|
import type { ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent, AgentSession, RuntimeSelectorMetadata, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnOutcome, TurnResult } from './types.js';
|
|
10
10
|
/** Bound safe-boundary waiting without turning a hung tool into cancellation. */
|
|
@@ -157,7 +157,9 @@ export declare class AcpSession implements AgentSession {
|
|
|
157
157
|
private readonly sharedTurns;
|
|
158
158
|
/** Commands the adapter currently advertises; only these are control operations. */
|
|
159
159
|
private advertisedCommands;
|
|
160
|
+
private presentedCommands;
|
|
160
161
|
private queueDepth;
|
|
162
|
+
private readonly observation;
|
|
161
163
|
private exit;
|
|
162
164
|
private steeringSupported;
|
|
163
165
|
private agentCapabilities?;
|
|
@@ -396,6 +398,11 @@ export declare class AcpSession implements AgentSession {
|
|
|
396
398
|
after?: string;
|
|
397
399
|
limit?: number;
|
|
398
400
|
}): ConversationHandlePage;
|
|
401
|
+
subscribeConversationFailure(listener: (generation: string) => void): () => void;
|
|
402
|
+
conversationReplayAvailable(expectedGeneration: string): boolean;
|
|
403
|
+
replayConversation(request: import('./conversation-replay.js').ConversationReplayRequest): AsyncGenerator<import("./conversation-replay.js").ConversationReplayPage, any, any>;
|
|
404
|
+
observeConversation(listener: (state: ConversationObservationState) => void): import("./conversation-types.js").ConversationObservation;
|
|
405
|
+
private captureObservation;
|
|
399
406
|
conversationSnapshot(): ConversationSnapshot;
|
|
400
407
|
subscribeConversation(listener: Parameters<ConversationEventStore['subscribe']>[0]): () => void;
|
|
401
408
|
private isCurrentConversationEvent;
|
package/dist/session/acp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConversationObservation } from './conversation-observation.js';
|
|
1
2
|
import { appendTaskCreated } from './task-notice.js';
|
|
2
3
|
import { spawn } from 'node:child_process';
|
|
3
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -278,7 +279,9 @@ export class AcpSession {
|
|
|
278
279
|
sharedTurns = new Map();
|
|
279
280
|
/** Commands the adapter currently advertises; only these are control operations. */
|
|
280
281
|
advertisedCommands = new Set();
|
|
282
|
+
presentedCommands = [];
|
|
281
283
|
queueDepth = 0;
|
|
284
|
+
observation = new ConversationObservation(() => this.captureObservation());
|
|
282
285
|
exit = null;
|
|
283
286
|
steeringSupported = false;
|
|
284
287
|
agentCapabilities;
|
|
@@ -330,6 +333,9 @@ export class AcpSession {
|
|
|
330
333
|
roleId: options.name, log: line => options.log(`[${options.name}] ${line}`),
|
|
331
334
|
});
|
|
332
335
|
this.conversationStartCursor = this.conversation.lastCursor();
|
|
336
|
+
// A durable append can precede its inventory mutation. Capture only after
|
|
337
|
+
// the synchronous owner checkpoint, never the state at append notification.
|
|
338
|
+
this.conversation.subscribe(() => this.observation.scheduleCapture());
|
|
333
339
|
this.sessionFile = join(options.stateDir, '.acp-session-id');
|
|
334
340
|
this.terminated = new Promise((_resolve, reject) => { this.terminate = reject; });
|
|
335
341
|
// Nothing awaits this promise until a request races it; an unobserved
|
|
@@ -363,6 +369,7 @@ export class AcpSession {
|
|
|
363
369
|
acpSessionId: this.sessionId,
|
|
364
370
|
payload: { status: 'failed', detail: this.lastError },
|
|
365
371
|
});
|
|
372
|
+
this.observation.publish();
|
|
366
373
|
});
|
|
367
374
|
}
|
|
368
375
|
static async start(options) {
|
|
@@ -451,7 +458,8 @@ export class AcpSession {
|
|
|
451
458
|
const promptId = open.promptId;
|
|
452
459
|
this.queueDepth++;
|
|
453
460
|
const run = this.enqueue(promptId, text, undefined);
|
|
454
|
-
|
|
461
|
+
this.observation.publish();
|
|
462
|
+
void run.finally(() => { this.queueDepth = Math.max(0, this.queueDepth - 1); this.observation.publish(); });
|
|
455
463
|
}
|
|
456
464
|
}
|
|
457
465
|
isAlive() {
|
|
@@ -470,6 +478,7 @@ export class AcpSession {
|
|
|
470
478
|
return;
|
|
471
479
|
this.steeringOccupied = true;
|
|
472
480
|
this.refreshSteeringOccupancy();
|
|
481
|
+
this.observation.publish();
|
|
473
482
|
}
|
|
474
483
|
refreshSteeringOccupancy() {
|
|
475
484
|
if (!this.steeringOccupied)
|
|
@@ -492,6 +501,7 @@ export class AcpSession {
|
|
|
492
501
|
if (!this.steeringOccupied)
|
|
493
502
|
return;
|
|
494
503
|
this.steeringOccupied = false;
|
|
504
|
+
this.observation.publish();
|
|
495
505
|
this.options.log(`[${this.options.name}] steering-started turn no longer holds the adapter (${reason})`);
|
|
496
506
|
}
|
|
497
507
|
snapshot() {
|
|
@@ -714,11 +724,18 @@ export class AcpSession {
|
|
|
714
724
|
}
|
|
715
725
|
const promptId = randomUUID();
|
|
716
726
|
const queuedBehind = this.queueDepth;
|
|
717
|
-
this.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
727
|
+
const run = this.observation.checkpoint(() => {
|
|
728
|
+
this.admitToLedger(promptId, text, queuedBehind, options);
|
|
729
|
+
this.queueDepth++;
|
|
730
|
+
return this.enqueue(promptId, text, options.origin);
|
|
731
|
+
});
|
|
732
|
+
const completion = run.then(result => {
|
|
733
|
+
this.queueDepth = Math.max(0, this.queueDepth - 1);
|
|
734
|
+
this.observation.publish();
|
|
735
|
+
return result;
|
|
736
|
+
}, error => {
|
|
721
737
|
this.queueDepth = Math.max(0, this.queueDepth - 1);
|
|
738
|
+
this.observation.publish();
|
|
722
739
|
return turnResult(false, 'failed', error?.message ?? String(error));
|
|
723
740
|
});
|
|
724
741
|
return {
|
|
@@ -856,6 +873,7 @@ export class AcpSession {
|
|
|
856
873
|
actor: { browserSession: command.actorBrowserSession },
|
|
857
874
|
});
|
|
858
875
|
const receipt = {
|
|
876
|
+
sessionGeneration: this.sessionGeneration,
|
|
859
877
|
commandId: command.commandId,
|
|
860
878
|
promptId: queued.promptId,
|
|
861
879
|
state: queued.queuedBehind > 0 ? 'queued' : 'starting',
|
|
@@ -1003,6 +1021,7 @@ export class AcpSession {
|
|
|
1003
1021
|
this.allowPermission(pending.toolCallId, permissionId);
|
|
1004
1022
|
else
|
|
1005
1023
|
this.releasePermission(pending.toolCallId, permissionId);
|
|
1024
|
+
this.readiness = this.pendingPermissions.size > 0 ? 'awaiting_permission' : 'running';
|
|
1006
1025
|
this.events.emit('permission', {
|
|
1007
1026
|
turnId: this.activeTurn?.id,
|
|
1008
1027
|
origin: this.activeTurn?.origin,
|
|
@@ -1019,7 +1038,7 @@ export class AcpSession {
|
|
|
1019
1038
|
promptId: this.activeTurn?.id, turnId: this.activeTurn?.id,
|
|
1020
1039
|
payload: { decision, decisionSource: 'manual', optionId },
|
|
1021
1040
|
});
|
|
1022
|
-
this.
|
|
1041
|
+
this.observation.publish();
|
|
1023
1042
|
return true;
|
|
1024
1043
|
}
|
|
1025
1044
|
/**
|
|
@@ -1085,6 +1104,8 @@ export class AcpSession {
|
|
|
1085
1104
|
: { outcome: { outcome: 'cancelled' } });
|
|
1086
1105
|
const settled = decision === 'denied' && !rejectOption ? 'cancelled' : decision;
|
|
1087
1106
|
this.releasePermission(pending.toolCallId, permissionId);
|
|
1107
|
+
if (this.pendingPermissions.size === 0 && this.readiness === 'awaiting_permission')
|
|
1108
|
+
this.readiness = 'running';
|
|
1088
1109
|
this.events.emit('permission', {
|
|
1089
1110
|
turnId: this.activeTurn?.id,
|
|
1090
1111
|
origin: this.activeTurn?.origin,
|
|
@@ -1108,8 +1129,7 @@ export class AcpSession {
|
|
|
1108
1129
|
...(rejectOption ? { optionId: rejectOption.optionId } : {}),
|
|
1109
1130
|
},
|
|
1110
1131
|
});
|
|
1111
|
-
|
|
1112
|
-
this.readiness = 'running';
|
|
1132
|
+
this.observation.publish();
|
|
1113
1133
|
}
|
|
1114
1134
|
exitResult() {
|
|
1115
1135
|
return this.exit;
|
|
@@ -1322,6 +1342,7 @@ export class AcpSession {
|
|
|
1322
1342
|
kind: 'session.state', sessionGeneration: this.sessionGeneration,
|
|
1323
1343
|
acpSessionId: this.sessionId, payload: { status: 'idle' },
|
|
1324
1344
|
});
|
|
1345
|
+
this.observation.publish();
|
|
1325
1346
|
}
|
|
1326
1347
|
captureRuntimeMetadata(options, modelId) {
|
|
1327
1348
|
this.runtimeModel = runtimeSelector(options, 'model');
|
|
@@ -1460,8 +1481,11 @@ export class AcpSession {
|
|
|
1460
1481
|
attempt.recoveryStartedAt = Date.now();
|
|
1461
1482
|
const recoveryOrigin = origin?.kind === 'scheduled-loop'
|
|
1462
1483
|
? origin : { kind: 'stall-watchdog' };
|
|
1463
|
-
this.
|
|
1464
|
-
|
|
1484
|
+
const recovery = this.observation.checkpoint(() => {
|
|
1485
|
+
this.admitToLedger(attempt.recoveryId, STALL_RECOVERY_PROMPT, 0, { origin: recoveryOrigin });
|
|
1486
|
+
return this.runSinglePrompt(STALL_RECOVERY_PROMPT, attempt.recoveryId, recoveryOrigin);
|
|
1487
|
+
});
|
|
1488
|
+
const recovered = await recovery;
|
|
1465
1489
|
attempt.report(attempt.superseded ? 'superseded'
|
|
1466
1490
|
: recovered.succeeded && attempt.resumed ? 'recovery_completed' : 'blocked_recovery');
|
|
1467
1491
|
// A failed diagnostic turn must not make startup tear down the session.
|
|
@@ -1477,6 +1501,7 @@ export class AcpSession {
|
|
|
1477
1501
|
* so none is left open and none is reported delivered that was not sent.
|
|
1478
1502
|
*/
|
|
1479
1503
|
async runSinglePrompt(text, turnId = randomUUID(), origin, members = [{ promptId: turnId, text, origin }]) {
|
|
1504
|
+
let completionPayload;
|
|
1480
1505
|
const closeMembers = (payload) => {
|
|
1481
1506
|
for (const member of members)
|
|
1482
1507
|
this.conversation.appendSafe({
|
|
@@ -1494,6 +1519,7 @@ export class AcpSession {
|
|
|
1494
1519
|
let settle;
|
|
1495
1520
|
const settled = new Promise(resolve => { settle = resolve; });
|
|
1496
1521
|
this.activeTurn = { toolEvidence: new Map(), startedAt: Date.now(), toolIds: new Set(), lastProgressAt: 0, progressCount: 0, transportFailures: 0, boundaryUnknown: false, id: turnId, output: '', origin, settled, settle, promptIds: members.map(member => member.promptId), console: members.some(member => member.origin?.kind === 'owner-admin-console') };
|
|
1522
|
+
this.observation.publish();
|
|
1497
1523
|
this.events.emit('state', { turnId, status: 'running', origin });
|
|
1498
1524
|
members.forEach((member, index) => this.conversation.appendSafe({
|
|
1499
1525
|
kind: 'prompt.started', sessionGeneration: this.sessionGeneration,
|
|
@@ -1516,11 +1542,11 @@ export class AcpSession {
|
|
|
1516
1542
|
turnId, stopReason: response.stopReason, origin, cancellationSource,
|
|
1517
1543
|
});
|
|
1518
1544
|
this.events.emit('state', { status: 'idle' });
|
|
1519
|
-
|
|
1545
|
+
completionPayload = {
|
|
1520
1546
|
outcome: classifyStopReason(response.stopReason),
|
|
1521
1547
|
stopReason: response.stopReason,
|
|
1522
1548
|
...(cancellationSource ? { cancellationSource } : {}),
|
|
1523
|
-
}
|
|
1549
|
+
};
|
|
1524
1550
|
// The prompt was accepted either way — the agent answered. Whether the
|
|
1525
1551
|
// turn SUCCEEDED is a separate question, and only `stopReason` answers it.
|
|
1526
1552
|
return turnResult(true, classifyStopReason(response.stopReason), response.stopReason, this.activeTurn?.id === turnId ? this.activeTurn.output : undefined, this.activeTurn?.id === turnId ? this.activeTurn.cancellationSource : undefined);
|
|
@@ -1540,7 +1566,7 @@ export class AcpSession {
|
|
|
1540
1566
|
});
|
|
1541
1567
|
if (this.isAlive())
|
|
1542
1568
|
this.events.emit('state', { status: 'idle' });
|
|
1543
|
-
|
|
1569
|
+
completionPayload = { outcome: 'failed', stopReason: this.lastError };
|
|
1544
1570
|
return turnResult(false, 'failed', this.lastError, this.activeTurn?.id === turnId ? this.activeTurn.output : undefined);
|
|
1545
1571
|
}
|
|
1546
1572
|
finally {
|
|
@@ -1556,6 +1582,9 @@ export class AcpSession {
|
|
|
1556
1582
|
this.cancelEscalation = undefined;
|
|
1557
1583
|
this.activeTurn = undefined;
|
|
1558
1584
|
}
|
|
1585
|
+
if (completionPayload)
|
|
1586
|
+
closeMembers(completionPayload);
|
|
1587
|
+
this.observation.publish();
|
|
1559
1588
|
}
|
|
1560
1589
|
}
|
|
1561
1590
|
async steerPrompt(text) {
|
|
@@ -1648,42 +1677,20 @@ export class AcpSession {
|
|
|
1648
1677
|
}
|
|
1649
1678
|
const timeoutMs = this.options.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS;
|
|
1650
1679
|
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
origin: this.activeTurn?.origin,
|
|
1655
|
-
permissionId,
|
|
1656
|
-
toolCallId: this.activeTurn?.origin?.kind === 'scheduled-loop'
|
|
1657
|
-
? 'scheduled-loop-tool' : params.toolCall.toolCallId,
|
|
1658
|
-
title: this.activeTurn?.origin?.kind === 'scheduled-loop'
|
|
1680
|
+
const presented = {
|
|
1681
|
+
toolCallId: scheduledTurn(this.activeTurn) ? 'scheduled-loop-tool' : toolCallId,
|
|
1682
|
+
title: scheduledTurn(this.activeTurn)
|
|
1659
1683
|
? 'Scheduled-loop permission requested' : params.toolCall.title ?? 'Permission requested',
|
|
1660
|
-
status: 'pending',
|
|
1661
1684
|
options: params.options.map(option => ({
|
|
1662
1685
|
optionId: option.optionId,
|
|
1663
|
-
name: this.activeTurn
|
|
1686
|
+
name: scheduledTurn(this.activeTurn) ? option.kind : option.name,
|
|
1664
1687
|
kind: option.kind,
|
|
1665
1688
|
})),
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
kind: 'permission.requested', sessionGeneration: this.sessionGeneration,
|
|
1669
|
-
acpSessionId: this.sessionId, permissionId,
|
|
1670
|
-
promptId: this.activeTurn?.id, turnId: this.activeTurn?.id,
|
|
1671
|
-
toolCallId: scheduledTurn(this.activeTurn) ? 'scheduled-loop-tool' : params.toolCall.toolCallId,
|
|
1672
|
-
payload: {
|
|
1673
|
-
toolCallId: scheduledTurn(this.activeTurn) ? 'scheduled-loop-tool' : params.toolCall.toolCallId,
|
|
1674
|
-
title: scheduledTurn(this.activeTurn)
|
|
1675
|
-
? 'Scheduled-loop permission requested' : params.toolCall.title ?? 'Permission requested',
|
|
1676
|
-
options: params.options.map(option => ({
|
|
1677
|
-
optionId: option.optionId,
|
|
1678
|
-
name: scheduledTurn(this.activeTurn) ? option.kind : option.name,
|
|
1679
|
-
kind: option.kind,
|
|
1680
|
-
})),
|
|
1681
|
-
expiresAt,
|
|
1682
|
-
},
|
|
1683
|
-
});
|
|
1689
|
+
expiresAt,
|
|
1690
|
+
};
|
|
1684
1691
|
return new Promise(resolve => {
|
|
1685
1692
|
const pending = {
|
|
1686
|
-
options: params.options, resolve,
|
|
1693
|
+
options: params.options, resolve, presented,
|
|
1687
1694
|
toolCallId,
|
|
1688
1695
|
eventToolCallId: scheduledTurn(this.activeTurn) ? 'scheduled-loop-tool' : toolCallId,
|
|
1689
1696
|
};
|
|
@@ -1692,6 +1699,18 @@ export class AcpSession {
|
|
|
1692
1699
|
}, timeoutMs);
|
|
1693
1700
|
pending.expiry.unref?.();
|
|
1694
1701
|
this.pendingPermissions.set(permissionId, pending);
|
|
1702
|
+
this.readiness = 'awaiting_permission';
|
|
1703
|
+
this.events.emit('permission', {
|
|
1704
|
+
turnId: this.activeTurn?.id, origin: this.activeTurn?.origin,
|
|
1705
|
+
permissionId, ...presented, status: 'pending',
|
|
1706
|
+
});
|
|
1707
|
+
this.conversation.appendSafe({
|
|
1708
|
+
kind: 'permission.requested', sessionGeneration: this.sessionGeneration,
|
|
1709
|
+
acpSessionId: this.sessionId, permissionId,
|
|
1710
|
+
promptId: this.activeTurn?.id, turnId: this.activeTurn?.id,
|
|
1711
|
+
toolCallId: presented.toolCallId, payload: presented,
|
|
1712
|
+
});
|
|
1713
|
+
this.observation.publish();
|
|
1695
1714
|
});
|
|
1696
1715
|
}
|
|
1697
1716
|
/**
|
|
@@ -1868,11 +1887,13 @@ export class AcpSession {
|
|
|
1868
1887
|
const scheduled = this.activeTurn?.origin?.kind === 'scheduled-loop';
|
|
1869
1888
|
const messagePhase = update.sessionUpdate === 'agent_message_chunk'
|
|
1870
1889
|
? this.codexMessagePhase(update) : undefined;
|
|
1871
|
-
|
|
1890
|
+
// Install live capability state before the durable append can synchronously
|
|
1891
|
+
// notify a subscriber that reenters observation at the new ledger barrier.
|
|
1872
1892
|
if (update.sessionUpdate === 'config_option_update')
|
|
1873
1893
|
this.captureRuntimeMetadata(update.configOptions);
|
|
1874
1894
|
if (update.sessionUpdate === 'available_commands_update')
|
|
1875
1895
|
this.advertisedCommands = new Set((update.availableCommands ?? []).map(command => command.name));
|
|
1896
|
+
this.recordConversationUpdate(update, scheduled);
|
|
1876
1897
|
if (update.sessionUpdate === 'usage_update') {
|
|
1877
1898
|
// Claude subscription rate-limit snapshot (claude-agent-acp forwards the SDK rate_limit_event).
|
|
1878
1899
|
const rateLimit = update._meta?.['_claude/rateLimit'];
|
|
@@ -1937,6 +1958,7 @@ export class AcpSession {
|
|
|
1937
1958
|
default:
|
|
1938
1959
|
break;
|
|
1939
1960
|
}
|
|
1961
|
+
this.observation.publish();
|
|
1940
1962
|
}
|
|
1941
1963
|
/**
|
|
1942
1964
|
* Codex ACP's phase extension is the only currently supported visibility
|
|
@@ -1963,6 +1985,8 @@ export class AcpSession {
|
|
|
1963
1985
|
redactText: SCHEDULED_LOOP_REDACTION,
|
|
1964
1986
|
redactToolCallId: 'scheduled-loop-tool',
|
|
1965
1987
|
} : {});
|
|
1988
|
+
if (normalized.kind === 'capabilities.updated' && 'commands' in normalized.payload)
|
|
1989
|
+
this.presentedCommands = normalized.payload.commands ?? [];
|
|
1966
1990
|
this.conversation.appendSafe({
|
|
1967
1991
|
kind: normalized.kind,
|
|
1968
1992
|
sessionGeneration: this.sessionGeneration,
|
|
@@ -2001,6 +2025,36 @@ export class AcpSession {
|
|
|
2001
2025
|
snapshot: this.conversationSnapshot(),
|
|
2002
2026
|
};
|
|
2003
2027
|
}
|
|
2028
|
+
subscribeConversationFailure(listener) {
|
|
2029
|
+
return this.conversation.subscribeFailure(listener);
|
|
2030
|
+
}
|
|
2031
|
+
conversationReplayAvailable(expectedGeneration) {
|
|
2032
|
+
return expectedGeneration === this.sessionGeneration && this.conversation.generationDurable(expectedGeneration);
|
|
2033
|
+
}
|
|
2034
|
+
replayConversation(request) {
|
|
2035
|
+
if (request.expectedGeneration !== this.sessionGeneration)
|
|
2036
|
+
throw new Error('session generation changed');
|
|
2037
|
+
return this.conversation.replay(request);
|
|
2038
|
+
}
|
|
2039
|
+
observeConversation(listener) {
|
|
2040
|
+
return this.observation.observe(listener);
|
|
2041
|
+
}
|
|
2042
|
+
captureObservation() {
|
|
2043
|
+
const snapshot = this.snapshot();
|
|
2044
|
+
return {
|
|
2045
|
+
...this.conversationSnapshot(),
|
|
2046
|
+
...pendingInventory(this.pendingPrompts),
|
|
2047
|
+
ledgerBarrier: Number(this.conversation.lastCursor() ?? 0),
|
|
2048
|
+
alive: snapshot.alive,
|
|
2049
|
+
readiness: snapshot.alive ? snapshot.readiness : 'offline',
|
|
2050
|
+
activePromptIds: [...(this.activeTurn?.promptIds ?? [])],
|
|
2051
|
+
pendingPermissions: [...this.pendingPermissions].flatMap(([permissionId, pending]) => pending.presented ? [{ permissionId, ...pending.presented }] : []),
|
|
2052
|
+
commands: this.presentedCommands,
|
|
2053
|
+
runtimeModel: snapshot.runtimeModel,
|
|
2054
|
+
reasoningEffort: snapshot.reasoningEffort,
|
|
2055
|
+
permissionMode: snapshot.permissionMode,
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2004
2058
|
conversationSnapshot() {
|
|
2005
2059
|
return {
|
|
2006
2060
|
sessionGeneration: this.sessionGeneration,
|
|
@@ -79,6 +79,10 @@ export declare class RoleTurnArbiter implements AgentSession {
|
|
|
79
79
|
limit?: number;
|
|
80
80
|
}): ConversationHandlePage;
|
|
81
81
|
conversationSnapshot(): ConversationSnapshot;
|
|
82
|
+
subscribeConversationFailure(listener: (generation: string) => void): () => void;
|
|
83
|
+
conversationReplayAvailable(expectedGeneration: string): boolean;
|
|
84
|
+
replayConversation(request: import('./conversation-replay.js').ConversationReplayRequest): AsyncIterable<import("./conversation-replay.js").ConversationReplayPage>;
|
|
85
|
+
observeConversation(listener: (state: import('./conversation-types.js').ConversationObservationState) => void): import('./conversation-types.js').ConversationObservation;
|
|
82
86
|
subscribeConversation(listener: (event: ConversationEventV1) => void): () => void;
|
|
83
87
|
submitPromptBrowser(command: SubmitPromptCommand): Promise<PromptReceipt>;
|
|
84
88
|
get liveVoiceSupported(): boolean;
|
package/dist/session/arbiter.js
CHANGED
|
@@ -181,6 +181,24 @@ export class RoleTurnArbiter {
|
|
|
181
181
|
throw new Error('conversation snapshot is unavailable');
|
|
182
182
|
return this.session.conversationSnapshot();
|
|
183
183
|
}
|
|
184
|
+
subscribeConversationFailure(listener) {
|
|
185
|
+
if (!this.session.subscribeConversationFailure)
|
|
186
|
+
throw new Error('conversation durability observation is unavailable');
|
|
187
|
+
return this.session.subscribeConversationFailure(listener);
|
|
188
|
+
}
|
|
189
|
+
conversationReplayAvailable(expectedGeneration) {
|
|
190
|
+
return this.session.conversationReplayAvailable?.(expectedGeneration) ?? false;
|
|
191
|
+
}
|
|
192
|
+
replayConversation(request) {
|
|
193
|
+
if (!this.session.replayConversation)
|
|
194
|
+
throw new Error('conversation replay is unavailable');
|
|
195
|
+
return this.session.replayConversation(request);
|
|
196
|
+
}
|
|
197
|
+
observeConversation(listener) {
|
|
198
|
+
if (!this.session.observeConversation)
|
|
199
|
+
throw new Error('conversation observation is unavailable');
|
|
200
|
+
return this.session.observeConversation(listener);
|
|
201
|
+
}
|
|
184
202
|
subscribeConversation(listener) {
|
|
185
203
|
if (!this.session.subscribeConversation)
|
|
186
204
|
throw new Error('conversation subscription is unavailable');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
|
|
2
2
|
import type { CommonPermissions } from '../config.js';
|
|
3
|
-
import type { ConversationEventV1, ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
3
|
+
import type { ConversationEventV1, ConversationObservationState, ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
4
4
|
import { type PendingPrompt } from './prompt-batch.js';
|
|
5
5
|
import type { CodexAppServerTransportFactory } from './codex-app-server-transport.js';
|
|
6
6
|
import type { AgentSession, ConversationHandlePage, ExitRecord, InterruptOutcome, QueuedPrompt, SessionEvent, SessionSnapshot, SubmitPromptOptions, TurnCancellationSource, TurnResult } from './types.js';
|
|
@@ -66,6 +66,7 @@ export declare class CodexAppServerSession implements AgentSession {
|
|
|
66
66
|
private lastError?;
|
|
67
67
|
private exit;
|
|
68
68
|
private queueDepth;
|
|
69
|
+
private readonly observation;
|
|
69
70
|
private promptTail;
|
|
70
71
|
/** Admitted prompts not yet handed to the app-server, in admission order. */
|
|
71
72
|
private readonly pendingPrompts;
|
|
@@ -112,6 +113,11 @@ export declare class CodexAppServerSession implements AgentSession {
|
|
|
112
113
|
after?: string;
|
|
113
114
|
limit?: number;
|
|
114
115
|
}): ConversationHandlePage;
|
|
116
|
+
subscribeConversationFailure(listener: (generation: string) => void): () => void;
|
|
117
|
+
conversationReplayAvailable(expectedGeneration: string): boolean;
|
|
118
|
+
replayConversation(request: import('./conversation-replay.js').ConversationReplayRequest): AsyncGenerator<import("./conversation-replay.js").ConversationReplayPage, any, any>;
|
|
119
|
+
observeConversation(listener: (state: ConversationObservationState) => void): import("./conversation-types.js").ConversationObservation;
|
|
120
|
+
private captureObservation;
|
|
115
121
|
conversationSnapshot(): ConversationSnapshot;
|
|
116
122
|
subscribeConversation(listener: (event: ConversationEventV1) => void): () => void;
|
|
117
123
|
submitPromptBrowser(command: SubmitPromptCommand): Promise<PromptReceipt>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ConversationObservation } from './conversation-observation.js';
|
|
1
2
|
import { appendTaskCreated } from './task-notice.js';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -148,6 +149,7 @@ export class CodexAppServerSession {
|
|
|
148
149
|
lastError;
|
|
149
150
|
exit = null;
|
|
150
151
|
queueDepth = 0;
|
|
152
|
+
observation = new ConversationObservation(() => this.captureObservation());
|
|
151
153
|
promptTail = Promise.resolve();
|
|
152
154
|
/** Admitted prompts not yet handed to the app-server, in admission order. */
|
|
153
155
|
pendingPrompts = [];
|
|
@@ -170,6 +172,9 @@ export class CodexAppServerSession {
|
|
|
170
172
|
roleId: options.name, log: line => options.log(`[${options.name}] ${line}`),
|
|
171
173
|
});
|
|
172
174
|
this.conversationStartCursor = this.conversation.lastCursor();
|
|
175
|
+
// A durable append can precede its inventory mutation. Capture only after
|
|
176
|
+
// the synchronous owner checkpoint, never the state at append notification.
|
|
177
|
+
this.conversation.subscribe(() => this.observation.scheduleCapture());
|
|
173
178
|
// Use Fleet's provider-neutral resume marker so runner recovery rotates the
|
|
174
179
|
// exact native thread id after a poisoned fast resume.
|
|
175
180
|
this.threadFile = join(options.stateDir, '.session-id');
|
|
@@ -234,14 +239,18 @@ export class CodexAppServerSession {
|
|
|
234
239
|
}
|
|
235
240
|
const promptId = randomUUID();
|
|
236
241
|
const queuedBehind = this.queueDepth;
|
|
237
|
-
this.
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
const run = this.observation.checkpoint(() => {
|
|
243
|
+
this.admit(promptId, text, queuedBehind, options);
|
|
244
|
+
this.queueDepth++;
|
|
245
|
+
return this.enqueue(promptId, text, options.origin);
|
|
246
|
+
});
|
|
240
247
|
const completion = run.then(result => {
|
|
241
248
|
this.queueDepth = Math.max(0, this.queueDepth - 1);
|
|
249
|
+
this.observation.publish();
|
|
242
250
|
return result;
|
|
243
251
|
}, error => {
|
|
244
252
|
this.queueDepth = Math.max(0, this.queueDepth - 1);
|
|
253
|
+
this.observation.publish();
|
|
245
254
|
return turnResult(false, 'failed', error instanceof Error ? error.message : String(error));
|
|
246
255
|
});
|
|
247
256
|
return {
|
|
@@ -413,6 +422,7 @@ export class CodexAppServerSession {
|
|
|
413
422
|
});
|
|
414
423
|
this.conversation.close();
|
|
415
424
|
await this.transport.close();
|
|
425
|
+
this.observation.publish();
|
|
416
426
|
}
|
|
417
427
|
taskNoticeBinding() {
|
|
418
428
|
return this.activeTurn && this.threadId ? { sessionGeneration: this.sessionGeneration, acpSessionId: this.threadId, promptId: this.activeTurn.promptId } : undefined;
|
|
@@ -432,6 +442,36 @@ export class CodexAppServerSession {
|
|
|
432
442
|
}
|
|
433
443
|
return { ...page, events: visible, snapshot: this.conversationSnapshot() };
|
|
434
444
|
}
|
|
445
|
+
subscribeConversationFailure(listener) {
|
|
446
|
+
return this.conversation.subscribeFailure(listener);
|
|
447
|
+
}
|
|
448
|
+
conversationReplayAvailable(expectedGeneration) {
|
|
449
|
+
return expectedGeneration === this.sessionGeneration && this.conversation.generationDurable(expectedGeneration);
|
|
450
|
+
}
|
|
451
|
+
replayConversation(request) {
|
|
452
|
+
if (request.expectedGeneration !== this.sessionGeneration)
|
|
453
|
+
throw new Error('session generation changed');
|
|
454
|
+
return this.conversation.replay(request);
|
|
455
|
+
}
|
|
456
|
+
observeConversation(listener) {
|
|
457
|
+
return this.observation.observe(listener);
|
|
458
|
+
}
|
|
459
|
+
captureObservation() {
|
|
460
|
+
const snapshot = this.snapshot();
|
|
461
|
+
return {
|
|
462
|
+
...this.conversationSnapshot(),
|
|
463
|
+
...pendingInventory(this.pendingPrompts),
|
|
464
|
+
ledgerBarrier: Number(this.conversation.lastCursor() ?? 0),
|
|
465
|
+
alive: snapshot.alive,
|
|
466
|
+
readiness: snapshot.alive ? snapshot.readiness : 'offline',
|
|
467
|
+
activePromptIds: [...(this.activeTurn?.promptIds ?? [])],
|
|
468
|
+
pendingPermissions: [...this.pendingPermissions].flatMap(([permissionId, pending]) => pending.presented ? [{ permissionId, ...pending.presented }] : []),
|
|
469
|
+
commands: [{ name: 'compact', description: textBlock('Compact session context') }],
|
|
470
|
+
runtimeModel: snapshot.runtimeModel,
|
|
471
|
+
reasoningEffort: snapshot.reasoningEffort,
|
|
472
|
+
permissionMode: snapshot.permissionMode,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
435
475
|
conversationSnapshot() {
|
|
436
476
|
return {
|
|
437
477
|
sessionGeneration: this.sessionGeneration,
|
|
@@ -460,6 +500,7 @@ export class CodexAppServerSession {
|
|
|
460
500
|
actor: { browserSession: command.actorBrowserSession },
|
|
461
501
|
});
|
|
462
502
|
const receipt = {
|
|
503
|
+
sessionGeneration: this.sessionGeneration,
|
|
463
504
|
commandId: command.commandId, promptId: queued.promptId,
|
|
464
505
|
state: queued.queuedBehind ? 'queued' : 'starting', queuedBehind: queued.queuedBehind,
|
|
465
506
|
acceptedAt: new Date().toISOString(), eventCursor: this.conversation.lastCursor() ?? '0',
|
|
@@ -515,6 +556,7 @@ export class CodexAppServerSession {
|
|
|
515
556
|
kind: 'session.state', sessionGeneration: this.sessionGeneration,
|
|
516
557
|
payload: { status: 'idle' },
|
|
517
558
|
});
|
|
559
|
+
this.observation.publish();
|
|
518
560
|
}
|
|
519
561
|
recoverOpenPrompts() {
|
|
520
562
|
for (const open of this.conversation.openPrompts()) {
|
|
@@ -534,7 +576,8 @@ export class CodexAppServerSession {
|
|
|
534
576
|
? { kind: 'owner-admin-console', commandId: open.commandId }
|
|
535
577
|
: undefined;
|
|
536
578
|
const run = this.enqueue(open.promptId, open.text, origin);
|
|
537
|
-
|
|
579
|
+
this.observation.publish();
|
|
580
|
+
void run.finally(() => { this.queueDepth = Math.max(0, this.queueDepth - 1); this.observation.publish(); });
|
|
538
581
|
}
|
|
539
582
|
}
|
|
540
583
|
admit(promptId, text, queuedBehind, options) {
|
|
@@ -580,6 +623,7 @@ export class CodexAppServerSession {
|
|
|
580
623
|
this.finalAnswerObserved = false;
|
|
581
624
|
this.activeItems.clear();
|
|
582
625
|
this.readiness = 'running';
|
|
626
|
+
this.observation.publish();
|
|
583
627
|
this.events.emit('state', { turnId: promptId, status: 'running', origin });
|
|
584
628
|
members.forEach((member, index) => this.conversation.appendSafe({
|
|
585
629
|
kind: 'prompt.started', sessionGeneration: this.sessionGeneration,
|
|
@@ -890,6 +934,7 @@ export class CodexAppServerSession {
|
|
|
890
934
|
...(active.cancellationSource ? { cancellationSource: active.cancellationSource } : {}),
|
|
891
935
|
},
|
|
892
936
|
});
|
|
937
|
+
this.observation.publish();
|
|
893
938
|
active.settle(result);
|
|
894
939
|
}
|
|
895
940
|
/**
|
|
@@ -1053,7 +1098,6 @@ export class CodexAppServerSession {
|
|
|
1053
1098
|
this.resolvePermission(permissionId, pending, 'denied', 'automatic', 'deny_once', unattended ? 'permissions.unattended=deny' : 'permissions.approval=deny', unattended ? 'no controller is attached' : 'the role denies permission requests');
|
|
1054
1099
|
return;
|
|
1055
1100
|
}
|
|
1056
|
-
this.pendingPermissions.set(permissionId, pending);
|
|
1057
1101
|
this.readiness = 'awaiting_permission';
|
|
1058
1102
|
const timeoutMs = this.options.permissionTimeoutMs ?? PERMISSION_TIMEOUT_MS;
|
|
1059
1103
|
const expiresAt = new Date(Date.now() + timeoutMs).toISOString();
|
|
@@ -1063,19 +1107,23 @@ export class CodexAppServerSession {
|
|
|
1063
1107
|
?? (method === 'item/fileChange/requestApproval' ? 'Apply file changes'
|
|
1064
1108
|
: method === 'item/permissions/requestApproval' ? 'Grant additional permissions'
|
|
1065
1109
|
: 'Run command');
|
|
1110
|
+
const presented = { toolCallId, title, options, expiresAt };
|
|
1111
|
+
pending.presented = presented;
|
|
1112
|
+
this.pendingPermissions.set(permissionId, pending);
|
|
1066
1113
|
this.events.emit('permission', {
|
|
1067
1114
|
turnId: this.activeTurn?.promptId, origin: this.activeTurn?.origin,
|
|
1068
|
-
permissionId,
|
|
1115
|
+
permissionId, ...presented, status: 'pending',
|
|
1069
1116
|
});
|
|
1070
1117
|
this.conversation.appendSafe({
|
|
1071
1118
|
kind: 'permission.requested', sessionGeneration: this.sessionGeneration,
|
|
1072
1119
|
permissionId, promptId: this.activeTurn?.promptId, turnId: this.activeTurn?.promptId,
|
|
1073
|
-
toolCallId, payload:
|
|
1120
|
+
toolCallId, payload: presented,
|
|
1074
1121
|
});
|
|
1075
1122
|
pending.expiry = setTimeout(() => {
|
|
1076
1123
|
this.autoResolvePermission(permissionId, pending, undefined, `no decision arrived within ${Math.round(timeoutMs / 1000)}s`);
|
|
1077
1124
|
}, timeoutMs);
|
|
1078
1125
|
pending.expiry.unref?.();
|
|
1126
|
+
this.observation.publish();
|
|
1079
1127
|
}
|
|
1080
1128
|
serverRequestResolved(params) {
|
|
1081
1129
|
const requestId = typeof params.requestId === 'string' || typeof params.requestId === 'number'
|
|
@@ -1101,6 +1149,8 @@ export class CodexAppServerSession {
|
|
|
1101
1149
|
this.resolvePermission(permissionId, pending, 'denied', 'automatic', 'deny_once', policy, reason);
|
|
1102
1150
|
}
|
|
1103
1151
|
resolvePermission(permissionId, pending, decision, decisionSource, optionId, policy, reason) {
|
|
1152
|
+
if (!this.pendingPermissions.size && this.readiness === 'awaiting_permission')
|
|
1153
|
+
this.readiness = 'running';
|
|
1104
1154
|
this.events.emit('permission', {
|
|
1105
1155
|
turnId: this.activeTurn?.promptId, origin: this.activeTurn?.origin,
|
|
1106
1156
|
permissionId, toolCallId: pending.toolCallId, status: 'completed', decision,
|
|
@@ -1113,8 +1163,7 @@ export class CodexAppServerSession {
|
|
|
1113
1163
|
payload: { decision, decisionSource, ...(optionId ? { optionId } : {}),
|
|
1114
1164
|
...(policy ? { policy } : {}), ...(reason ? { reason } : {}) },
|
|
1115
1165
|
});
|
|
1116
|
-
|
|
1117
|
-
this.readiness = 'running';
|
|
1166
|
+
this.observation.publish();
|
|
1118
1167
|
this.scheduleTerminalReconciliation();
|
|
1119
1168
|
}
|
|
1120
1169
|
currentEvent(event) {
|
|
@@ -1162,10 +1211,12 @@ export class CodexAppServerSession {
|
|
|
1162
1211
|
if (this.activeTurn)
|
|
1163
1212
|
this.finishTurn('failed', this.lastError);
|
|
1164
1213
|
}
|
|
1214
|
+
this.observation.publish();
|
|
1165
1215
|
}
|
|
1166
1216
|
fail(error) {
|
|
1167
1217
|
this.lastError = error instanceof Error ? error.message : String(error);
|
|
1168
1218
|
this.readiness = 'failed';
|
|
1169
1219
|
this.events.emit('error', { text: this.lastError });
|
|
1220
|
+
this.observation.publish();
|
|
1170
1221
|
}
|
|
1171
1222
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LayoutControlRequest } from '../rooms-tasks/layout-control.js';
|
|
2
2
|
import type { SupervisorCorrespondence, AgentHistoryQuery } from '../agent-ours/correspondence.js';
|
|
3
3
|
import { type Socket } from 'node:net';
|
|
4
|
+
import { type PassiveConversationFollowRequest } from './conversation-replay.js';
|
|
4
5
|
import type { AgentSession, ControlFailureKind, SessionEvent, SessionSnapshot } from './types.js';
|
|
5
6
|
import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
|
|
6
7
|
import type { ScheduledLoopManagerHandle } from '../loops/manager.js';
|
|
@@ -26,7 +27,8 @@ export interface ControlRequest {
|
|
|
26
27
|
ownerChannel?: OwnerChannelManagementRequest;
|
|
27
28
|
loop?: string;
|
|
28
29
|
/** Conversation cursor: replay strictly after this durable seq. */
|
|
29
|
-
after?: string;
|
|
30
|
+
after?: string | number;
|
|
31
|
+
expectedGeneration?: string;
|
|
30
32
|
limit?: number;
|
|
31
33
|
/** Idempotency key for v3 mutations. */
|
|
32
34
|
commandId?: string;
|
|
@@ -141,6 +143,7 @@ export declare class RoleControlServer {
|
|
|
141
143
|
private handle;
|
|
142
144
|
/** Version and capability gate shared by every conversation v3 command. */
|
|
143
145
|
private requireConversation;
|
|
146
|
+
private observationFeatures;
|
|
144
147
|
private write;
|
|
145
148
|
}
|
|
146
149
|
/**
|
|
@@ -160,7 +163,7 @@ export declare function followControl(stateDir: string, onMessage: (message: Rec
|
|
|
160
163
|
* `{ version, conversationEvent }`. Closing the socket detaches the
|
|
161
164
|
* controller-presence this connection contributed.
|
|
162
165
|
*/
|
|
163
|
-
export declare function followConversation(stateDir: string, after: string | undefined, onMessage: (message: Record<string, unknown>) => void): Promise<{
|
|
166
|
+
export declare function followConversation(stateDir: string, after: string | undefined | PassiveConversationFollowRequest, onMessage: (message: Record<string, unknown>) => void): Promise<{
|
|
164
167
|
socket: Socket;
|
|
165
168
|
close(): void;
|
|
166
169
|
}>;
|