@myagentroam/node 0.9.2 → 0.9.4
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/dist/codex-app-server.d.ts +24 -0
- package/dist/codex-app-server.js +100 -5
- package/dist/connector.js +18 -15
- package/dist/control-outbound-scheduler.d.ts +4 -1
- package/dist/control-outbound-scheduler.js +39 -6
- package/dist/database.d.ts +2 -19
- package/dist/database.js +1 -62
- package/dist/event-buffer.d.ts +2 -5
- package/dist/event-buffer.js +3 -12
- package/dist/native-session-history.js +33 -12
- package/dist/runner/claude/managed-run-controller.d.ts +0 -1
- package/dist/runner/claude/managed-run-controller.js +17 -12
- package/dist/runner/codex/conversation-parser.d.ts +1 -1
- package/dist/runner/codex/conversation-parser.js +3 -0
- package/dist/runner/codex/managed-run-controller.d.ts +11 -2
- package/dist/runner/codex/managed-run-controller.js +79 -24
- package/dist/runner/codex-runner.d.ts +3 -0
- package/dist/runner/codex-runner.js +42 -3
- package/dist/runner/opencode/conversation-parser.js +1 -1
- package/dist/runner/opencode/managed-run-controller.d.ts +7 -2
- package/dist/runner/opencode/managed-run-controller.js +72 -14
- package/dist/runner/opencode-runner.d.ts +3 -0
- package/dist/runner/opencode-runner.js +25 -0
- package/dist/runner-profiles.js +10 -6
- package/dist/runtime-state.d.ts +7 -12
- package/dist/runtime-state.js +43 -53
- package/dist/service/attachment-domain-state.d.ts +2 -0
- package/dist/service/attachment-domain-state.js +2 -0
- package/dist/service/conversation-history-service.d.ts +8 -0
- package/dist/service/conversation-history-service.js +181 -9
- package/dist/service/conversation-segment-service.js +17 -10
- package/dist/service/external-session-resume-service.js +9 -0
- package/dist/service/fake-managed-run-service.d.ts +0 -1
- package/dist/service/fake-managed-run-service.js +0 -2
- package/dist/service/native-session-projection-service.js +5 -0
- package/dist/service/native-session-watch-service.d.ts +8 -2
- package/dist/service/native-session-watch-service.js +28 -10
- package/dist/service/node-connection-lifecycle-service.d.ts +0 -2
- package/dist/service/node-connection-lifecycle-service.js +0 -1
- package/dist/service/node-control-channel.js +1 -1
- package/dist/service/node-control-message-service.d.ts +0 -1
- package/dist/service/node-control-message-service.js +9 -6
- package/dist/service/node-request-service.js +2 -1
- package/dist/service/node-workspace-coordinator.d.ts +1 -4
- package/dist/service/node-workspace-coordinator.js +2 -12
- package/dist/service/run-attachment-service.js +30 -6
- package/dist/service/run-event-service.d.ts +1 -0
- package/dist/service/run-event-service.js +80 -0
- package/dist/service/run-workbench-service.d.ts +1 -1
- package/dist/service/run-workbench-service.js +1 -5
- package/dist/service/runner-interaction-service.d.ts +1 -2
- package/dist/service/runner-interaction-service.js +1 -4
- package/dist/service/session-context-service.d.ts +1 -0
- package/dist/service/session-context-service.js +14 -0
- package/dist/service/session-domain-state.d.ts +1 -0
- package/dist/service/session-domain-state.js +1 -0
- package/dist/service/session-lifecycle-service.js +4 -12
- package/dist/service/session-message-service.d.ts +1 -0
- package/dist/service/session-message-service.js +109 -85
- package/dist/service/skill-directory-service.d.ts +3 -1
- package/dist/service/skill-directory-service.js +28 -4
- package/dist/service/skill-node-operation-service.d.ts +1 -1
- package/dist/service/skill-node-operation-service.js +4 -2
- package/dist/service/workbench-event-service.d.ts +1 -13
- package/dist/service/workbench-event-service.js +2 -36
- package/dist/service/workbench-manifest-service.d.ts +0 -1
- package/dist/service/workbench-manifest-service.js +0 -1
- package/dist/service/workspace-change-service.js +1 -1
- package/dist/service/workspace-domain-state.d.ts +1 -0
- package/dist/service/workspace-queue-workbench-service.d.ts +13 -2
- package/dist/service/workspace-queue-workbench-service.js +8 -2
- package/dist/service/workspace-upload-service.d.ts +2 -0
- package/dist/service/workspace-upload-service.js +28 -6
- package/dist/service/workspace-workbench-service.d.ts +0 -3
- package/dist/service/workspace-workbench-service.js +0 -17
- package/dist/util/runner-error.d.ts +6 -0
- package/dist/util/runner-error.js +52 -0
- package/dist/util/runner-native-session-parsers.d.ts +2 -1
- package/dist/util/runner-native-session-parsers.js +63 -15
- package/dist/workspace.js +33 -8
- package/package.json +2 -2
package/dist/runtime-state.js
CHANGED
|
@@ -9,22 +9,22 @@ export class NodeRuntimeState {
|
|
|
9
9
|
nodeId;
|
|
10
10
|
getWorkspace;
|
|
11
11
|
now;
|
|
12
|
+
static MAX_COMPLETED_TURNS_PER_SESSION = 10;
|
|
12
13
|
sessions = new Map();
|
|
13
14
|
runs = new Map();
|
|
14
15
|
messageRuns = new Map();
|
|
15
16
|
events = new Map();
|
|
16
17
|
turns = new Map();
|
|
17
18
|
approvals = new Map();
|
|
18
|
-
workspaceEvents = new Map();
|
|
19
19
|
/** A Session owns at most one running turn; Workspace sessions may run concurrently. */
|
|
20
20
|
leases = new Map();
|
|
21
21
|
/** Session FIFO metadata is deliberately process-local with the rest of run state. */
|
|
22
22
|
queuedRuns = new Map();
|
|
23
23
|
queueVersions = new Map();
|
|
24
24
|
pausedQueues = new Set();
|
|
25
|
-
|
|
25
|
+
/** Bounded tombstones reject stale internal start envelopes after terminal cleanup. */
|
|
26
|
+
terminalRunIds = new Set();
|
|
26
27
|
nextEventId = 1;
|
|
27
|
-
nextWorkspaceEventId = 1;
|
|
28
28
|
constructor(nodeId, getWorkspace, now = Date.now) {
|
|
29
29
|
this.nodeId = nodeId;
|
|
30
30
|
this.getWorkspace = getWorkspace;
|
|
@@ -73,6 +73,8 @@ export class NodeRuntimeState {
|
|
|
73
73
|
}
|
|
74
74
|
/** Internal envelope compatibility for a Run already accepted in this process. */
|
|
75
75
|
adoptRun(input) {
|
|
76
|
+
if (this.terminalRunIds.has(input.runId))
|
|
77
|
+
throw new Error('RUN_TERMINAL');
|
|
76
78
|
let session = this.sessions.get(input.sessionId);
|
|
77
79
|
if (session === undefined) {
|
|
78
80
|
const now = this.now();
|
|
@@ -253,7 +255,7 @@ export class NodeRuntimeState {
|
|
|
253
255
|
const session = this.requireSession(input.sessionId);
|
|
254
256
|
const now = this.now();
|
|
255
257
|
const run = {
|
|
256
|
-
id: randomUUID(),
|
|
258
|
+
id: input.runId ?? randomUUID(),
|
|
257
259
|
sessionId: session.id,
|
|
258
260
|
nodeId: this.nodeId(),
|
|
259
261
|
workspaceId: session.workspaceId,
|
|
@@ -262,6 +264,9 @@ export class NodeRuntimeState {
|
|
|
262
264
|
recoveryState: 'ACTIVE',
|
|
263
265
|
version: 0,
|
|
264
266
|
clientMessageId: input.clientMessageId,
|
|
267
|
+
...(input.initiatedByUserId === undefined
|
|
268
|
+
? {}
|
|
269
|
+
: { initiatedByUserId: input.initiatedByUserId }),
|
|
265
270
|
createdAt: now,
|
|
266
271
|
updatedAt: now
|
|
267
272
|
};
|
|
@@ -316,11 +321,6 @@ export class NodeRuntimeState {
|
|
|
316
321
|
this.queuedRuns.set(session.id, [...(this.queuedRuns.get(session.id) ?? []), run.id]);
|
|
317
322
|
this.bumpQueueVersion(session.id);
|
|
318
323
|
this.turns.set(session.id, [...(this.turns.get(session.id) ?? []), fixedTurn]);
|
|
319
|
-
this.recordWorkspaceActivity(run.workspaceId, 'run.created', {
|
|
320
|
-
runId: run.id,
|
|
321
|
-
sessionId: session.id,
|
|
322
|
-
runner: run.runner
|
|
323
|
-
});
|
|
324
324
|
this.replaceSession(session.id, { lastActivityAt: now });
|
|
325
325
|
return { run, created: true };
|
|
326
326
|
}
|
|
@@ -402,7 +402,6 @@ export class NodeRuntimeState {
|
|
|
402
402
|
this.bumpQueueVersion(run.sessionId);
|
|
403
403
|
this.runs.delete(runId);
|
|
404
404
|
this.events.delete(runId);
|
|
405
|
-
this.snapshots.delete(runId);
|
|
406
405
|
this.turns.set(run.sessionId, (this.turns.get(run.sessionId) ?? []).filter((turn) => turn.runId !== runId));
|
|
407
406
|
for (const [key, candidate] of this.messageRuns)
|
|
408
407
|
if (candidate === runId)
|
|
@@ -468,12 +467,6 @@ export class NodeRuntimeState {
|
|
|
468
467
|
if (input.status !== undefined)
|
|
469
468
|
this.transitionRun(input.runId, input.status);
|
|
470
469
|
this.projectEvent(value);
|
|
471
|
-
const run = this.requireRun(input.runId);
|
|
472
|
-
this.recordWorkspaceActivity(run.workspaceId, 'run.updated', {
|
|
473
|
-
runId: run.id,
|
|
474
|
-
eventType: input.eventType,
|
|
475
|
-
status: input.status ?? null
|
|
476
|
-
});
|
|
477
470
|
return value;
|
|
478
471
|
}
|
|
479
472
|
listRunEvents(id, after = 0) {
|
|
@@ -481,15 +474,16 @@ export class NodeRuntimeState {
|
|
|
481
474
|
}
|
|
482
475
|
listConversationTurns(input) {
|
|
483
476
|
this.requireSession(input.sessionId);
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
487
|
-
//
|
|
477
|
+
// A managed Turn keeps the user's acceptance time as its stable timeline
|
|
478
|
+
// position. Runner start/completion timestamps must not move it behind a
|
|
479
|
+
// message that was accepted later. Native Turns without a user timestamp
|
|
480
|
+
// fall back to their own lifecycle time.
|
|
488
481
|
const all = [...(this.turns.get(input.sessionId) ?? [])]
|
|
489
482
|
.map((turn, index) => ({ turn, index }))
|
|
490
483
|
.sort((a, b) => {
|
|
491
|
-
const timestamp = (value) => value.startedAt ??
|
|
492
|
-
value.
|
|
484
|
+
const timestamp = (value) => value.items.find((item) => item.id === value.userItemId)?.startedAt ??
|
|
485
|
+
value.startedAt ??
|
|
486
|
+
value.completedAt ??
|
|
493
487
|
0;
|
|
494
488
|
return timestamp(a.turn) - timestamp(b.turn) || a.index - b.index;
|
|
495
489
|
})
|
|
@@ -559,16 +553,6 @@ export class NodeRuntimeState {
|
|
|
559
553
|
this.replaceSession(session.id, { lastActivityAt: now });
|
|
560
554
|
return completedTurn;
|
|
561
555
|
}
|
|
562
|
-
listWorkspaceActivity(input) {
|
|
563
|
-
const all = this.workspaceEvents.get(input.workspaceId) ?? [];
|
|
564
|
-
const start = decodeCursor(input.cursor, input.workspaceId);
|
|
565
|
-
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
566
|
-
const events = all.slice(start, start + limit);
|
|
567
|
-
return {
|
|
568
|
-
events,
|
|
569
|
-
nextCursor: start + limit < all.length ? encodeCursor(input.workspaceId, start + limit) : null
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
556
|
createApproval(input) {
|
|
573
557
|
this.requireRun(input.runId);
|
|
574
558
|
if (this.approvals.has(input.approvalId))
|
|
@@ -631,11 +615,30 @@ export class NodeRuntimeState {
|
|
|
631
615
|
lastRunSequence(id) {
|
|
632
616
|
return this.events.get(id)?.at(-1)?.sequence ?? 0;
|
|
633
617
|
}
|
|
634
|
-
|
|
635
|
-
this.
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
618
|
+
removeTerminalRun(runId) {
|
|
619
|
+
const run = this.requireRun(runId);
|
|
620
|
+
if (!terminal(run.status))
|
|
621
|
+
throw new Error('RUN_ACTIVE');
|
|
622
|
+
this.terminalRunIds.add(runId);
|
|
623
|
+
if (this.terminalRunIds.size > 1_024)
|
|
624
|
+
this.terminalRunIds.delete(this.terminalRunIds.values().next().value);
|
|
625
|
+
const queue = this.queuedRuns.get(run.sessionId) ?? [];
|
|
626
|
+
if (queue.includes(runId)) {
|
|
627
|
+
this.queuedRuns.set(run.sessionId, queue.filter((candidate) => candidate !== runId));
|
|
628
|
+
this.bumpQueueVersion(run.sessionId);
|
|
629
|
+
}
|
|
630
|
+
this.runs.delete(runId);
|
|
631
|
+
this.events.delete(runId);
|
|
632
|
+
const turns = this.turns.get(run.sessionId) ?? [];
|
|
633
|
+
const completed = turns.filter((turn) => terminalTurn(turn.status));
|
|
634
|
+
const retainedCompleted = new Set(completed.slice(-NodeRuntimeState.MAX_COMPLETED_TURNS_PER_SESSION).map((turn) => turn.id));
|
|
635
|
+
this.turns.set(run.sessionId, turns.filter((turn) => !terminalTurn(turn.status) || retainedCompleted.has(turn.id)));
|
|
636
|
+
for (const [key, candidate] of this.messageRuns)
|
|
637
|
+
if (candidate === runId)
|
|
638
|
+
this.messageRuns.delete(key);
|
|
639
|
+
for (const [approvalId, approval] of this.approvals)
|
|
640
|
+
if (approval.runId === runId)
|
|
641
|
+
this.approvals.delete(approvalId);
|
|
639
642
|
}
|
|
640
643
|
hasActiveWorkspaceLease(workspaceId) {
|
|
641
644
|
return [...this.leases.values()]
|
|
@@ -658,7 +661,6 @@ export class NodeRuntimeState {
|
|
|
658
661
|
for (const runId of runIds) {
|
|
659
662
|
this.runs.delete(runId);
|
|
660
663
|
this.events.delete(runId);
|
|
661
|
-
this.snapshots.delete(runId);
|
|
662
664
|
}
|
|
663
665
|
for (const [key, runId] of this.messageRuns)
|
|
664
666
|
if (runIds.has(runId))
|
|
@@ -681,7 +683,6 @@ export class NodeRuntimeState {
|
|
|
681
683
|
for (const runId of runIds) {
|
|
682
684
|
this.runs.delete(runId);
|
|
683
685
|
this.events.delete(runId);
|
|
684
|
-
this.snapshots.delete(runId);
|
|
685
686
|
}
|
|
686
687
|
for (const [key, runId] of this.messageRuns)
|
|
687
688
|
if (runIds.has(runId))
|
|
@@ -689,7 +690,6 @@ export class NodeRuntimeState {
|
|
|
689
690
|
for (const [approvalId, approval] of this.approvals)
|
|
690
691
|
if (runIds.has(approval.runId))
|
|
691
692
|
this.approvals.delete(approvalId);
|
|
692
|
-
this.workspaceEvents.delete(workspaceId);
|
|
693
693
|
for (const sessionId of sessionIds) {
|
|
694
694
|
this.queuedRuns.delete(sessionId);
|
|
695
695
|
this.queueVersions.delete(sessionId);
|
|
@@ -730,19 +730,6 @@ export class NodeRuntimeState {
|
|
|
730
730
|
throw new Error('APPROVAL_NOT_FOUND');
|
|
731
731
|
return value;
|
|
732
732
|
}
|
|
733
|
-
recordWorkspaceActivity(workspaceId, eventType, payload) {
|
|
734
|
-
const event = {
|
|
735
|
-
id: this.nextWorkspaceEventId++,
|
|
736
|
-
workspaceId,
|
|
737
|
-
eventType,
|
|
738
|
-
payload,
|
|
739
|
-
createdAt: this.now()
|
|
740
|
-
};
|
|
741
|
-
this.workspaceEvents.set(workspaceId, [
|
|
742
|
-
...(this.workspaceEvents.get(workspaceId) ?? []),
|
|
743
|
-
event
|
|
744
|
-
]);
|
|
745
|
-
}
|
|
746
733
|
updateTurnForRun(runId, update) {
|
|
747
734
|
const run = this.requireRun(runId);
|
|
748
735
|
const current = this.turns.get(run.sessionId) ?? [];
|
|
@@ -840,6 +827,9 @@ export class NodeRuntimeState {
|
|
|
840
827
|
function terminal(status) {
|
|
841
828
|
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
842
829
|
}
|
|
830
|
+
function terminalTurn(status) {
|
|
831
|
+
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(status);
|
|
832
|
+
}
|
|
843
833
|
function transitionAllowed(from, to) {
|
|
844
834
|
return !terminal(from) || from === to;
|
|
845
835
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare class AttachmentDomainState<TCachedRunImages, TNativeImageSource> {
|
|
2
2
|
readonly runDirectories: Map<string, string>;
|
|
3
|
+
readonly runFileDirectories: Map<string, string>;
|
|
3
4
|
readonly runImages: Map<string, TCachedRunImages>;
|
|
4
5
|
readonly nativeHistoryImages: Map<string, TNativeImageSource[]>;
|
|
6
|
+
readonly nativeHistoryImageTimers: Map<string, NodeJS.Timeout>;
|
|
5
7
|
}
|
|
@@ -5,6 +5,10 @@ import type { RunnerRegistry } from '../runner/runner-registry.js';
|
|
|
5
5
|
import { type ConversationHistoryPage } from '../util/runner-native-session-parsers.js';
|
|
6
6
|
import type { RunAttachmentService } from './run-attachment-service.js';
|
|
7
7
|
export declare function hasManagedActiveRun(runtime: NodeRuntimeState, session: NodeAgentSession): boolean;
|
|
8
|
+
export declare function convergeConversationWatchPage(previous: ConversationHistoryPage | undefined, previousObserved: ConversationHistoryPage | undefined, next: ConversationHistoryPage, previousAuthority: unknown): {
|
|
9
|
+
readonly page: ConversationHistoryPage;
|
|
10
|
+
readonly authority: ConversationHistoryPage['source'];
|
|
11
|
+
};
|
|
8
12
|
interface ConversationHistoryServiceOptions {
|
|
9
13
|
readonly runtime: NodeRuntimeState;
|
|
10
14
|
readonly runners: RunnerRegistry;
|
|
@@ -16,11 +20,15 @@ interface ConversationHistoryServiceOptions {
|
|
|
16
20
|
}
|
|
17
21
|
export declare class ConversationHistoryService {
|
|
18
22
|
private readonly options;
|
|
23
|
+
private readonly activeOfficialReads;
|
|
19
24
|
constructor(options: ConversationHistoryServiceOptions);
|
|
20
25
|
read(session: NodeAgentSession, input: {
|
|
21
26
|
readonly cursor?: string;
|
|
22
27
|
readonly limit?: number;
|
|
23
28
|
}): Promise<ConversationHistoryPage>;
|
|
29
|
+
private readManagedActive;
|
|
30
|
+
private prefetchActiveOfficial;
|
|
31
|
+
private takeActiveOfficialRead;
|
|
24
32
|
refreshExternalActivity(session: NodeAgentSession): Promise<void>;
|
|
25
33
|
readNative(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeSessionHistory | undefined>;
|
|
26
34
|
private attachNativeImages;
|
|
@@ -8,14 +8,55 @@ export function hasManagedActiveRun(runtime, session) {
|
|
|
8
8
|
.listRunsForSession(session.id)
|
|
9
9
|
.some((run) => run.status === 'STARTING' || run.status === 'RUNNING' || run.status === 'CANCELLING'));
|
|
10
10
|
}
|
|
11
|
+
export function convergeConversationWatchPage(previous, previousObserved, next, previousAuthority) {
|
|
12
|
+
const authority = isConversationHistorySource(previousAuthority)
|
|
13
|
+
? previousAuthority
|
|
14
|
+
: previous?.source;
|
|
15
|
+
if (previous === undefined ||
|
|
16
|
+
authority === undefined ||
|
|
17
|
+
sourceRank(next.source) >= sourceRank(authority))
|
|
18
|
+
return { page: next, authority: next.source };
|
|
19
|
+
if (previous.turns.length === 0)
|
|
20
|
+
return { page: next, authority };
|
|
21
|
+
const boundary = next.turns.findLastIndex((turn) => previous.turns.some((candidate) => sameConversationTurn(candidate, turn)));
|
|
22
|
+
const observedBoundary = boundary >= 0 || previousObserved === undefined
|
|
23
|
+
? -1
|
|
24
|
+
: next.turns.findLastIndex((turn) => previousObserved.turns.some((candidate) => sameConversationTurn(candidate, turn)));
|
|
25
|
+
const safeBoundary = Math.max(boundary, observedBoundary);
|
|
26
|
+
if (safeBoundary < 0)
|
|
27
|
+
return { page: previous, authority };
|
|
28
|
+
const additions = next.turns
|
|
29
|
+
.slice(safeBoundary + 1)
|
|
30
|
+
.filter((turn) => !previous.turns.some((candidate) => sameConversationTurn(candidate, turn)));
|
|
31
|
+
if (additions.length === 0)
|
|
32
|
+
return { page: previous, authority };
|
|
33
|
+
return {
|
|
34
|
+
page: {
|
|
35
|
+
...previous,
|
|
36
|
+
source: next.source,
|
|
37
|
+
snapshotSequence: Math.max(previous.snapshotSequence, next.snapshotSequence),
|
|
38
|
+
readAt: next.readAt,
|
|
39
|
+
latestVisibleAt: next.latestVisibleAt ?? previous.latestVisibleAt,
|
|
40
|
+
turns: [...previous.turns, ...additions]
|
|
41
|
+
},
|
|
42
|
+
authority
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isConversationHistorySource(value) {
|
|
46
|
+
return value === 'official' || value === 'native' || value === 'runtime';
|
|
47
|
+
}
|
|
11
48
|
export class ConversationHistoryService {
|
|
12
49
|
options;
|
|
50
|
+
activeOfficialReads = new Map();
|
|
13
51
|
constructor(options) {
|
|
14
52
|
this.options = options;
|
|
15
53
|
}
|
|
16
54
|
async read(session, input) {
|
|
17
55
|
const snapshotSequence = this.options.snapshotSequence();
|
|
18
56
|
const readAt = Date.now();
|
|
57
|
+
const activeCursor = decodeActiveHistoryCursor(input.cursor, session.id);
|
|
58
|
+
if (activeCursor !== undefined || this.options.hasManagedActiveRun(session))
|
|
59
|
+
return this.readManagedActive(session, input, activeCursor, snapshotSequence, readAt);
|
|
19
60
|
let official = await this.readOfficial(session, input);
|
|
20
61
|
if (official !== undefined) {
|
|
21
62
|
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
@@ -23,13 +64,11 @@ export class ConversationHistoryService {
|
|
|
23
64
|
? []
|
|
24
65
|
: this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 }).turns;
|
|
25
66
|
const initialOfficialRunIds = new Set(official.turns.flatMap((turn) => (turn.runId === null ? [] : [turn.runId])));
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
? absentLocalTurns.filter((turn) => Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0) >= officialFloor)
|
|
32
|
-
: absentLocalTurns.slice(-1);
|
|
67
|
+
const lastRepresentedRuntimeIndex = runtimeTurns.findLastIndex((turn) => turn.runId !== null && initialOfficialRunIds.has(turn.runId));
|
|
68
|
+
const officialHasTurns = official.turns.length > 0;
|
|
69
|
+
const absentLocalTurns = runtimeTurns.filter((turn, index) => (turn.runId === null || !initialOfficialRunIds.has(turn.runId)) &&
|
|
70
|
+
(!officialHasTurns || index > lastRepresentedRuntimeIndex));
|
|
71
|
+
const missingLocalTurns = absentLocalTurns.slice(-limit);
|
|
33
72
|
if (input.cursor === undefined &&
|
|
34
73
|
missingLocalTurns.length > 0 &&
|
|
35
74
|
official.turns.length + missingLocalTurns.length > limit) {
|
|
@@ -49,7 +88,7 @@ export class ConversationHistoryService {
|
|
|
49
88
|
pendingItemsByRunId.set(turn.runId, pendingItems);
|
|
50
89
|
}
|
|
51
90
|
const availableLocalSlots = Math.max(0, limit - officialTurns.length);
|
|
52
|
-
const localCandidates =
|
|
91
|
+
const localCandidates = input.cursor === undefined ? missingLocalTurns : [];
|
|
53
92
|
const localTurns = availableLocalSlots === 0 ? [] : localCandidates.slice(-availableLocalSlots);
|
|
54
93
|
const localRequestIds = new Set([
|
|
55
94
|
...localTurns.flatMap((turn) => turn.items.flatMap((item) => {
|
|
@@ -78,7 +117,7 @@ export class ConversationHistoryService {
|
|
|
78
117
|
})
|
|
79
118
|
.filter((turn) => turn.items.length > 0),
|
|
80
119
|
...localTurns
|
|
81
|
-
]
|
|
120
|
+
];
|
|
82
121
|
return this.page(official.nextCursor, turns, {
|
|
83
122
|
snapshotSequence,
|
|
84
123
|
source: 'official',
|
|
@@ -119,6 +158,86 @@ export class ConversationHistoryService {
|
|
|
119
158
|
const page = this.options.runtime.listConversationTurns({ sessionId: session.id, ...input });
|
|
120
159
|
return this.page(page.nextCursor, page.turns, { snapshotSequence, source: 'runtime', readAt });
|
|
121
160
|
}
|
|
161
|
+
async readManagedActive(session, input, cursor, snapshotSequence, readAt) {
|
|
162
|
+
const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
|
|
163
|
+
const runtimeTurns = this.options.runtime.listConversationTurns({
|
|
164
|
+
sessionId: session.id,
|
|
165
|
+
limit: 500
|
|
166
|
+
}).turns;
|
|
167
|
+
const beforeAt = cursor?.beforeAt ?? oldestTurnActivityAt(runtimeTurns);
|
|
168
|
+
if (cursor?.stage === 'OFFICIAL') {
|
|
169
|
+
const prefetched = this.takeActiveOfficialRead(session.id, limit, cursor.cursor);
|
|
170
|
+
const official = await (prefetched ??
|
|
171
|
+
this.readOfficial(session, {
|
|
172
|
+
...(cursor.cursor === null ? {} : { cursor: cursor.cursor }),
|
|
173
|
+
limit
|
|
174
|
+
}));
|
|
175
|
+
if (official === undefined)
|
|
176
|
+
return this.page(null, [], { snapshotSequence, source: 'runtime', readAt });
|
|
177
|
+
const turns = (await this.attachNativeImages(session, official.turns)).filter((turn) => latestTurnActivityAt(turn) < beforeAt);
|
|
178
|
+
return this.page(official.nextCursor === null
|
|
179
|
+
? null
|
|
180
|
+
: encodeActiveHistoryCursor({
|
|
181
|
+
sessionId: session.id,
|
|
182
|
+
stage: 'OFFICIAL',
|
|
183
|
+
beforeAt,
|
|
184
|
+
cursor: official.nextCursor
|
|
185
|
+
}), turns, { snapshotSequence, source: 'official', readAt });
|
|
186
|
+
}
|
|
187
|
+
if (cursor === undefined && session.externalSessionId !== null)
|
|
188
|
+
this.prefetchActiveOfficial(session, limit);
|
|
189
|
+
const runtimePage = this.options.runtime.listConversationTurns({
|
|
190
|
+
sessionId: session.id,
|
|
191
|
+
...(cursor?.cursor === null || cursor === undefined ? {} : { cursor: cursor.cursor }),
|
|
192
|
+
limit
|
|
193
|
+
});
|
|
194
|
+
const nextCursor = runtimePage.nextCursor !== null
|
|
195
|
+
? encodeActiveHistoryCursor({
|
|
196
|
+
sessionId: session.id,
|
|
197
|
+
stage: 'RUNTIME',
|
|
198
|
+
beforeAt,
|
|
199
|
+
cursor: runtimePage.nextCursor
|
|
200
|
+
})
|
|
201
|
+
: session.externalSessionId === null
|
|
202
|
+
? null
|
|
203
|
+
: encodeActiveHistoryCursor({
|
|
204
|
+
sessionId: session.id,
|
|
205
|
+
stage: 'OFFICIAL',
|
|
206
|
+
beforeAt,
|
|
207
|
+
cursor: null
|
|
208
|
+
});
|
|
209
|
+
return this.page(nextCursor, runtimePage.turns, {
|
|
210
|
+
snapshotSequence,
|
|
211
|
+
source: 'runtime',
|
|
212
|
+
readAt,
|
|
213
|
+
deferredOlderHistory: true
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
prefetchActiveOfficial(session, limit) {
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
for (const [sessionId, read] of this.activeOfficialReads)
|
|
219
|
+
if (read.createdAt + 60_000 <= now)
|
|
220
|
+
this.activeOfficialReads.delete(sessionId);
|
|
221
|
+
const current = this.activeOfficialReads.get(session.id);
|
|
222
|
+
if (current !== undefined && current.limit === limit)
|
|
223
|
+
return;
|
|
224
|
+
this.activeOfficialReads.set(session.id, {
|
|
225
|
+
createdAt: now,
|
|
226
|
+
limit,
|
|
227
|
+
// The active Run fast path must stay independent from an optional
|
|
228
|
+
// background reader failure until the user explicitly requests older history.
|
|
229
|
+
page: this.readOfficial(session, { limit }).catch(() => undefined)
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
takeActiveOfficialRead(sessionId, limit, cursor) {
|
|
233
|
+
if (cursor !== null)
|
|
234
|
+
return undefined;
|
|
235
|
+
const read = this.activeOfficialReads.get(sessionId);
|
|
236
|
+
if (read === undefined || read.limit !== limit || read.createdAt + 60_000 <= Date.now())
|
|
237
|
+
return undefined;
|
|
238
|
+
this.activeOfficialReads.delete(sessionId);
|
|
239
|
+
return read.page;
|
|
240
|
+
}
|
|
122
241
|
async refreshExternalActivity(session) {
|
|
123
242
|
const runner = this.options.runners.require(session.runner);
|
|
124
243
|
if (!runner.available(this.options.capabilities())) {
|
|
@@ -187,3 +306,56 @@ export class ConversationHistoryService {
|
|
|
187
306
|
};
|
|
188
307
|
}
|
|
189
308
|
}
|
|
309
|
+
const ACTIVE_HISTORY_CURSOR_PREFIX = 'mar-active-history:1:';
|
|
310
|
+
function encodeActiveHistoryCursor(cursor) {
|
|
311
|
+
return `${ACTIVE_HISTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString('base64url')}`;
|
|
312
|
+
}
|
|
313
|
+
function decodeActiveHistoryCursor(value, sessionId) {
|
|
314
|
+
if (value === undefined || !value.startsWith(ACTIVE_HISTORY_CURSOR_PREFIX))
|
|
315
|
+
return undefined;
|
|
316
|
+
try {
|
|
317
|
+
const decoded = JSON.parse(Buffer.from(value.slice(ACTIVE_HISTORY_CURSOR_PREFIX.length), 'base64url').toString('utf8'));
|
|
318
|
+
if (decoded.sessionId !== sessionId ||
|
|
319
|
+
(decoded.stage !== 'RUNTIME' && decoded.stage !== 'OFFICIAL') ||
|
|
320
|
+
typeof decoded.beforeAt !== 'number' ||
|
|
321
|
+
!Number.isFinite(decoded.beforeAt) ||
|
|
322
|
+
decoded.beforeAt < 0 ||
|
|
323
|
+
(decoded.cursor !== null && typeof decoded.cursor !== 'string'))
|
|
324
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
325
|
+
return decoded;
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
throw new Error('EVENT_CURSOR_INVALID');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function latestTurnActivityAt(turn) {
|
|
332
|
+
return Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0);
|
|
333
|
+
}
|
|
334
|
+
function oldestTurnActivityAt(turns) {
|
|
335
|
+
return turns.length === 0 ? 0 : Math.min(...turns.map(latestTurnActivityAt));
|
|
336
|
+
}
|
|
337
|
+
function sameConversationTurn(left, right) {
|
|
338
|
+
if (left.id === right.id)
|
|
339
|
+
return true;
|
|
340
|
+
if (left.runId !== null && left.runId === right.runId)
|
|
341
|
+
return true;
|
|
342
|
+
const leftMessageId = conversationClientMessageId(left);
|
|
343
|
+
return leftMessageId !== null && leftMessageId === conversationClientMessageId(right);
|
|
344
|
+
}
|
|
345
|
+
function conversationClientMessageId(turn) {
|
|
346
|
+
for (const item of turn.items) {
|
|
347
|
+
if (item.kind !== 'user_message' || !isPlainRecord(item.payload))
|
|
348
|
+
continue;
|
|
349
|
+
const value = item.payload.clientMessageId;
|
|
350
|
+
if (typeof value === 'string' && value.length > 0)
|
|
351
|
+
return value;
|
|
352
|
+
}
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
function sourceRank(source) {
|
|
356
|
+
if (source === 'official')
|
|
357
|
+
return 3;
|
|
358
|
+
if (source === 'native')
|
|
359
|
+
return 2;
|
|
360
|
+
return 1;
|
|
361
|
+
}
|
|
@@ -3,11 +3,11 @@ export function conversationSegments(turn) {
|
|
|
3
3
|
const groups = [];
|
|
4
4
|
let current = [];
|
|
5
5
|
for (const item of turn.items) {
|
|
6
|
+
if (current.at(-1)?.kind === 'assistant_message' && item.kind !== 'assistant_message') {
|
|
7
|
+
groups.push(current);
|
|
8
|
+
current = [];
|
|
9
|
+
}
|
|
6
10
|
current.push(item);
|
|
7
|
-
if (item.kind !== 'assistant_message')
|
|
8
|
-
continue;
|
|
9
|
-
groups.push(current);
|
|
10
|
-
current = [];
|
|
11
11
|
}
|
|
12
12
|
if (current.length > 0 || groups.length === 0)
|
|
13
13
|
groups.push(current);
|
|
@@ -16,6 +16,7 @@ export function conversationSegments(turn) {
|
|
|
16
16
|
const latest = index === groups.length - 1;
|
|
17
17
|
const projectedItems = items.map((item) => ({
|
|
18
18
|
...item,
|
|
19
|
+
runId: turn.runId,
|
|
19
20
|
segmentId: id,
|
|
20
21
|
segmentIndex: index
|
|
21
22
|
}));
|
|
@@ -36,14 +37,15 @@ export function conversationSegments(turn) {
|
|
|
36
37
|
export function conversationSegmentIdentityForItem(turn, itemId, itemKind) {
|
|
37
38
|
let index = 0;
|
|
38
39
|
const runtimeItemId = turn.runId === null ? itemId : `${turn.runId}:${itemId}`;
|
|
39
|
-
for (const item of turn.items) {
|
|
40
|
+
for (const [itemIndex, item] of turn.items.entries()) {
|
|
41
|
+
const previous = turn.items[itemIndex - 1];
|
|
42
|
+
if (previous?.kind === 'assistant_message' && item.kind !== 'assistant_message')
|
|
43
|
+
index += 1;
|
|
40
44
|
if (item.id === itemId || item.id === runtimeItemId)
|
|
41
45
|
return { id: `${turn.id}:segment:${index}`, index };
|
|
42
|
-
if (item.kind === 'assistant_message')
|
|
43
|
-
index += 1;
|
|
44
46
|
}
|
|
45
|
-
if (
|
|
46
|
-
index
|
|
47
|
+
if (turn.items.at(-1)?.kind === 'assistant_message' && itemKind !== 'assistant_message')
|
|
48
|
+
index += 1;
|
|
47
49
|
return { id: `${turn.id}:segment:${index}`, index };
|
|
48
50
|
}
|
|
49
51
|
export class ConversationSegmentService {
|
|
@@ -88,7 +90,10 @@ export class ConversationSegmentService {
|
|
|
88
90
|
snapshotSequence: page.snapshotSequence,
|
|
89
91
|
source: page.source,
|
|
90
92
|
readAt: page.readAt,
|
|
91
|
-
latestVisibleAt: page.latestVisibleAt
|
|
93
|
+
latestVisibleAt: page.latestVisibleAt,
|
|
94
|
+
...(boundary === undefined && page.deferredOlderHistory === true
|
|
95
|
+
? { deferredOlderHistory: true }
|
|
96
|
+
: {})
|
|
92
97
|
};
|
|
93
98
|
const pageSegments = page.turns.flatMap(conversationSegments);
|
|
94
99
|
let eligible = pageSegments;
|
|
@@ -107,6 +112,8 @@ export class ConversationSegmentService {
|
|
|
107
112
|
hasOlder = eligible.length > take.length || page.nextCursor !== null;
|
|
108
113
|
collected.unshift(...take);
|
|
109
114
|
}
|
|
115
|
+
if (boundary === undefined && page.deferredOlderHistory === true)
|
|
116
|
+
break;
|
|
110
117
|
if (collected.length >= limit || page.nextCursor === null)
|
|
111
118
|
break;
|
|
112
119
|
if (visited.has(page.nextCursor))
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const RESUME_FAILURE_TTL_MS = 30 * 60_000;
|
|
1
2
|
export class ExternalSessionResumeService {
|
|
2
3
|
options;
|
|
3
4
|
constructor(options) {
|
|
@@ -84,15 +85,23 @@ export class ExternalSessionResumeService {
|
|
|
84
85
|
: 'REJECTED';
|
|
85
86
|
}
|
|
86
87
|
clearFailure(sessionId) {
|
|
88
|
+
const timer = this.options.state.failureTimers.get(sessionId);
|
|
89
|
+
if (timer !== undefined)
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
this.options.state.failureTimers.delete(sessionId);
|
|
87
92
|
this.options.state.resumeFailures.delete(sessionId);
|
|
88
93
|
this.options.state.resumeFailureDetails.delete(sessionId);
|
|
89
94
|
this.options.state.messageResumeFailures.delete(sessionId);
|
|
90
95
|
}
|
|
91
96
|
setFailure(sessionId, code, detail) {
|
|
97
|
+
this.clearFailure(sessionId);
|
|
92
98
|
this.options.state.resumeFailures.set(sessionId, code);
|
|
93
99
|
if (detail === undefined)
|
|
94
100
|
this.options.state.resumeFailureDetails.delete(sessionId);
|
|
95
101
|
else
|
|
96
102
|
this.options.state.resumeFailureDetails.set(sessionId, detail);
|
|
103
|
+
const timer = setTimeout(() => this.clearFailure(sessionId), RESUME_FAILURE_TTL_MS);
|
|
104
|
+
timer.unref();
|
|
105
|
+
this.options.state.failureTimers.set(sessionId, timer);
|
|
97
106
|
}
|
|
98
107
|
}
|
|
@@ -17,7 +17,6 @@ interface FakeManagedRunServiceOptions {
|
|
|
17
17
|
readonly payload: unknown;
|
|
18
18
|
readonly expiresAt: number;
|
|
19
19
|
}) => void;
|
|
20
|
-
readonly captureSnapshot: (runId: string, cwd: string) => Promise<void>;
|
|
21
20
|
readonly disconnect: () => void;
|
|
22
21
|
}
|
|
23
22
|
/** Test-only managed Run adapter; production Runner paths never depend on it. */
|
|
@@ -32,8 +32,6 @@ export class FakeManagedRunService {
|
|
|
32
32
|
this.options.emitRun(runId, eventType, eventPayload, status);
|
|
33
33
|
if (status !== undefined && isTerminalRunStatus(status)) {
|
|
34
34
|
this.options.state.active.delete(runId);
|
|
35
|
-
if (cwd !== undefined)
|
|
36
|
-
void this.options.captureSnapshot(runId, cwd);
|
|
37
35
|
}
|
|
38
36
|
},
|
|
39
37
|
approval: (approvalId, approvalPayload, expiresAt) => {
|
|
@@ -238,7 +238,12 @@ export class NativeSessionProjectionService {
|
|
|
238
238
|
this.options.state.externalActivity.delete(directId);
|
|
239
239
|
this.options.state.resumedByDirectId.delete(directId);
|
|
240
240
|
this.options.state.resumeFailures.delete(directId);
|
|
241
|
+
this.options.state.messageResumeFailures.delete(directId);
|
|
241
242
|
this.options.state.resumeFailureDetails.delete(directId);
|
|
243
|
+
const timer = this.options.state.failureTimers.get(directId);
|
|
244
|
+
if (timer !== undefined)
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
this.options.state.failureTimers.delete(directId);
|
|
242
247
|
}
|
|
243
248
|
}
|
|
244
249
|
for (const [directId, resumedId] of this.options.state.resumedByDirectId)
|
|
@@ -2,18 +2,24 @@ import type { NodeAgentSession } from '../database.js';
|
|
|
2
2
|
import type { NodeOperationHandler } from '../connector/node-operation-router.js';
|
|
3
3
|
export interface NativeSessionWatchOptions<TPage extends {
|
|
4
4
|
readonly turns: readonly unknown[];
|
|
5
|
+
readonly source?: unknown;
|
|
5
6
|
}> {
|
|
6
7
|
readonly resolve: (sessionId: string) => Promise<NodeAgentSession | undefined>;
|
|
7
8
|
readonly managedActive: (session: NodeAgentSession) => boolean;
|
|
8
9
|
readonly refreshActivity: (session: NodeAgentSession) => Promise<void>;
|
|
9
10
|
readonly present: (session: NodeAgentSession) => unknown;
|
|
10
11
|
readonly readPage: (session: NodeAgentSession, limit: number) => Promise<TPage>;
|
|
12
|
+
readonly convergePage: (previous: TPage | undefined, previousObserved: TPage | undefined, next: TPage, previousAuthority: unknown) => {
|
|
13
|
+
readonly page: TPage;
|
|
14
|
+
readonly authority: unknown;
|
|
15
|
+
};
|
|
11
16
|
readonly projectInitialPage?: (session: NodeAgentSession, page: TPage, unit: 'SEGMENT', limit: number) => unknown;
|
|
12
17
|
readonly emitSession: (session: unknown) => void;
|
|
13
|
-
readonly
|
|
18
|
+
readonly emitPage: (session: NodeAgentSession, page: TPage) => void;
|
|
14
19
|
}
|
|
15
20
|
export declare class NativeSessionWatchService<TPage extends {
|
|
16
21
|
readonly turns: readonly unknown[];
|
|
22
|
+
readonly source?: unknown;
|
|
17
23
|
}> {
|
|
18
24
|
private readonly options;
|
|
19
25
|
private readonly watches;
|
|
@@ -33,6 +39,6 @@ export declare class NativeSessionWatchService<TPage extends {
|
|
|
33
39
|
readonly expiresAt: number;
|
|
34
40
|
} | undefined;
|
|
35
41
|
get size(): number;
|
|
36
|
-
refresh(sessionId: string, limit?: number): Promise<TPage | undefined>;
|
|
42
|
+
refresh(sessionId: string, limit?: number, emit?: boolean): Promise<TPage | undefined>;
|
|
37
43
|
private performRefresh;
|
|
38
44
|
}
|