@myagentroam/node 0.9.8 → 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.
@@ -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()) {
@@ -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;
@@ -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);
@@ -714,6 +720,9 @@ export class NodeRuntimeState {
714
720
  const value = this.requireSession(id);
715
721
  const next = { ...value, ...patch };
716
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);
717
726
  return next;
718
727
  }
719
728
  bumpQueueVersion(sessionId) {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/node",
3
- "version": "0.9.8",
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.8"
27
+ "@myagentroam/protocol": "^0.9.9"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@types/ws": "^8.18.1"