@myagentroam/node 0.9.7 → 0.9.9
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/connector.d.ts +1 -0
- package/dist/connector.js +6 -2
- package/dist/runner/codex/conversation-parser.d.ts +1 -1
- package/dist/runner/codex/conversation-parser.js +25 -1
- package/dist/runner/codex/managed-run-controller.d.ts +1 -1
- package/dist/runner/codex/managed-run-controller.js +7 -1
- package/dist/runner/opencode/conversation-parser.js +5 -1
- package/dist/runtime-state.d.ts +3 -1
- package/dist/runtime-state.js +31 -9
- package/dist/service/node-connection-lifecycle-service.d.ts +1 -0
- package/dist/service/node-connection-lifecycle-service.js +1 -0
- package/dist/service/recent-run-projection-service.d.ts +14 -0
- package/dist/service/recent-run-projection-service.js +78 -0
- package/dist/service/run-event-service.js +10 -5
- package/dist/util/runner-native-session-parsers.d.ts +2 -1
- package/dist/util/runner-native-session-parsers.js +28 -22
- package/package.json +2 -2
package/dist/connector.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare class NodeConnector {
|
|
|
68
68
|
private readonly workspaceChangeService;
|
|
69
69
|
private readonly workspaceWorkbenchService;
|
|
70
70
|
private database;
|
|
71
|
+
private readonly recentRunProjectionService;
|
|
71
72
|
/** Runtime/session state is intentionally not persisted to Node SQLite. */
|
|
72
73
|
private readonly runtime;
|
|
73
74
|
private readonly metrics;
|
package/dist/connector.js
CHANGED
|
@@ -30,6 +30,7 @@ import { WorkspaceChangeService } from './service/workspace-change-service.js';
|
|
|
30
30
|
import { WorkspaceSessionService } from './service/workspace-session-service.js';
|
|
31
31
|
import { WorkbenchEventService } from './service/workbench-event-service.js';
|
|
32
32
|
import { RunDomainState } from './service/run-domain-state.js';
|
|
33
|
+
import { RecentRunProjectionService } from './service/recent-run-projection-service.js';
|
|
33
34
|
import { SessionDomainState } from './service/session-domain-state.js';
|
|
34
35
|
import { RunEventService } from './service/run-event-service.js';
|
|
35
36
|
import { RunAttachmentService } from './service/run-attachment-service.js';
|
|
@@ -166,8 +167,9 @@ export class NodeConnector {
|
|
|
166
167
|
workspaceChangeService = new WorkspaceChangeService(this.workspaceState);
|
|
167
168
|
workspaceWorkbenchService;
|
|
168
169
|
database;
|
|
170
|
+
recentRunProjectionService = new RecentRunProjectionService((message) => this.send('recent-runs', message));
|
|
169
171
|
/** Runtime/session state is intentionally not persisted to Node SQLite. */
|
|
170
|
-
runtime = new NodeRuntimeState(() => this.config?.nodeId ?? 'runtime-node', (workspaceId) => this.database?.getWorkspace(workspaceId));
|
|
172
|
+
runtime = new NodeRuntimeState(() => this.config?.nodeId ?? 'runtime-node', (workspaceId) => this.database?.getWorkspace(workspaceId), Date.now, (run, session) => this.recentRunProjectionService.record(run, session));
|
|
171
173
|
metrics = new NodeMetrics();
|
|
172
174
|
runEventService = new RunEventService({
|
|
173
175
|
runtime: this.runtime,
|
|
@@ -684,7 +686,8 @@ export class NodeConnector {
|
|
|
684
686
|
terminal: this.terminalChannel,
|
|
685
687
|
metrics: this.metrics,
|
|
686
688
|
heartbeatMs: HEARTBEAT_MS,
|
|
687
|
-
refreshMs: CAPABILITY_REFRESH_MS
|
|
689
|
+
refreshMs: CAPABILITY_REFRESH_MS,
|
|
690
|
+
onConnected: () => this.recentRunProjectionService.publishSnapshot()
|
|
688
691
|
});
|
|
689
692
|
const requireDatabase = () => {
|
|
690
693
|
if (this.database === undefined)
|
|
@@ -763,6 +766,7 @@ export class NodeConnector {
|
|
|
763
766
|
}
|
|
764
767
|
stop() {
|
|
765
768
|
this.stopped = true;
|
|
769
|
+
this.recentRunProjectionService.dispose();
|
|
766
770
|
this.upgradeCoordinator.stop();
|
|
767
771
|
this.connectionLifecycle.stop();
|
|
768
772
|
for (const approval of this.claudeClient.execution.approvals.values()) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { NodeAgentSession, NodeConversationItem, NodeConversationTurn, NodeMessageAttachmentInput } from '../../database.js';
|
|
2
2
|
export type PublicConversationItem = {
|
|
3
3
|
readonly itemId: string;
|
|
4
|
-
readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
|
|
4
|
+
readonly kind: 'assistant_message' | 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
|
|
5
5
|
readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
|
|
6
6
|
readonly payload: Record<string, unknown>;
|
|
7
7
|
readonly merge?: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const MAX_COMPOSER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
2
2
|
const MAX_COMPOSER_ATTACHMENTS_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
const MAX_ASSISTANT_MESSAGE_CHARACTERS = 64 * 1024;
|
|
3
4
|
export * from '../../util/runner-native-session-parsers.js';
|
|
4
5
|
export function codexOfficialConversationPage(session, result, input, managedTurnForNative) {
|
|
5
6
|
if (!isPlainRecord(result) || !isPlainRecord(result.thread))
|
|
@@ -43,7 +44,7 @@ export function codexOfficialTurn(session, value, sequence, managedTurn) {
|
|
|
43
44
|
const officialNativeItemIds = codexNativeItemIds(value.items);
|
|
44
45
|
const officialAssistantCount = items.filter((item) => item.kind === 'assistant_message').length;
|
|
45
46
|
const officialAssistantMessage = officialAssistantCount > 0;
|
|
46
|
-
const managedAssistants = managedTurn.items.filter((item) => item.kind === 'assistant_message');
|
|
47
|
+
const managedAssistants = managedTurn.items.filter((item) => item.kind === 'assistant_message' && conversationItemNativeId(item.id) === undefined);
|
|
47
48
|
const managedAssistantCandidates = officialAssistantCount === 0 ? [] : managedAssistants.slice(-officialAssistantCount);
|
|
48
49
|
const assistantLength = (values) => values.reduce((length, item) => {
|
|
49
50
|
if (item.kind !== 'assistant_message' || !isPlainRecord(item.payload))
|
|
@@ -108,6 +109,15 @@ export function codexOfficialTurn(session, value, sequence, managedTurn) {
|
|
|
108
109
|
replacement = officialAssistants.find((item) => !consumedOfficialIds.has(item.id));
|
|
109
110
|
}
|
|
110
111
|
if (replacement !== undefined) {
|
|
112
|
+
if (managedItem.kind === 'assistant_message' &&
|
|
113
|
+
replacement.kind === 'assistant_message' &&
|
|
114
|
+
(assistantLength([managedItem]) > assistantLength([replacement]) ||
|
|
115
|
+
((managedTurn.completedAt ?? 0) > (completedAt ?? 0) &&
|
|
116
|
+
assistantLength([managedItem]) > 0))) {
|
|
117
|
+
reconciledItems.push(managedItem);
|
|
118
|
+
consumedOfficialIds.add(replacement.id);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
111
121
|
if (!consumedOfficialIds.has(replacement.id)) {
|
|
112
122
|
reconciledItems.push(replacement);
|
|
113
123
|
consumedOfficialIds.add(replacement.id);
|
|
@@ -504,6 +514,20 @@ export function codexLiveConversationItem(value, completed, imageAttachments = [
|
|
|
504
514
|
status: completed ? 'COMPLETED' : 'IN_PROGRESS',
|
|
505
515
|
payload: { explanation: compactRunnerText(planText, 20_000), steps: [] }
|
|
506
516
|
};
|
|
517
|
+
if (value.type === 'agentMessage') {
|
|
518
|
+
if (!completed || typeof value.text !== 'string' || value.text.length === 0)
|
|
519
|
+
return undefined;
|
|
520
|
+
return {
|
|
521
|
+
itemId,
|
|
522
|
+
kind: 'assistant_message',
|
|
523
|
+
status: 'COMPLETED',
|
|
524
|
+
payload: {
|
|
525
|
+
text: compactRunnerText(value.text, MAX_ASSISTANT_MESSAGE_CHARACTERS),
|
|
526
|
+
format: 'markdown',
|
|
527
|
+
model: null
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
507
531
|
if (value.type === 'commandExecution') {
|
|
508
532
|
const output = typeof value.aggregatedOutput === 'string' ? value.aggregatedOutput : '';
|
|
509
533
|
return {
|
|
@@ -53,7 +53,7 @@ export interface CodexManagedRunHost<TAttachment> {
|
|
|
53
53
|
}): void;
|
|
54
54
|
emitItem(runId: string, item: {
|
|
55
55
|
readonly itemId: string;
|
|
56
|
-
readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
|
|
56
|
+
readonly kind: 'reasoning_summary' | 'tool_call' | 'command_execution' | 'user_input_request' | 'assistant_message' | 'plan' | 'context_compaction' | 'file_change' | 'error' | 'unknown';
|
|
57
57
|
readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED' | 'DECLINED';
|
|
58
58
|
readonly payload: Record<string, unknown>;
|
|
59
59
|
readonly merge?: boolean;
|
|
@@ -155,7 +155,13 @@ export class CodexManagedRunController {
|
|
|
155
155
|
return;
|
|
156
156
|
}
|
|
157
157
|
if (notification.method === 'item/agentMessage/delta' && typeof params.delta === 'string') {
|
|
158
|
-
|
|
158
|
+
const itemId = typeof params.itemId === 'string'
|
|
159
|
+
? boundedConversationItemId('codex', params.itemId)
|
|
160
|
+
: undefined;
|
|
161
|
+
this.host.emit(runId, 'text.delta', {
|
|
162
|
+
text: params.delta,
|
|
163
|
+
...(itemId === undefined ? {} : { itemId })
|
|
164
|
+
});
|
|
159
165
|
return;
|
|
160
166
|
}
|
|
161
167
|
if (notification.method === 'item/reasoning/summaryTextDelta' &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../codex/conversation-parser.js';
|
|
2
|
+
import { runtimeAssistantProjectionMoreComplete } from '../../util/runner-native-session-parsers.js';
|
|
2
3
|
export function openCodeConversationPage(session, messages, input, managedTurn) {
|
|
3
4
|
const records = messages.flatMap((value) => {
|
|
4
5
|
const entry = record(value);
|
|
@@ -63,6 +64,9 @@ function openCodeTurn(session, user, userParts, assistants, sequence, managed) {
|
|
|
63
64
|
const completedAt = Math.max(startedAt ?? 0, ...assistants.map(({ info }) => number(record(info.time)?.completed) ?? 0)) || startedAt;
|
|
64
65
|
const id = managed?.id ?? `${session.id}:official:${nativeTurnId}`;
|
|
65
66
|
const runId = managed?.runId ?? null;
|
|
67
|
+
const projectedItems = runtimeAssistantProjectionMoreComplete(managed, items)
|
|
68
|
+
? managed.items
|
|
69
|
+
: items;
|
|
66
70
|
return {
|
|
67
71
|
id,
|
|
68
72
|
sessionId: session.id,
|
|
@@ -72,7 +76,7 @@ function openCodeTurn(session, user, userParts, assistants, sequence, managed) {
|
|
|
72
76
|
model: assistants.find(({ info }) => typeof info.modelID === 'string')?.info.modelID ?? null,
|
|
73
77
|
effort: managed?.effort ?? null,
|
|
74
78
|
access: managed?.access ?? null,
|
|
75
|
-
items:
|
|
79
|
+
items: projectedItems.map((value) => ({ ...value, turnId: id, runId })),
|
|
76
80
|
startedAt,
|
|
77
81
|
completedAt
|
|
78
82
|
};
|
package/dist/runtime-state.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare class NodeRuntimeState {
|
|
|
10
10
|
private readonly nodeId;
|
|
11
11
|
private readonly getWorkspace;
|
|
12
12
|
private readonly now;
|
|
13
|
+
private readonly onRunChanged?;
|
|
14
|
+
private static readonly MAX_RUN_EVENTS_PER_RUN;
|
|
13
15
|
private static readonly MAX_COMPLETED_TURNS_PER_SESSION;
|
|
14
16
|
private readonly sessions;
|
|
15
17
|
private readonly runs;
|
|
@@ -26,7 +28,7 @@ export declare class NodeRuntimeState {
|
|
|
26
28
|
/** Bounded tombstones reject stale internal start envelopes after terminal cleanup. */
|
|
27
29
|
private readonly terminalRunIds;
|
|
28
30
|
private nextEventId;
|
|
29
|
-
constructor(nodeId: () => string, getWorkspace: (id: string) => NodeWorkspace | undefined, now?: () => number);
|
|
31
|
+
constructor(nodeId: () => string, getWorkspace: (id: string) => NodeWorkspace | undefined, now?: () => number, onRunChanged?: ((run: NodeAgentRun, session: NodeAgentSession) => void) | undefined);
|
|
30
32
|
activeWorkspaceLeaseCount(): number;
|
|
31
33
|
activeSessionRun(sessionId: string): NodeAgentRun | undefined;
|
|
32
34
|
hasActiveSessionLease(sessionId: string): boolean;
|
package/dist/runtime-state.js
CHANGED
|
@@ -9,6 +9,8 @@ export class NodeRuntimeState {
|
|
|
9
9
|
nodeId;
|
|
10
10
|
getWorkspace;
|
|
11
11
|
now;
|
|
12
|
+
onRunChanged;
|
|
13
|
+
static MAX_RUN_EVENTS_PER_RUN = 100;
|
|
12
14
|
static MAX_COMPLETED_TURNS_PER_SESSION = 10;
|
|
13
15
|
sessions = new Map();
|
|
14
16
|
runs = new Map();
|
|
@@ -25,10 +27,11 @@ export class NodeRuntimeState {
|
|
|
25
27
|
/** Bounded tombstones reject stale internal start envelopes after terminal cleanup. */
|
|
26
28
|
terminalRunIds = new Set();
|
|
27
29
|
nextEventId = 1;
|
|
28
|
-
constructor(nodeId, getWorkspace, now = Date.now) {
|
|
30
|
+
constructor(nodeId, getWorkspace, now = Date.now, onRunChanged) {
|
|
29
31
|
this.nodeId = nodeId;
|
|
30
32
|
this.getWorkspace = getWorkspace;
|
|
31
33
|
this.now = now;
|
|
34
|
+
this.onRunChanged = onRunChanged;
|
|
32
35
|
}
|
|
33
36
|
activeWorkspaceLeaseCount() {
|
|
34
37
|
return this.leases.size;
|
|
@@ -118,6 +121,7 @@ export class NodeRuntimeState {
|
|
|
118
121
|
};
|
|
119
122
|
this.runs.set(run.id, run);
|
|
120
123
|
this.leases.set(run.sessionId, run.id);
|
|
124
|
+
this.onRunChanged?.(run, session);
|
|
121
125
|
return run;
|
|
122
126
|
}
|
|
123
127
|
getAgentSession(id) {
|
|
@@ -322,6 +326,7 @@ export class NodeRuntimeState {
|
|
|
322
326
|
this.bumpQueueVersion(session.id);
|
|
323
327
|
this.turns.set(session.id, [...(this.turns.get(session.id) ?? []), fixedTurn]);
|
|
324
328
|
this.replaceSession(session.id, { lastActivityAt: now });
|
|
329
|
+
this.onRunChanged?.(run, this.requireSession(session.id));
|
|
325
330
|
return { run, created: true };
|
|
326
331
|
}
|
|
327
332
|
sessionQueue(sessionId) {
|
|
@@ -434,6 +439,7 @@ export class NodeRuntimeState {
|
|
|
434
439
|
throw new Error('RUN_STATE_INVALID');
|
|
435
440
|
const value = { ...current, status: next, version: current.version + 1, updatedAt: this.now() };
|
|
436
441
|
this.runs.set(id, value);
|
|
442
|
+
this.onRunChanged?.(value, this.requireSession(value.sessionId));
|
|
437
443
|
if (terminal(next) && this.leases.get(value.sessionId) === value.id)
|
|
438
444
|
this.leases.delete(value.sessionId);
|
|
439
445
|
this.updateTurnForRun(id, (turn) => ({
|
|
@@ -464,7 +470,7 @@ export class NodeRuntimeState {
|
|
|
464
470
|
...(input.status === undefined ? {} : { status: input.status }),
|
|
465
471
|
createdAt: this.now()
|
|
466
472
|
};
|
|
467
|
-
this.events.set(input.runId, [...prior, value]);
|
|
473
|
+
this.events.set(input.runId, [...prior, value].slice(-NodeRuntimeState.MAX_RUN_EVENTS_PER_RUN));
|
|
468
474
|
if (input.status !== undefined)
|
|
469
475
|
this.transitionRun(input.runId, input.status);
|
|
470
476
|
this.projectEvent(value);
|
|
@@ -630,7 +636,13 @@ export class NodeRuntimeState {
|
|
|
630
636
|
}
|
|
631
637
|
this.runs.delete(runId);
|
|
632
638
|
this.events.delete(runId);
|
|
633
|
-
const turns = this.turns.get(run.sessionId) ?? []
|
|
639
|
+
const turns = (this.turns.get(run.sessionId) ?? []).filter((turn) => {
|
|
640
|
+
const hasVisibleError = turn.items.some((item) => item.kind === 'error');
|
|
641
|
+
if (turn.runId === runId)
|
|
642
|
+
return run.status === 'SUCCEEDED' || run.status === 'INTERRUPTED' || hasVisibleError;
|
|
643
|
+
const isTransientErrorTurn = hasVisibleError && turn.status !== 'SUCCEEDED' && turn.status !== 'INTERRUPTED';
|
|
644
|
+
return run.status !== 'SUCCEEDED' || !isTransientErrorTurn;
|
|
645
|
+
});
|
|
634
646
|
const completed = turns.filter((turn) => terminalTurn(turn.status));
|
|
635
647
|
const retainedCompleted = new Set(completed.slice(-NodeRuntimeState.MAX_COMPLETED_TURNS_PER_SESSION).map((turn) => turn.id));
|
|
636
648
|
this.turns.set(run.sessionId, turns.filter((turn) => !terminalTurn(turn.status) || retainedCompleted.has(turn.id)));
|
|
@@ -708,6 +720,9 @@ export class NodeRuntimeState {
|
|
|
708
720
|
const value = this.requireSession(id);
|
|
709
721
|
const next = { ...value, ...patch };
|
|
710
722
|
this.sessions.set(id, next);
|
|
723
|
+
if (value.customTitle !== next.customTitle || value.runnerTitle !== next.runnerTitle)
|
|
724
|
+
for (const run of this.listRunsForSession(id))
|
|
725
|
+
this.onRunChanged?.(run, next);
|
|
711
726
|
return next;
|
|
712
727
|
}
|
|
713
728
|
bumpQueueVersion(sessionId) {
|
|
@@ -742,13 +757,19 @@ export class NodeRuntimeState {
|
|
|
742
757
|
if (event.eventType === 'text.delta' &&
|
|
743
758
|
isRecord(event.payload) &&
|
|
744
759
|
typeof event.payload.text === 'string') {
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
? lastItem
|
|
760
|
+
const nativeItemId = typeof event.payload.itemId === 'string' && event.payload.itemId.length <= 88
|
|
761
|
+
? event.payload.itemId
|
|
748
762
|
: undefined;
|
|
763
|
+
const stableItemId = nativeItemId === undefined ? undefined : `${event.runId}:${nativeItemId}`;
|
|
764
|
+
const lastItem = turn.items.at(-1);
|
|
765
|
+
const existing = stableItemId === undefined
|
|
766
|
+
? lastItem?.kind === 'assistant_message' && activeConversationItem(lastItem.status)
|
|
767
|
+
? lastItem
|
|
768
|
+
: undefined
|
|
769
|
+
: turn.items.find((item) => item.id === stableItemId && item.kind === 'assistant_message');
|
|
749
770
|
const text = `${existing === undefined ? '' : readableText(existing.payload)}${event.payload.text}`;
|
|
750
771
|
const item = {
|
|
751
|
-
id: existing?.id ?? `${event.runId}:assistant:${event.sequence}`,
|
|
772
|
+
id: existing?.id ?? stableItemId ?? `${event.runId}:assistant:${event.sequence}`,
|
|
752
773
|
sessionId: run.sessionId,
|
|
753
774
|
turnId: turn.id,
|
|
754
775
|
runId: event.runId,
|
|
@@ -829,7 +850,7 @@ function terminal(status) {
|
|
|
829
850
|
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
830
851
|
}
|
|
831
852
|
function terminalTurn(status) {
|
|
832
|
-
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(status);
|
|
853
|
+
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
|
|
833
854
|
}
|
|
834
855
|
function transitionAllowed(from, to) {
|
|
835
856
|
return !terminal(from) || from === to;
|
|
@@ -869,7 +890,8 @@ function conversationItemFromEvent(event) {
|
|
|
869
890
|
};
|
|
870
891
|
}
|
|
871
892
|
function projectableConversationKind(value) {
|
|
872
|
-
return (value === '
|
|
893
|
+
return (value === 'assistant_message' ||
|
|
894
|
+
value === 'reasoning_summary' ||
|
|
873
895
|
value === 'tool_call' ||
|
|
874
896
|
value === 'command_execution' ||
|
|
875
897
|
value === 'user_input_request' ||
|
|
@@ -17,6 +17,7 @@ interface NodeConnectionLifecycleServiceOptions {
|
|
|
17
17
|
readonly metrics: NodeMetrics;
|
|
18
18
|
readonly heartbeatMs: number;
|
|
19
19
|
readonly refreshMs: number;
|
|
20
|
+
readonly onConnected?: () => void;
|
|
20
21
|
}
|
|
21
22
|
/** Owns registration completion, heartbeat and background capability refresh timers. */
|
|
22
23
|
export declare class NodeConnectionLifecycleService {
|
|
@@ -47,6 +47,7 @@ export class NodeConnectionLifecycleService {
|
|
|
47
47
|
this.options.control.send('capabilities', this.options.capabilities());
|
|
48
48
|
this.options.terminal.connect();
|
|
49
49
|
this.options.control.send('heartbeat', {});
|
|
50
|
+
this.options.onConnected?.();
|
|
50
51
|
this.options.metrics.heartbeatSent();
|
|
51
52
|
this.heartbeat = setInterval(() => {
|
|
52
53
|
this.options.metrics.heartbeatSent();
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NodeAgentRun, NodeAgentSession } from '../database.js';
|
|
2
|
+
import type { RecentRunProjection, RecentRunProjectionMessage } from '@myagentroam/protocol';
|
|
3
|
+
export declare class RecentRunProjectionService {
|
|
4
|
+
private readonly emit;
|
|
5
|
+
private readonly now;
|
|
6
|
+
private readonly runs;
|
|
7
|
+
private readonly cleanupTimer;
|
|
8
|
+
constructor(emit: (message: RecentRunProjectionMessage) => void, now?: () => number);
|
|
9
|
+
record(run: NodeAgentRun, session: NodeAgentSession): void;
|
|
10
|
+
snapshot(): readonly RecentRunProjection[];
|
|
11
|
+
publishSnapshot(): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
private cleanup;
|
|
14
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const TERMINAL_TTL_MS = 12 * 60 * 60_000;
|
|
2
|
+
const CLEANUP_INTERVAL_MS = 5 * 60_000;
|
|
3
|
+
const TERMINAL_LIMIT = 100;
|
|
4
|
+
export class RecentRunProjectionService {
|
|
5
|
+
emit;
|
|
6
|
+
now;
|
|
7
|
+
runs = new Map();
|
|
8
|
+
cleanupTimer;
|
|
9
|
+
constructor(emit, now = Date.now) {
|
|
10
|
+
this.emit = emit;
|
|
11
|
+
this.now = now;
|
|
12
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
|
|
13
|
+
this.cleanupTimer.unref();
|
|
14
|
+
}
|
|
15
|
+
record(run, session) {
|
|
16
|
+
if (!Number.isSafeInteger(run.initiatedByUserId) || run.initiatedByUserId === undefined)
|
|
17
|
+
return;
|
|
18
|
+
const existing = this.runs.get(run.id);
|
|
19
|
+
if (existing !== undefined && existing.version > run.version)
|
|
20
|
+
return;
|
|
21
|
+
const startedAt = existing?.startedAt ?? (run.status === 'QUEUED' ? null : run.updatedAt);
|
|
22
|
+
const completedAt = terminal(run.status) ? (existing?.completedAt ?? run.updatedAt) : null;
|
|
23
|
+
const projection = {
|
|
24
|
+
runId: run.id,
|
|
25
|
+
nodeId: run.nodeId,
|
|
26
|
+
workspaceId: run.workspaceId,
|
|
27
|
+
sessionId: run.sessionId,
|
|
28
|
+
sessionTitle: session.customTitle ?? session.runnerTitle ?? session.id,
|
|
29
|
+
runner: run.runner,
|
|
30
|
+
status: run.status,
|
|
31
|
+
createdAt: run.createdAt,
|
|
32
|
+
startedAt,
|
|
33
|
+
completedAt,
|
|
34
|
+
sortAt: startedAt ?? run.createdAt,
|
|
35
|
+
updatedAt: run.updatedAt,
|
|
36
|
+
version: run.version,
|
|
37
|
+
userId: run.initiatedByUserId
|
|
38
|
+
};
|
|
39
|
+
this.runs.set(run.id, projection);
|
|
40
|
+
this.cleanup();
|
|
41
|
+
this.emit({ kind: 'UPDATE', run: projection });
|
|
42
|
+
}
|
|
43
|
+
snapshot() {
|
|
44
|
+
this.cleanup();
|
|
45
|
+
return [...this.runs.values()].sort(compareRecentRuns);
|
|
46
|
+
}
|
|
47
|
+
publishSnapshot() {
|
|
48
|
+
this.emit({ kind: 'SNAPSHOT', runs: [...this.snapshot()] });
|
|
49
|
+
}
|
|
50
|
+
dispose() {
|
|
51
|
+
clearInterval(this.cleanupTimer);
|
|
52
|
+
}
|
|
53
|
+
cleanup() {
|
|
54
|
+
const cutoff = this.now() - TERMINAL_TTL_MS;
|
|
55
|
+
for (const [runId, run] of this.runs)
|
|
56
|
+
if (run.completedAt !== null && run.completedAt <= cutoff)
|
|
57
|
+
this.runs.delete(runId);
|
|
58
|
+
const byUser = new Map();
|
|
59
|
+
for (const run of this.runs.values()) {
|
|
60
|
+
if (run.completedAt === null)
|
|
61
|
+
continue;
|
|
62
|
+
const values = byUser.get(run.userId) ?? [];
|
|
63
|
+
values.push(run);
|
|
64
|
+
byUser.set(run.userId, values);
|
|
65
|
+
}
|
|
66
|
+
for (const values of byUser.values())
|
|
67
|
+
for (const run of values.sort(compareRecentRuns).slice(TERMINAL_LIMIT))
|
|
68
|
+
this.runs.delete(run.runId);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function compareRecentRuns(left, right) {
|
|
72
|
+
return (right.sortAt - left.sortAt ||
|
|
73
|
+
right.createdAt - left.createdAt ||
|
|
74
|
+
right.runId.localeCompare(left.runId));
|
|
75
|
+
}
|
|
76
|
+
function terminal(status) {
|
|
77
|
+
return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(status);
|
|
78
|
+
}
|
|
@@ -14,7 +14,7 @@ export class RunEventService {
|
|
|
14
14
|
status === undefined &&
|
|
15
15
|
isPlainRecord(payload) &&
|
|
16
16
|
typeof payload.text === 'string') {
|
|
17
|
-
this.enqueueText(runId, payload.text);
|
|
17
|
+
this.enqueueText(runId, payload.text, typeof payload.itemId === 'string' ? payload.itemId : undefined);
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
this.flushText(runId);
|
|
@@ -28,18 +28,20 @@ export class RunEventService {
|
|
|
28
28
|
flushPendingText(runId) {
|
|
29
29
|
this.flushText(runId);
|
|
30
30
|
}
|
|
31
|
-
enqueueText(runId, text) {
|
|
31
|
+
enqueueText(runId, text, itemId) {
|
|
32
32
|
if (text.length === 0)
|
|
33
33
|
return;
|
|
34
34
|
const pending = this.pendingText.get(runId);
|
|
35
|
-
if (pending !== undefined) {
|
|
35
|
+
if (pending !== undefined && pending.itemId === itemId) {
|
|
36
36
|
pending.text += text;
|
|
37
37
|
if (pending.text.length >= MAX_BATCH_CHARS)
|
|
38
38
|
this.flushText(runId);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
|
+
if (pending !== undefined)
|
|
42
|
+
this.flushText(runId);
|
|
41
43
|
const timer = setTimeout(() => this.flushText(runId), COALESCE_MS);
|
|
42
|
-
this.pendingText.set(runId, { text, timer });
|
|
44
|
+
this.pendingText.set(runId, { text, ...(itemId === undefined ? {} : { itemId }), timer });
|
|
43
45
|
}
|
|
44
46
|
flushText(runId) {
|
|
45
47
|
const pending = this.pendingText.get(runId);
|
|
@@ -47,7 +49,10 @@ export class RunEventService {
|
|
|
47
49
|
return;
|
|
48
50
|
clearTimeout(pending.timer);
|
|
49
51
|
this.pendingText.delete(runId);
|
|
50
|
-
this.emitNow(runId, 'text.delta', {
|
|
52
|
+
this.emitNow(runId, 'text.delta', {
|
|
53
|
+
text: pending.text,
|
|
54
|
+
...(pending.itemId === undefined ? {} : { itemId: pending.itemId })
|
|
55
|
+
});
|
|
51
56
|
}
|
|
52
57
|
emitNow(runId, eventType, payload, status) {
|
|
53
58
|
if (this.options.runtime.getRun(runId) === undefined)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionActivityState } from '@myagentroam/protocol';
|
|
2
|
-
import type { NodeAgentSession, NodeConversationTurn } from '../database.js';
|
|
2
|
+
import type { NodeAgentSession, NodeConversationItem, NodeConversationTurn } from '../database.js';
|
|
3
3
|
import type { NativeSessionHistory, NativeTranscriptImageAttachment } from '../native-session-history.js';
|
|
4
4
|
export interface SessionContextUsage {
|
|
5
5
|
readonly model: string | null;
|
|
@@ -46,6 +46,7 @@ export declare function nativeConversationPage(session: NodeAgentSession, histor
|
|
|
46
46
|
readonly turns: readonly NodeConversationTurn[];
|
|
47
47
|
readonly nextCursor: string | null;
|
|
48
48
|
};
|
|
49
|
+
export declare function runtimeAssistantProjectionMoreComplete(runtimeTurn: NodeConversationTurn | undefined, projectedItems: readonly NodeConversationItem[]): boolean;
|
|
49
50
|
/**
|
|
50
51
|
* Claude JSONL does not retain browser request IDs. While the Node process is alive, match current
|
|
51
52
|
* runtime user messages by content and creation time so native history replaces the optimistic Turn.
|
|
@@ -156,19 +156,6 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
156
156
|
const effectiveCursor = history.cursorReset === true ? undefined : input.cursor;
|
|
157
157
|
const nativeTurns = nativeTranscriptTurns(history.items);
|
|
158
158
|
const nativeTurnIds = nativeTranscriptTurnIds(session.id, nativeTurns);
|
|
159
|
-
const representedClientMessageIds = new Set(nativeTurns.flatMap((entries) => entries.flatMap(({ entry, index }) => {
|
|
160
|
-
if (entry.kind !== 'message' || entry.role !== 'USER')
|
|
161
|
-
return [];
|
|
162
|
-
const clientMessageId = nativeUserClientMessageId(entry, entry.createdAt ?? index, runtimeTurns);
|
|
163
|
-
return clientMessageId === null ? [] : [clientMessageId];
|
|
164
|
-
})));
|
|
165
|
-
const missingRuntimeErrorTurns = effectiveCursor === undefined
|
|
166
|
-
? runtimeTurns.filter((turn) => {
|
|
167
|
-
const clientMessageId = runtimeTurnClientMessageId(turn);
|
|
168
|
-
return (turn.items.some((item) => item.kind === 'error') &&
|
|
169
|
-
(clientMessageId === undefined || !representedClientMessageIds.has(clientMessageId)));
|
|
170
|
-
})
|
|
171
|
-
: [];
|
|
172
159
|
const end = nativePageEnd(effectiveCursor, session.id, nativeTurns.length);
|
|
173
160
|
const start = Math.max(0, end - limit);
|
|
174
161
|
const nativePageTurns = nativeTurns
|
|
@@ -190,10 +177,16 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
190
177
|
const runtimeErrors = (runtimeTurn?.items ?? [])
|
|
191
178
|
.filter((item) => item.kind === 'error')
|
|
192
179
|
.map((item) => ({ ...item, turnId, runId: runtimeTurn?.runId ?? null }));
|
|
193
|
-
const items =
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
180
|
+
const items = runtimeAssistantProjectionMoreComplete(runtimeTurn, nativeItems)
|
|
181
|
+
? runtimeTurn.items.map((item) => ({
|
|
182
|
+
...item,
|
|
183
|
+
turnId,
|
|
184
|
+
runId: runtimeTurn.runId
|
|
185
|
+
}))
|
|
186
|
+
: [
|
|
187
|
+
...nativeItems,
|
|
188
|
+
...runtimeErrors.filter((error) => !nativeItems.some((item) => item.id === error.id))
|
|
189
|
+
];
|
|
197
190
|
const startedAt = items[0]?.startedAt ?? null;
|
|
198
191
|
const completedAt = items.at(-1)?.completedAt ?? startedAt;
|
|
199
192
|
const after = lastNativeItemId(entries);
|
|
@@ -219,11 +212,8 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
219
212
|
completedAt
|
|
220
213
|
};
|
|
221
214
|
});
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
const turns = combined.slice(-limit);
|
|
225
|
-
const nativePageIds = new Set(nativePageTurns.map((turn) => turn.id));
|
|
226
|
-
const nextEnd = start + dropped.filter((turn) => nativePageIds.has(turn.id)).length;
|
|
215
|
+
const turns = nativePageTurns;
|
|
216
|
+
const nextEnd = start;
|
|
227
217
|
const cursorBase = {
|
|
228
218
|
sessionId: session.id,
|
|
229
219
|
...(history.snapshotEnd === undefined ? {} : { snapshotEnd: history.snapshotEnd }),
|
|
@@ -253,6 +243,22 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
|
|
|
253
243
|
: precedingWindow
|
|
254
244
|
};
|
|
255
245
|
}
|
|
246
|
+
export function runtimeAssistantProjectionMoreComplete(runtimeTurn, projectedItems) {
|
|
247
|
+
if (runtimeTurn === undefined)
|
|
248
|
+
return false;
|
|
249
|
+
const assistantStats = (items) => items.reduce((stats, item) => {
|
|
250
|
+
if (item.kind !== 'assistant_message' || !isPlainRecord(item.payload))
|
|
251
|
+
return stats;
|
|
252
|
+
return {
|
|
253
|
+
count: stats.count + 1,
|
|
254
|
+
characters: stats.characters +
|
|
255
|
+
(typeof item.payload.text === 'string' ? item.payload.text.length : 0)
|
|
256
|
+
};
|
|
257
|
+
}, { count: 0, characters: 0 });
|
|
258
|
+
const runtime = assistantStats(runtimeTurn.items);
|
|
259
|
+
const projected = assistantStats(projectedItems);
|
|
260
|
+
return runtime.count > projected.count || runtime.characters > projected.characters;
|
|
261
|
+
}
|
|
256
262
|
function runtimeTurnClientMessageId(turn) {
|
|
257
263
|
const user = turn.items.find((item) => item.kind === 'user_message');
|
|
258
264
|
return isPlainRecord(user?.payload) && typeof user.payload.clientMessageId === 'string'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"node-pty": "1.1.0",
|
|
25
25
|
"ws": "^8.21.3",
|
|
26
26
|
"zod": "4.4.3",
|
|
27
|
-
"@myagentroam/protocol": "^0.9.
|
|
27
|
+
"@myagentroam/protocol": "^0.9.9"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@types/ws": "^8.18.1"
|