@myagentroam/node 0.10.61 → 0.10.62

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.
@@ -1,17 +1,16 @@
1
- import { isPlainRecord } from '../runner/codex/conversation-parser.js';
1
+ import { isPlainRecord } from '../util/node-operation-parsers.js';
2
+ import { ConversationObservation } from './conversation-observation.js';
2
3
  import { safeErrorCode } from '../util/safe-error.js';
3
4
  import { SessionWatchSlotCoordinator } from './session-watch-slot-coordinator.js';
4
5
  const INTERVAL_MS = 2_000;
5
6
  const TTL_MS = 45_000;
6
7
  const MAX_FAILURES = 3;
7
- const INITIAL_TURN_LIMIT = 30;
8
- const EMITTED_TURN_LIMIT = 10;
9
8
  export class NativeSessionWatchService {
10
9
  options;
11
10
  watches = new Map();
12
11
  leases = new Map();
13
12
  sessionWatchSlots;
14
- snapshotSequence = 0;
13
+ snapshotNumber = 0;
15
14
  constructor(options) {
16
15
  this.options = options;
17
16
  this.sessionWatchSlots = options.sessionWatchSlots ?? new SessionWatchSlotCoordinator();
@@ -111,23 +110,11 @@ export class NativeSessionWatchService {
111
110
  return { expiresAt: existing.expiresAt };
112
111
  }
113
112
  const watch = {
114
- session,
115
- mode: this.options.managedActive(session) ? 'RUNNING' : 'HISTORY',
113
+ observation: new ConversationObservation(session, this.options),
116
114
  expiresAt: Date.now() + TTL_MS,
117
115
  refresh: undefined,
118
- refreshMode: undefined,
119
116
  sessionSignature: undefined,
120
- signature: undefined,
121
- page: undefined,
122
- runtimePage: undefined,
123
- runtimeRevision: undefined,
124
- runtimeRead: undefined,
125
- historyRead: undefined,
126
- observedPage: undefined,
127
- authority: undefined,
128
- eventRevision: 0,
129
117
  failures: 0,
130
- transitionGeneration: 0,
131
118
  timer: undefined
132
119
  };
133
120
  watch.timer = this.refreshTimer(session.id);
@@ -136,25 +123,16 @@ export class NativeSessionWatchService {
136
123
  }
137
124
  emitRealtime(sessionId, event) {
138
125
  const watch = this.watches.get(sessionId);
139
- if (watch !== undefined)
140
- watch.eventRevision = event.sequence;
141
126
  const leases = [...this.leases.values()].filter((lease) => lease.sessionId === sessionId);
142
- const activity = sessionActivity(event);
143
- if (watch !== undefined && activity === 'RUNNING' && watch.mode !== 'RUNNING') {
144
- watch.mode = 'RUNNING';
145
- watch.transitionGeneration += 1;
127
+ const transition = watch?.observation.observe(event);
128
+ if (transition !== undefined && watch !== undefined) {
146
129
  for (const lease of leases)
147
130
  lease.ready = false;
148
- void this.publishRuntimeSnapshot(watch, watch.transitionGeneration);
149
- }
150
- else if (watch !== undefined && activity === 'IDLE' && watch.mode === 'RUNNING') {
151
- watch.mode = 'CONVERGING';
152
- const generation = ++watch.transitionGeneration;
153
- for (const lease of leases)
154
- lease.ready = false;
155
- setTimeout(() => void this.convergeHistory(watch, generation), 0);
131
+ void this.completeRefresh(sessionId, watch, transition, 'observation');
156
132
  }
157
133
  for (const lease of leases) {
134
+ if (watch?.observation.snapshotPending === true)
135
+ lease.ready = false;
158
136
  if (!lease.ready) {
159
137
  // Observations are useful only to an already watching page, never after a snapshot.
160
138
  if (event.category !== 'conversation' ||
@@ -181,6 +159,7 @@ export class NativeSessionWatchService {
181
159
  if (watch === undefined)
182
160
  return;
183
161
  clearTimeout(watch.timer);
162
+ watch.observation.close();
184
163
  this.watches.delete(sessionId);
185
164
  }
186
165
  invalidate(sessionId) {
@@ -190,8 +169,10 @@ export class NativeSessionWatchService {
190
169
  this.stop(sessionId);
191
170
  }
192
171
  clear() {
193
- for (const watch of this.watches.values())
172
+ for (const watch of this.watches.values()) {
194
173
  clearTimeout(watch.timer);
174
+ watch.observation.close();
175
+ }
195
176
  this.watches.clear();
196
177
  for (const ownerKey of [...this.leases.keys()])
197
178
  this.removeLease(ownerKey);
@@ -228,58 +209,26 @@ export class NativeSessionWatchService {
228
209
  this.stop(sessionId);
229
210
  return undefined;
230
211
  }
231
- const refresh = this.performRefresh(sessionId, watch, limit, mode).finally(() => {
232
- if (this.watches.get(sessionId) === watch) {
233
- watch.refresh = undefined;
234
- watch.refreshMode = undefined;
235
- clearTimeout(watch.timer);
236
- watch.timer = this.refreshTimer(sessionId);
237
- }
238
- });
239
- watch.refresh = refresh;
240
- watch.refreshMode = mode;
241
- return refresh;
212
+ return this.completeRefresh(sessionId, watch, this.readObservation(sessionId, watch, limit, mode), mode);
242
213
  }
243
- async performRefresh(sessionId, watch, limit, mode) {
244
- let stage = 'resolve';
245
- let session;
246
- const readHistoricalPage = watch.mode === 'HISTORY' && this.options.readHistoryPage !== undefined;
247
- try {
248
- if (this.options.suspended?.(sessionId) === true)
249
- return watch.page;
250
- session = await this.options.resolve(sessionId);
251
- if (session === undefined || session.externalSessionId === null) {
252
- this.releaseSessionLeases(sessionId);
214
+ async readObservation(sessionId, watch, limit, mode) {
215
+ const session = await this.options.resolve(sessionId);
216
+ if (this.watches.get(sessionId) !== watch)
217
+ return undefined;
218
+ if (session === undefined || session.externalSessionId === null) {
219
+ this.releaseSessionLeases(sessionId);
220
+ return undefined;
221
+ }
222
+ watch.observation.session = session;
223
+ return watch.observation.refresh(mode, limit);
224
+ }
225
+ completeRefresh(sessionId, watch, read, mode) {
226
+ const current = () => this.watches.get(sessionId) === watch && watch.refresh === refresh;
227
+ const refresh = read
228
+ .then((update) => {
229
+ if (!current() || update === undefined)
253
230
  return undefined;
254
- }
255
- watch.session = session;
256
- if (mode === 'observation' && this.options.managedActive(session))
257
- return watch.page;
258
- stage = 'refresh_activity';
259
- await this.options.refreshActivity(session);
260
- if (this.options.suspended?.(sessionId) === true)
261
- return watch.page;
262
- stage = 'read_page';
263
- const revisionBeforeRead = watch.eventRevision;
264
- const page = await (readHistoricalPage
265
- ? this.options.readHistoryPage(session, limit)
266
- : this.options.readPage(session, limit));
267
- if (revisionBeforeRead !== watch.eventRevision) {
268
- // Initial still needs a full page for its new lease, whose pending events will
269
- // replay above that page's sequence. Never publish this invalidated read to
270
- // existing leases or replace their shared observation state.
271
- if (mode !== 'initial')
272
- return watch.page;
273
- const initial = this.options.convergePage(watch.page, watch.observedPage, page, watch.authority).page;
274
- if (watch.page === undefined && watch.signature === undefined)
275
- watch.signature = JSON.stringify({
276
- source: initial.source,
277
- turns: initial.turns.slice(-(EMITTED_TURN_LIMIT + 1))
278
- });
279
- return initial;
280
- }
281
- stage = 'present_session';
282
- const presented = this.options.present(session);
231
+ const presented = this.options.present(watch.observation.session);
283
232
  const sessionSignature = JSON.stringify(presented);
284
233
  if (sessionSignature !== watch.sessionSignature) {
285
234
  watch.sessionSignature = sessionSignature;
@@ -287,63 +236,48 @@ export class NativeSessionWatchService {
287
236
  this.options.emitSession(presented);
288
237
  }
289
238
  watch.failures = 0;
290
- const turns = page.turns.length <= EMITTED_TURN_LIMIT
291
- ? page.turns
292
- : page.turns.slice(-EMITTED_TURN_LIMIT);
293
- const observed = { ...page, turns };
294
- stage = 'converge_page';
295
- const convergence = this.options.convergePage(watch.page, watch.observedPage, page, watch.authority);
296
- const converged = convergence.page;
297
- const accepted = converged.turns.length <= EMITTED_TURN_LIMIT + 1
298
- ? converged
299
- : {
300
- ...converged,
301
- turns: converged.turns.slice(-(EMITTED_TURN_LIMIT + 1))
302
- };
303
- watch.observedPage = observed;
304
- watch.authority = convergence.authority;
305
- const completingConvergence = watch.mode === 'CONVERGING' && !this.options.managedActive(session);
306
- if (completingConvergence) {
307
- watch.mode = 'HISTORY';
308
- watch.runtimePage = undefined;
309
- watch.runtimeRevision = undefined;
310
- }
311
- const signature = JSON.stringify({ source: accepted.source, turns: accepted.turns });
312
- const changed = signature !== watch.signature;
313
- if (changed) {
314
- watch.signature = signature;
315
- }
316
- if (changed || watch.page === undefined)
317
- watch.page = accepted;
318
- stage = 'emit_page';
319
- if (completingConvergence && accepted.source === 'runtime') {
320
- if (changed)
321
- this.emitSnapshotToLeases(session, accepted, true);
322
- else
323
- this.releasePendingEvents(session.id);
324
- }
325
- else if (completingConvergence)
326
- this.emitSnapshotToLeases(session, accepted, true);
327
- else if (changed)
328
- this.emitObservedPage(session, accepted);
329
- // Initial uses the full result of the same authority decision as observation.
330
- return mode === 'initial' ? converged : accepted;
331
- }
332
- catch (error) {
239
+ this.deliverObservation(watch, update);
240
+ return update.page;
241
+ })
242
+ .catch((error) => {
243
+ if (!current())
244
+ return undefined;
333
245
  watch.failures += 1;
334
246
  this.options.log?.('session.watch.refresh.failed', {
335
247
  nodeId: this.options.nodeId?.() ?? 'local-test-node',
336
248
  sessionId,
337
- ...(session === undefined ? {} : { runner: session.runner }),
338
- stage,
249
+ runner: watch.observation.session.runner,
250
+ stage: watch.observation.stage,
339
251
  errorCode: diagnosticErrorCode(error, 'SESSION_WATCH_REFRESH_FAILED'),
340
252
  failureCount: watch.failures,
341
253
  terminal: watch.failures >= MAX_FAILURES
342
254
  });
343
255
  if (watch.failures >= MAX_FAILURES)
344
256
  this.failSession(sessionId);
257
+ else
258
+ this.releasePendingEvents(sessionId);
345
259
  return undefined;
346
- }
260
+ })
261
+ .finally(() => {
262
+ if (!current())
263
+ return;
264
+ watch.refresh = undefined;
265
+ clearTimeout(watch.timer);
266
+ watch.timer = this.refreshTimer(sessionId);
267
+ });
268
+ watch.refresh = refresh;
269
+ return refresh;
270
+ }
271
+ deliverObservation(watch, update) {
272
+ const session = watch.observation.session;
273
+ if (this.watches.get(session.id) !== watch || update === undefined)
274
+ return;
275
+ if (update.snapshot)
276
+ this.emitSnapshotToLeases(session, update.page, true);
277
+ else if (update.handoff)
278
+ this.releasePendingEvents(session.id);
279
+ else if (update.changed)
280
+ this.emitObservedPage(session, update.page);
347
281
  }
348
282
  sessionHash(input) {
349
283
  return typeof input.__workspaceWatchSessionHash === 'string'
@@ -375,41 +309,36 @@ export class NativeSessionWatchService {
375
309
  stage = 'initial_refresh';
376
310
  this.watch(session);
377
311
  const watch = this.watches.get(session.id);
378
- if (watch !== undefined)
379
- watch.session = session;
380
- const activeAtStart = this.options.managedActive(session);
381
- if (watch !== undefined)
382
- watch.mode = activeAtStart ? 'RUNNING' : watch.mode;
383
- let page;
384
- if (watch !== undefined) {
385
- if (activeAtStart)
386
- page = this.composeRuntimePage(watch.page, await this.readRuntimePage(watch, session));
387
- else {
388
- // An initial subscription must read after any older observation, not join its
389
- // potentially smaller or already-stale page.
390
- if (watch.refresh !== undefined &&
391
- (watch.page !== undefined || watch.refreshMode === 'observation'))
392
- await watch.refresh;
393
- page = await this.refresh(session.id, INITIAL_TURN_LIMIT, 'initial');
394
- }
395
- }
396
- if (page === undefined)
312
+ if (watch === undefined)
313
+ return;
314
+ watch.observation.session = session;
315
+ const update = await watch.observation.initial();
316
+ if (this.leases.get(lease.ownerKey) !== lease || this.watches.get(session.id) !== watch)
317
+ return;
318
+ if (update === undefined)
397
319
  throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
320
+ watch.failures = 0;
321
+ this.deliverObservation(watch, update);
398
322
  if (lease.snapshotSent)
399
323
  return;
400
- const active = this.options.managedActive(session);
401
- if (active && !activeAtStart && watch !== undefined) {
402
- watch.mode = 'RUNNING';
403
- page = this.composeRuntimePage(page, await this.readRuntimePage(watch, session));
404
- }
405
324
  stage = 'project_snapshot';
325
+ const page = update.page;
406
326
  const payload = lease.unit === 'SEGMENT' ? projectInitialPage(session, page, 'SEGMENT', 10) : page;
407
- if (this.leases.get(lease.ownerKey) !== lease)
408
- return;
409
327
  this.emitSnapshot(lease, session, payload);
410
328
  this.releaseSnapshotPendingEvents(lease, page);
411
- if (active && watch !== undefined && watch.page === undefined)
412
- void this.hydrateRunningHistory(watch, session);
329
+ const hydration = watch.observation.hydrate();
330
+ if (hydration !== undefined)
331
+ void hydration
332
+ .then((result) => this.deliverObservation(watch, result))
333
+ .catch((error) => {
334
+ if (this.watches.get(session.id) !== watch)
335
+ return;
336
+ this.options.log?.('session.watch.running-history.failed', {
337
+ sessionId: session.id,
338
+ errorCode: diagnosticErrorCode(error, 'SESSION_WATCH_HISTORY_FAILED')
339
+ });
340
+ this.releasePendingEvents(session.id);
341
+ });
413
342
  }
414
343
  catch (error) {
415
344
  if (this.leases.get(lease.ownerKey) !== lease)
@@ -453,100 +382,6 @@ export class NativeSessionWatchService {
453
382
  else if (observer !== undefined)
454
383
  observer.expiresAt = nextExpiry;
455
384
  }
456
- composeRuntimePage(history, runtime) {
457
- return this.options.composeRuntimePage?.(history, runtime) ?? runtime;
458
- }
459
- async readRuntimePage(watch, session) {
460
- if (watch.runtimePage !== undefined && watch.runtimeRevision === watch.eventRevision)
461
- return watch.runtimePage;
462
- if (watch.runtimeRead !== undefined)
463
- return watch.runtimeRead;
464
- const revision = watch.eventRevision;
465
- const read = this.options.readPage(session, INITIAL_TURN_LIMIT);
466
- watch.runtimeRead = read;
467
- try {
468
- const page = await read;
469
- if (this.watches.get(session.id) === watch) {
470
- watch.runtimePage = page;
471
- watch.runtimeRevision = revision;
472
- }
473
- return page;
474
- }
475
- finally {
476
- if (this.watches.get(session.id) === watch && watch.runtimeRead === read)
477
- watch.runtimeRead = undefined;
478
- }
479
- }
480
- async publishRuntimeSnapshot(watch, generation) {
481
- const session = watch.session;
482
- if (session === undefined)
483
- return;
484
- try {
485
- const runtime = await this.readRuntimePage(watch, session);
486
- if (this.watches.get(session.id) !== watch ||
487
- watch.mode !== 'RUNNING' ||
488
- watch.transitionGeneration !== generation)
489
- return;
490
- watch.runtimePage = runtime;
491
- this.emitSnapshotToLeases(session, this.composeRuntimePage(watch.page, runtime), true);
492
- }
493
- catch (error) {
494
- this.options.log?.('session.watch.runtime-snapshot.failed', {
495
- nodeId: this.options.nodeId?.() ?? 'local-test-node',
496
- sessionId: session.id,
497
- runner: session.runner,
498
- errorCode: diagnosticErrorCode(error, 'SESSION_WATCH_RUNTIME_SNAPSHOT_FAILED')
499
- });
500
- this.releasePendingEvents(session.id);
501
- }
502
- }
503
- async hydrateRunningHistory(watch, session) {
504
- if (this.options.readHistoryPage === undefined || watch.historyRead !== undefined)
505
- return;
506
- const read = this.options.readHistoryPage(session, INITIAL_TURN_LIMIT);
507
- watch.historyRead = read;
508
- try {
509
- const history = await read;
510
- if (this.watches.get(session.id) !== watch || watch.mode !== 'RUNNING')
511
- return;
512
- watch.page = history;
513
- watch.authority = history.source;
514
- const generation = watch.transitionGeneration;
515
- for (const lease of this.leases.values())
516
- if (lease.sessionId === session.id)
517
- lease.ready = false;
518
- await this.publishRuntimeSnapshot(watch, generation);
519
- }
520
- catch (error) {
521
- this.options.log?.('session.watch.running-history.failed', {
522
- nodeId: this.options.nodeId?.() ?? 'local-test-node',
523
- sessionId: session.id,
524
- runner: session.runner,
525
- errorCode: diagnosticErrorCode(error, 'SESSION_WATCH_HISTORY_FAILED')
526
- });
527
- }
528
- finally {
529
- if (this.watches.get(session.id) === watch && watch.historyRead === read)
530
- watch.historyRead = undefined;
531
- }
532
- }
533
- async convergeHistory(watch, generation) {
534
- if (watch.historyRead !== undefined) {
535
- try {
536
- await watch.historyRead;
537
- }
538
- catch {
539
- // The running snapshot remains usable; the authoritative convergence below retries.
540
- }
541
- }
542
- const session = watch.session;
543
- if (session === undefined ||
544
- this.watches.get(session.id) !== watch ||
545
- watch.mode !== 'CONVERGING' ||
546
- watch.transitionGeneration !== generation)
547
- return;
548
- await this.refresh(session.id, INITIAL_TURN_LIMIT);
549
- }
550
385
  emitLeaseEvent(lease, event) {
551
386
  this.options.emitWatchEvent?.({
552
387
  type: 'watch.session.event',
@@ -583,7 +418,7 @@ export class NativeSessionWatchService {
583
418
  workspaceId: session.workspaceId,
584
419
  sessionId: session.id,
585
420
  revision: 1,
586
- snapshotSequence: ++this.snapshotSequence,
421
+ snapshotSequence: ++this.snapshotNumber,
587
422
  expiresAt: lease.expiresAt,
588
423
  payload
589
424
  });
@@ -617,7 +452,7 @@ export class NativeSessionWatchService {
617
452
  workspaceId: session.workspaceId,
618
453
  sessionId: session.id,
619
454
  revision: 1,
620
- snapshotSequence: ++this.snapshotSequence,
455
+ snapshotSequence: ++this.snapshotNumber,
621
456
  expiresAt: lease.expiresAt,
622
457
  payload
623
458
  });
@@ -635,8 +470,7 @@ export class NativeSessionWatchService {
635
470
  }
636
471
  }
637
472
  releaseSnapshotPendingEvents(lease, page) {
638
- const pageRecord = isPlainRecord(page) ? page : undefined;
639
- const candidateSequence = pageRecord?.snapshotSequence;
473
+ const candidateSequence = page.snapshotSequence;
640
474
  const snapshotSequence = typeof candidateSequence === 'number' &&
641
475
  Number.isSafeInteger(candidateSequence) &&
642
476
  candidateSequence >= 0
@@ -704,19 +538,3 @@ function diagnosticErrorCode(error, fallback) {
704
538
  return error.code;
705
539
  return fallback;
706
540
  }
707
- function sessionActivity(event) {
708
- if (!isPlainRecord(event.payload))
709
- return undefined;
710
- if (event.category === 'run' && isPlainRecord(event.payload.run)) {
711
- const status = event.payload.run.status;
712
- if (status === 'STARTING' || status === 'RUNNING' || status === 'CANCELLING')
713
- return 'RUNNING';
714
- return undefined;
715
- }
716
- if (event.category !== 'session')
717
- return undefined;
718
- const session = event.payload.session;
719
- if (!isPlainRecord(session) || typeof session.activity !== 'string')
720
- return undefined;
721
- return session.activity === 'MANAGED_ACTIVE' ? 'RUNNING' : 'IDLE';
722
- }
@@ -115,7 +115,7 @@ export class RestartSessionRestorer {
115
115
  deliveryIntent: 'QUEUE',
116
116
  content: `${goals.prompt(session.id, 'continue')}
117
117
  ${RESTART_NOTICE}`,
118
- goal: { id: entry.goal.goal.id, revision: entry.goal.goal.revision, epoch: 0 },
118
+ goal: this.options.goalReference(session.id),
119
119
  initiatedByUserId: entry.ownerUserId,
120
120
  suppressNotifications: entry.suppressNotifications === true,
121
121
  secretEnvironment: context.secretEnvironment,
@@ -5,6 +5,32 @@ export class RunnerInteractionService {
5
5
  constructor(options) {
6
6
  this.options = options;
7
7
  }
8
+ publishApproval(input) {
9
+ const run = this.options.runtime.getRun(this.options.runtime.canonicalRunId(input.runId));
10
+ if (run?.source !== 'SCHEDULED') {
11
+ this.options.publishApproval(input);
12
+ return;
13
+ }
14
+ this.options.emitRun(input.runId, 'approval.request', input);
15
+ queueMicrotask(() => {
16
+ try {
17
+ this.options.runtime.decideApproval(input.approvalId, 'APPROVED');
18
+ this.options.emitRun(input.runId, 'approval.decided', {
19
+ approvalId: input.approvalId,
20
+ decision: 'APPROVED',
21
+ source: 'SCHEDULED'
22
+ });
23
+ this.options.runners.decideApproval({
24
+ runId: input.runId,
25
+ approvalId: input.approvalId,
26
+ decision: 'APPROVED'
27
+ });
28
+ }
29
+ catch {
30
+ // A cancelled execution cannot receive an approval after its pending request expires.
31
+ }
32
+ });
33
+ }
8
34
  requestClaudeApproval(runId, toolName, toolInput) {
9
35
  const approvalId = `claude:${crypto.randomUUID()}`;
10
36
  return new Promise((resolve) => {
@@ -49,7 +75,7 @@ export class RunnerInteractionService {
49
75
  timer,
50
76
  resolve: settle
51
77
  });
52
- this.options.publishApproval({
78
+ this.publishApproval({
53
79
  runId,
54
80
  approvalId,
55
81
  payload: { toolName, input: toolInput },