@myagentroam/node 0.9.6 → 0.9.8

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.js CHANGED
@@ -276,6 +276,7 @@ export class NodeConnector {
276
276
  setCommandState: (sessionId, state) => this.commandStates.set(sessionId, state),
277
277
  clearCommandState: (sessionId, commandId) => this.commandStates.clear(sessionId, commandId),
278
278
  commandStates: (sessionId) => this.commandStates.list(sessionId),
279
+ hasActiveSessionLease: (sessionId) => this.runtime.hasActiveSessionLease(sessionId),
279
280
  fast: (input) => this.executeNodeOperation('runner.command.execute', {
280
281
  runner: 'codex',
281
282
  commandId: 'fast',
@@ -489,7 +490,8 @@ export class NodeConnector {
489
490
  projection: this.nativeSessionProjectionService,
490
491
  resume: this.externalSessionResumeService,
491
492
  profiles: () => this.runnerService.profiles(),
492
- present: (session) => this.sessionPresentationService.present(session)
493
+ present: (session) => this.sessionPresentationService.present(session),
494
+ resumeQueueAfterCommandRun: (sessionId) => this.workspaceQueueWorkbenchService.resumeAfterCommandRun(sessionId)
493
495
  });
494
496
  this.sessionMessageService = new SessionMessageService({
495
497
  runtime: this.runtime,
@@ -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;
@@ -65,6 +65,7 @@ export interface CodexManagedRunHost<TAttachment> {
65
65
  setCommandState(sessionId: string, state: RunnerCommandState): void;
66
66
  clearCommandState(sessionId: string, commandId: string): void;
67
67
  commandStates(sessionId: string): readonly RunnerCommandState[];
68
+ hasActiveSessionLease(sessionId: string): boolean;
68
69
  fast(input: string): Promise<unknown>;
69
70
  }
70
71
  export declare class CodexManagedRunController<TAttachment> {
@@ -80,6 +80,8 @@ export class CodexManagedRunController {
80
80
  throw new Error('RUNNER_COMMAND_UNAVAILABLE');
81
81
  }
82
82
  async compact(session, threadId, runId) {
83
+ if (this.host.hasActiveSessionLease(session.id))
84
+ throw new Error('RUNNER_COMMAND_UNAVAILABLE');
83
85
  this.host.adopt({
84
86
  runId,
85
87
  sessionId: session.id,
@@ -153,7 +155,13 @@ export class CodexManagedRunController {
153
155
  return;
154
156
  }
155
157
  if (notification.method === 'item/agentMessage/delta' && typeof params.delta === 'string') {
156
- this.host.emit(runId, 'text.delta', { text: params.delta });
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
+ });
157
165
  return;
158
166
  }
159
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: items.map((value) => ({ ...value, turnId: id, runId })),
79
+ items: projectedItems.map((value) => ({ ...value, turnId: id, runId })),
76
80
  startedAt,
77
81
  completedAt
78
82
  };
@@ -630,7 +630,13 @@ export class NodeRuntimeState {
630
630
  }
631
631
  this.runs.delete(runId);
632
632
  this.events.delete(runId);
633
- const turns = this.turns.get(run.sessionId) ?? [];
633
+ const turns = (this.turns.get(run.sessionId) ?? []).filter((turn) => {
634
+ const hasVisibleError = turn.items.some((item) => item.kind === 'error');
635
+ if (turn.runId === runId)
636
+ return run.status === 'SUCCEEDED' || run.status === 'INTERRUPTED' || hasVisibleError;
637
+ const isTransientErrorTurn = hasVisibleError && turn.status !== 'SUCCEEDED' && turn.status !== 'INTERRUPTED';
638
+ return run.status !== 'SUCCEEDED' || !isTransientErrorTurn;
639
+ });
634
640
  const completed = turns.filter((turn) => terminalTurn(turn.status));
635
641
  const retainedCompleted = new Set(completed.slice(-NodeRuntimeState.MAX_COMPLETED_TURNS_PER_SESSION).map((turn) => turn.id));
636
642
  this.turns.set(run.sessionId, turns.filter((turn) => !terminalTurn(turn.status) || retainedCompleted.has(turn.id)));
@@ -742,13 +748,19 @@ export class NodeRuntimeState {
742
748
  if (event.eventType === 'text.delta' &&
743
749
  isRecord(event.payload) &&
744
750
  typeof event.payload.text === 'string') {
745
- const lastItem = turn.items.at(-1);
746
- const existing = lastItem?.kind === 'assistant_message' && activeConversationItem(lastItem.status)
747
- ? lastItem
751
+ const nativeItemId = typeof event.payload.itemId === 'string' && event.payload.itemId.length <= 88
752
+ ? event.payload.itemId
748
753
  : undefined;
754
+ const stableItemId = nativeItemId === undefined ? undefined : `${event.runId}:${nativeItemId}`;
755
+ const lastItem = turn.items.at(-1);
756
+ const existing = stableItemId === undefined
757
+ ? lastItem?.kind === 'assistant_message' && activeConversationItem(lastItem.status)
758
+ ? lastItem
759
+ : undefined
760
+ : turn.items.find((item) => item.id === stableItemId && item.kind === 'assistant_message');
749
761
  const text = `${existing === undefined ? '' : readableText(existing.payload)}${event.payload.text}`;
750
762
  const item = {
751
- id: existing?.id ?? `${event.runId}:assistant:${event.sequence}`,
763
+ id: existing?.id ?? stableItemId ?? `${event.runId}:assistant:${event.sequence}`,
752
764
  sessionId: run.sessionId,
753
765
  turnId: turn.id,
754
766
  runId: event.runId,
@@ -829,7 +841,7 @@ function terminal(status) {
829
841
  return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
830
842
  }
831
843
  function terminalTurn(status) {
832
- return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED'].includes(status);
844
+ return ['SUCCEEDED', 'FAILED', 'CANCELLED', 'INTERRUPTED', 'LOST'].includes(status);
833
845
  }
834
846
  function transitionAllowed(from, to) {
835
847
  return !terminal(from) || from === to;
@@ -869,7 +881,8 @@ function conversationItemFromEvent(event) {
869
881
  };
870
882
  }
871
883
  function projectableConversationKind(value) {
872
- return (value === 'reasoning_summary' ||
884
+ return (value === 'assistant_message' ||
885
+ value === 'reasoning_summary' ||
873
886
  value === 'tool_call' ||
874
887
  value === 'command_execution' ||
875
888
  value === 'user_input_request' ||
@@ -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', { text: pending.text });
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)
@@ -12,6 +12,7 @@ interface SessionCommandServiceOptions {
12
12
  readonly resume: ExternalSessionResumeService;
13
13
  readonly profiles: () => readonly RunnerProfile[];
14
14
  readonly present: (session: NodeAgentSession) => unknown;
15
+ readonly resumeQueueAfterCommandRun: (sessionId: string) => void;
15
16
  }
16
17
  export declare class SessionCommandService {
17
18
  private readonly options;
@@ -48,6 +48,9 @@ export class SessionCommandService {
48
48
  ?.commands.find((candidate) => isPlainRecord(candidate) && candidate.id === input.commandId);
49
49
  if (command?.available !== true)
50
50
  throw new Error('RUNNER_COMMAND_UNAVAILABLE');
51
+ if (command.interaction === 'IMMEDIATE_ACTION' &&
52
+ this.options.runtime.hasActiveSessionLease(session.id))
53
+ throw new Error('RUNNER_COMMAND_UNAVAILABLE');
51
54
  const invocation = parseCommandInvocation({ descriptor: command }, input.input);
52
55
  const secretEnvironment = parseSecretEnvironment(input.secretEnvironment);
53
56
  const runner = this.options.runners.product(session.runner);
@@ -56,6 +59,10 @@ export class SessionCommandService {
56
59
  const result = await runner.executeCommand(session, command, invocation.input, {
57
60
  secretEnvironment
58
61
  });
62
+ if (command.interaction === 'IMMEDIATE_ACTION' &&
63
+ isPlainRecord(result) &&
64
+ isPlainRecord(result.run))
65
+ this.options.resumeQueueAfterCommandRun(session.id);
59
66
  return {
60
67
  ...(isPlainRecord(result) ? result : { result }),
61
68
  session: this.options.present(session)
@@ -66,6 +66,7 @@ export declare class WorkspaceQueueWorkbenchService<TAttachment> {
66
66
  migrateSession(previousId: string, nextId: string): void;
67
67
  cancelQueued(runId: string): void;
68
68
  pauseForInterrupt(sessionId: string): void;
69
+ resumeAfterCommandRun(sessionId: string): void;
69
70
  operations(): Readonly<Record<string, NodeOperationHandler>>;
70
71
  private get;
71
72
  private pauseOrResume;
@@ -64,6 +64,16 @@ export class WorkspaceQueueWorkbenchService {
64
64
  this.options.runtime.pauseSessionQueue(session.id);
65
65
  this.publish(session.id);
66
66
  }
67
+ resumeAfterCommandRun(sessionId) {
68
+ const session = this.options.runtime.getAgentSession(sessionId);
69
+ if (session === undefined)
70
+ throw new Error('SESSION_NOT_FOUND');
71
+ if (!this.options.runtime.sessionQueue(session.id).paused)
72
+ return;
73
+ this.options.runtime.resumeSessionQueue(session.id);
74
+ this.publish(session.id);
75
+ this.schedule(session.id);
76
+ }
67
77
  operations() {
68
78
  return {
69
79
  'workspace.queue.get': (data) => this.get(record(data)),
@@ -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
- ...nativeItems,
195
- ...runtimeErrors.filter((error) => !nativeItems.some((item) => item.id === error.id))
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 combined = [...nativePageTurns, ...missingRuntimeErrorTurns];
223
- const dropped = combined.slice(0, Math.max(0, combined.length - limit));
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.6",
3
+ "version": "0.9.8",
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.6"
27
+ "@myagentroam/protocol": "^0.9.8"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/ws": "^8.18.1"