@myagentroam/node 0.9.3 → 0.9.5

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.
Files changed (66) hide show
  1. package/dist/claude-agent-sdk.d.ts +1 -0
  2. package/dist/claude-agent-sdk.js +9 -0
  3. package/dist/codex-app-server.d.ts +25 -0
  4. package/dist/codex-app-server.js +104 -7
  5. package/dist/connector.d.ts +1 -0
  6. package/dist/connector.js +21 -12
  7. package/dist/database.d.ts +2 -0
  8. package/dist/native-session-history.d.ts +3 -2
  9. package/dist/native-session-history.js +199 -30
  10. package/dist/opencode-server.d.ts +1 -0
  11. package/dist/opencode-server.js +1 -0
  12. package/dist/runner/abstract-runner.d.ts +11 -2
  13. package/dist/runner/abstract-runner.js +4 -0
  14. package/dist/runner/claude/managed-run-controller.js +3 -0
  15. package/dist/runner/claude-code-runner.d.ts +2 -1
  16. package/dist/runner/claude-code-runner.js +4 -0
  17. package/dist/runner/codex/managed-run-controller.d.ts +9 -0
  18. package/dist/runner/codex/managed-run-controller.js +63 -17
  19. package/dist/runner/codex-runner.d.ts +3 -1
  20. package/dist/runner/codex-runner.js +23 -2
  21. package/dist/runner/opencode/managed-run-controller.js +4 -0
  22. package/dist/runner/opencode-runner.d.ts +10 -3
  23. package/dist/runner/opencode-runner.js +87 -14
  24. package/dist/runner/runner-registry.js +16 -1
  25. package/dist/runtime-command-detector.js +1 -6
  26. package/dist/runtime-state.d.ts +2 -0
  27. package/dist/runtime-state.js +12 -7
  28. package/dist/service/conversation-history-service.d.ts +10 -1
  29. package/dist/service/conversation-history-service.js +272 -42
  30. package/dist/service/conversation-segment-service.js +7 -1
  31. package/dist/service/external-session-resume-service.d.ts +2 -0
  32. package/dist/service/external-session-resume-service.js +45 -34
  33. package/dist/service/native-session-projection-service.d.ts +1 -0
  34. package/dist/service/native-session-projection-service.js +4 -0
  35. package/dist/service/native-session-watch-service.d.ts +26 -2
  36. package/dist/service/native-session-watch-service.js +258 -42
  37. package/dist/service/node-request-service.js +2 -1
  38. package/dist/service/run-event-service.js +16 -5
  39. package/dist/service/run-workbench-service.d.ts +2 -4
  40. package/dist/service/run-workbench-service.js +2 -7
  41. package/dist/service/session-command-service.js +1 -1
  42. package/dist/service/session-identity-service.d.ts +1 -1
  43. package/dist/service/session-identity-service.js +1 -1
  44. package/dist/service/session-lifecycle-service.js +7 -12
  45. package/dist/service/session-message-service.d.ts +1 -1
  46. package/dist/service/session-message-service.js +104 -83
  47. package/dist/service/skill-directory-service.d.ts +1 -0
  48. package/dist/service/skill-directory-service.js +40 -6
  49. package/dist/service/workspace-change-service.js +1 -1
  50. package/dist/service/workspace-domain-state.d.ts +3 -0
  51. package/dist/service/workspace-domain-state.js +1 -0
  52. package/dist/service/workspace-file-service.d.ts +1 -0
  53. package/dist/service/workspace-file-service.js +14 -1
  54. package/dist/service/workspace-queue-workbench-service.d.ts +29 -2
  55. package/dist/service/workspace-queue-workbench-service.js +73 -8
  56. package/dist/service/workspace-watch-service.d.ts +7 -2
  57. package/dist/service/workspace-watch-service.js +32 -6
  58. package/dist/service/workspace-workbench-service.d.ts +14 -0
  59. package/dist/service/workspace-workbench-service.js +215 -16
  60. package/dist/util/personal-instructions.d.ts +2 -0
  61. package/dist/util/personal-instructions.js +31 -0
  62. package/dist/util/runner-native-session-parsers.d.ts +3 -1
  63. package/dist/util/runner-native-session-parsers.js +35 -17
  64. package/dist/workspace.d.ts +14 -8
  65. package/dist/workspace.js +98 -50
  66. package/package.json +2 -2
@@ -2,41 +2,88 @@ import { discoverNativeSessions, readNativeSession } from '../native-session-his
2
2
  import { coalesceImageGenerationItems } from '../runner/codex/conversation-parser.js';
3
3
  import { nativeConversationPage } from '../util/runner-native-session-parsers.js';
4
4
  import { isPlainRecord } from '../util/node-operation-parsers.js';
5
+ import { nodeLog } from '../operational.js';
5
6
  export function hasManagedActiveRun(runtime, session) {
6
7
  return (session.nativeControl === 'MAR_MANAGED' &&
7
8
  runtime
8
9
  .listRunsForSession(session.id)
9
10
  .some((run) => run.status === 'STARTING' || run.status === 'RUNNING' || run.status === 'CANCELLING'));
10
11
  }
12
+ export function convergeConversationWatchPage(previous, previousObserved, next, previousAuthority) {
13
+ const authority = isConversationHistorySource(previousAuthority)
14
+ ? previousAuthority
15
+ : previous?.source;
16
+ if (previous === undefined ||
17
+ authority === undefined ||
18
+ sourceRank(next.source) >= sourceRank(authority))
19
+ return { page: next, authority: next.source };
20
+ if (previous.turns.length === 0)
21
+ return { page: next, authority };
22
+ const boundary = next.turns.findLastIndex((turn) => previous.turns.some((candidate) => sameConversationTurn(candidate, turn)));
23
+ const observedBoundary = boundary >= 0 || previousObserved === undefined
24
+ ? -1
25
+ : next.turns.findLastIndex((turn) => previousObserved.turns.some((candidate) => sameConversationTurn(candidate, turn)));
26
+ const safeBoundary = Math.max(boundary, observedBoundary);
27
+ if (safeBoundary < 0)
28
+ return { page: previous, authority };
29
+ const additions = next.turns
30
+ .slice(safeBoundary + 1)
31
+ .filter((turn) => !previous.turns.some((candidate) => sameConversationTurn(candidate, turn)));
32
+ if (additions.length === 0)
33
+ return { page: previous, authority };
34
+ return {
35
+ page: {
36
+ ...previous,
37
+ source: next.source,
38
+ snapshotSequence: Math.max(previous.snapshotSequence, next.snapshotSequence),
39
+ readAt: next.readAt,
40
+ latestVisibleAt: next.latestVisibleAt ?? previous.latestVisibleAt,
41
+ turns: [...previous.turns, ...additions]
42
+ },
43
+ authority
44
+ };
45
+ }
46
+ function isConversationHistorySource(value) {
47
+ return value === 'official' || value === 'native' || value === 'runtime';
48
+ }
11
49
  export class ConversationHistoryService {
12
50
  options;
51
+ activeHistoryReads = new Map();
13
52
  constructor(options) {
14
53
  this.options = options;
15
54
  }
16
55
  async read(session, input) {
17
56
  const snapshotSequence = this.options.snapshotSequence();
18
57
  const readAt = Date.now();
19
- let official = await this.readOfficial(session, input);
58
+ const activeCursor = decodeActiveHistoryCursor(input.cursor, session.id);
59
+ if (activeCursor !== undefined || this.options.hasManagedActiveRun(session))
60
+ return this.readManagedActive(session, input, activeCursor, snapshotSequence, readAt);
61
+ const selected = await this.readRunnerHistory(session, input);
62
+ if (selected.page?.source === 'native')
63
+ return this.page(selected.page.nextCursor, selected.page.turns, {
64
+ snapshotSequence,
65
+ source: 'native',
66
+ readAt
67
+ });
68
+ let official = selected.page?.source === 'official' ? selected.page : undefined;
20
69
  if (official !== undefined) {
21
70
  const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
22
71
  const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
23
72
  ? []
24
73
  : this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 }).turns;
25
74
  const initialOfficialRunIds = new Set(official.turns.flatMap((turn) => (turn.runId === null ? [] : [turn.runId])));
26
- const absentLocalTurns = runtimeTurns.filter((turn) => turn.runId === null || !initialOfficialRunIds.has(turn.runId));
27
- const officialFloor = Math.min(...official.turns.map((turn) => Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0)));
28
- const missingLocalTurns = official.turns.length === 0
29
- ? absentLocalTurns.slice(-limit)
30
- : officialFloor > 0
31
- ? absentLocalTurns.filter((turn) => Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0) >= officialFloor)
32
- : absentLocalTurns.slice(-1);
75
+ const lastRepresentedRuntimeIndex = runtimeTurns.findLastIndex((turn) => turn.runId !== null && initialOfficialRunIds.has(turn.runId));
76
+ const officialHasTurns = official.turns.length > 0;
77
+ const absentLocalTurns = runtimeTurns.filter((turn, index) => (turn.runId === null || !initialOfficialRunIds.has(turn.runId)) &&
78
+ (!officialHasTurns || index > lastRepresentedRuntimeIndex));
79
+ const missingLocalTurns = absentLocalTurns.slice(-limit);
33
80
  if (input.cursor === undefined &&
34
81
  missingLocalTurns.length > 0 &&
35
82
  official.turns.length + missingLocalTurns.length > limit) {
36
83
  const reserved = Math.min(missingLocalTurns.length, Math.max(0, limit - 1));
37
84
  const narrowed = await this.readOfficial(session, { ...input, limit: limit - reserved });
38
85
  if (narrowed !== undefined)
39
- official = narrowed;
86
+ official = { ...narrowed, source: 'official' };
40
87
  }
41
88
  const officialTurns = await this.attachNativeImages(session, official.turns);
42
89
  const officialRunIds = new Set(officialTurns.flatMap((turn) => (turn.runId === null ? [] : [turn.runId])));
@@ -49,7 +96,7 @@ export class ConversationHistoryService {
49
96
  pendingItemsByRunId.set(turn.runId, pendingItems);
50
97
  }
51
98
  const availableLocalSlots = Math.max(0, limit - officialTurns.length);
52
- const localCandidates = (input.cursor === undefined ? missingLocalTurns : []).sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
99
+ const localCandidates = input.cursor === undefined ? missingLocalTurns : [];
53
100
  const localTurns = availableLocalSlots === 0 ? [] : localCandidates.slice(-availableLocalSlots);
54
101
  const localRequestIds = new Set([
55
102
  ...localTurns.flatMap((turn) => turn.items.flatMap((item) => {
@@ -78,47 +125,157 @@ export class ConversationHistoryService {
78
125
  })
79
126
  .filter((turn) => turn.items.length > 0),
80
127
  ...localTurns
81
- ].sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
128
+ ];
82
129
  return this.page(official.nextCursor, turns, {
83
130
  snapshotSequence,
84
131
  source: 'official',
85
132
  readAt
86
133
  });
87
134
  }
88
- if (session.externalSessionId !== null && !this.options.hasManagedActiveRun(session)) {
89
- const history = await this.readNative(session);
90
- if (history !== undefined) {
91
- if (history.truncated !== true ||
92
- this.options.runners.require(session.runner).acceptsTruncatedNativeHistory()) {
93
- const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
94
- ? []
95
- : this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 })
96
- .turns;
97
- const page = nativeConversationPage(session, history, input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
98
- return this.page(page.nextCursor, page.turns, {
99
- snapshotSequence,
100
- source: 'native',
101
- readAt
102
- });
103
- }
104
- const runtimePage = this.options.runtime.listConversationTurns({
105
- sessionId: session.id,
106
- ...input
107
- });
108
- if (runtimePage.turns.length === 0)
109
- throw new Error('NATIVE_TRANSCRIPT_INCOMPLETE');
110
- return this.page(runtimePage.nextCursor, runtimePage.turns, {
111
- snapshotSequence,
112
- source: 'runtime',
113
- readAt
114
- });
115
- }
116
- if (session.nativeControl === 'EXTERNAL')
117
- throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
135
+ if (session.externalSessionId !== null && selected.incompleteNative) {
136
+ const runtimePage = this.options.runtime.listConversationTurns({
137
+ sessionId: session.id,
138
+ ...input
139
+ });
140
+ if (runtimePage.turns.length === 0)
141
+ throw new Error('NATIVE_TRANSCRIPT_INCOMPLETE');
142
+ return this.page(runtimePage.nextCursor, runtimePage.turns, {
143
+ snapshotSequence,
144
+ source: 'runtime',
145
+ readAt
146
+ });
118
147
  }
148
+ if (session.externalSessionId !== null && session.nativeControl === 'EXTERNAL')
149
+ throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
119
150
  const page = this.options.runtime.listConversationTurns({ sessionId: session.id, ...input });
120
151
  return this.page(page.nextCursor, page.turns, { snapshotSequence, source: 'runtime', readAt });
121
152
  }
153
+ async readRunnerHistory(session, input) {
154
+ const runner = this.options.runners.require(session.runner);
155
+ let incompleteNative = false;
156
+ const page = await runner.readConversationHistory({
157
+ native: async () => {
158
+ const startedAt = performance.now();
159
+ let history;
160
+ try {
161
+ history = await this.readNative(session);
162
+ }
163
+ catch {
164
+ return undefined;
165
+ }
166
+ finally {
167
+ const durationMs = Math.round(performance.now() - startedAt);
168
+ if (durationMs >= 250)
169
+ nodeLog('native.session.history.native-read.completed', {
170
+ runner: session.runner,
171
+ sessionId: session.id,
172
+ durationMs
173
+ });
174
+ }
175
+ if (history === undefined)
176
+ return undefined;
177
+ if (history.truncated === true && !runner.acceptsTruncatedNativeHistory()) {
178
+ incompleteNative = true;
179
+ return undefined;
180
+ }
181
+ const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
182
+ ? []
183
+ : this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 })
184
+ .turns;
185
+ const native = nativeConversationPage(session, history, input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
186
+ return native;
187
+ },
188
+ official: () => this.readOfficial(session, input)
189
+ });
190
+ return { page, incompleteNative };
191
+ }
192
+ async readManagedActive(session, input, cursor, snapshotSequence, readAt) {
193
+ const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
194
+ const runtimeTurns = this.options.runtime.listConversationTurns({
195
+ sessionId: session.id,
196
+ limit: 500
197
+ }).turns;
198
+ const beforeAt = cursor?.beforeAt ?? oldestTurnActivityAt(runtimeTurns);
199
+ if (cursor?.stage === 'OFFICIAL') {
200
+ const prefetched = this.takeActiveHistoryRead(session.id, limit, cursor.cursor);
201
+ const selected = await (prefetched ??
202
+ this.readRunnerHistory(session, {
203
+ ...(cursor.cursor === null ? {} : { cursor: cursor.cursor }),
204
+ limit
205
+ }));
206
+ const history = selected.page;
207
+ if (history === undefined)
208
+ return this.page(null, [], { snapshotSequence, source: 'runtime', readAt });
209
+ const hydratedTurns = history.source === 'official'
210
+ ? await this.attachNativeImages(session, history.turns)
211
+ : history.turns;
212
+ const turns = hydratedTurns.filter((turn) => latestTurnActivityAt(turn) < beforeAt);
213
+ return this.page(history.nextCursor === null
214
+ ? null
215
+ : encodeActiveHistoryCursor({
216
+ sessionId: session.id,
217
+ stage: 'OFFICIAL',
218
+ beforeAt,
219
+ cursor: history.nextCursor
220
+ }), turns, { snapshotSequence, source: history.source, readAt });
221
+ }
222
+ if (cursor === undefined && session.externalSessionId !== null)
223
+ this.prefetchActiveHistory(session, limit);
224
+ const runtimePage = this.options.runtime.listConversationTurns({
225
+ sessionId: session.id,
226
+ ...(cursor?.cursor === null || cursor === undefined ? {} : { cursor: cursor.cursor }),
227
+ limit
228
+ });
229
+ const nextCursor = runtimePage.nextCursor !== null
230
+ ? encodeActiveHistoryCursor({
231
+ sessionId: session.id,
232
+ stage: 'RUNTIME',
233
+ beforeAt,
234
+ cursor: runtimePage.nextCursor
235
+ })
236
+ : session.externalSessionId === null
237
+ ? null
238
+ : encodeActiveHistoryCursor({
239
+ sessionId: session.id,
240
+ stage: 'OFFICIAL',
241
+ beforeAt,
242
+ cursor: null
243
+ });
244
+ return this.page(nextCursor, runtimePage.turns, {
245
+ snapshotSequence,
246
+ source: 'runtime',
247
+ readAt,
248
+ deferredOlderHistory: true
249
+ });
250
+ }
251
+ prefetchActiveHistory(session, limit) {
252
+ const now = Date.now();
253
+ for (const [sessionId, read] of this.activeHistoryReads)
254
+ if (read.createdAt + 60_000 <= now)
255
+ this.activeHistoryReads.delete(sessionId);
256
+ const current = this.activeHistoryReads.get(session.id);
257
+ if (current !== undefined && current.limit === limit)
258
+ return;
259
+ this.activeHistoryReads.set(session.id, {
260
+ createdAt: now,
261
+ limit,
262
+ // The active Run fast path must stay independent from an optional
263
+ // background reader failure until the user explicitly requests older history.
264
+ page: this.readRunnerHistory(session, { limit }).catch(() => ({
265
+ page: undefined,
266
+ incompleteNative: false
267
+ }))
268
+ });
269
+ }
270
+ takeActiveHistoryRead(sessionId, limit, cursor) {
271
+ if (cursor !== null)
272
+ return undefined;
273
+ const read = this.activeHistoryReads.get(sessionId);
274
+ if (read === undefined || read.limit !== limit || read.createdAt + 60_000 <= Date.now())
275
+ return undefined;
276
+ this.activeHistoryReads.delete(sessionId);
277
+ return read.page;
278
+ }
122
279
  async refreshExternalActivity(session) {
123
280
  const runner = this.options.runners.require(session.runner);
124
281
  if (!runner.available(this.options.capabilities())) {
@@ -139,7 +296,15 @@ export class ConversationHistoryService {
139
296
  return readNativeSession(session.runner, session.cwd, session.externalSessionId);
140
297
  }
141
298
  async attachNativeImages(session, turns) {
299
+ const startedAt = performance.now();
142
300
  const history = await this.readNative(session);
301
+ const readDurationMs = Math.round(performance.now() - startedAt);
302
+ if (readDurationMs >= 250)
303
+ nodeLog('native.session.history.attachment-source-read.completed', {
304
+ runner: session.runner,
305
+ sessionId: session.id,
306
+ durationMs: readDurationMs
307
+ });
143
308
  if (history === undefined)
144
309
  return turns;
145
310
  const attachments = new Map();
@@ -166,7 +331,19 @@ export class ConversationHistoryService {
166
331
  const runner = this.options.runners.require(session.runner);
167
332
  if (session.externalSessionId === null || !runner.available(this.options.capabilities()))
168
333
  return undefined;
169
- return runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId));
334
+ const startedAt = performance.now();
335
+ try {
336
+ return await runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId));
337
+ }
338
+ finally {
339
+ const durationMs = Math.round(performance.now() - startedAt);
340
+ if (durationMs >= 250)
341
+ nodeLog('native.session.history.official-read.completed', {
342
+ runner: session.runner,
343
+ sessionId: session.id,
344
+ durationMs
345
+ });
346
+ }
170
347
  }
171
348
  managedRunnerTurn(session, nativeTurnId) {
172
349
  const runId = this.options.runners
@@ -187,3 +364,56 @@ export class ConversationHistoryService {
187
364
  };
188
365
  }
189
366
  }
367
+ const ACTIVE_HISTORY_CURSOR_PREFIX = 'mar-active-history:1:';
368
+ function encodeActiveHistoryCursor(cursor) {
369
+ return `${ACTIVE_HISTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString('base64url')}`;
370
+ }
371
+ function decodeActiveHistoryCursor(value, sessionId) {
372
+ if (value === undefined || !value.startsWith(ACTIVE_HISTORY_CURSOR_PREFIX))
373
+ return undefined;
374
+ try {
375
+ const decoded = JSON.parse(Buffer.from(value.slice(ACTIVE_HISTORY_CURSOR_PREFIX.length), 'base64url').toString('utf8'));
376
+ if (decoded.sessionId !== sessionId ||
377
+ (decoded.stage !== 'RUNTIME' && decoded.stage !== 'OFFICIAL') ||
378
+ typeof decoded.beforeAt !== 'number' ||
379
+ !Number.isFinite(decoded.beforeAt) ||
380
+ decoded.beforeAt < 0 ||
381
+ (decoded.cursor !== null && typeof decoded.cursor !== 'string'))
382
+ throw new Error('EVENT_CURSOR_INVALID');
383
+ return decoded;
384
+ }
385
+ catch {
386
+ throw new Error('EVENT_CURSOR_INVALID');
387
+ }
388
+ }
389
+ function latestTurnActivityAt(turn) {
390
+ return Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0);
391
+ }
392
+ function oldestTurnActivityAt(turns) {
393
+ return turns.length === 0 ? 0 : Math.min(...turns.map(latestTurnActivityAt));
394
+ }
395
+ function sameConversationTurn(left, right) {
396
+ if (left.id === right.id)
397
+ return true;
398
+ if (left.runId !== null && left.runId === right.runId)
399
+ return true;
400
+ const leftMessageId = conversationClientMessageId(left);
401
+ return leftMessageId !== null && leftMessageId === conversationClientMessageId(right);
402
+ }
403
+ function conversationClientMessageId(turn) {
404
+ for (const item of turn.items) {
405
+ if (item.kind !== 'user_message' || !isPlainRecord(item.payload))
406
+ continue;
407
+ const value = item.payload.clientMessageId;
408
+ if (typeof value === 'string' && value.length > 0)
409
+ return value;
410
+ }
411
+ return null;
412
+ }
413
+ function sourceRank(source) {
414
+ if (source === 'official')
415
+ return 3;
416
+ if (source === 'native')
417
+ return 2;
418
+ return 1;
419
+ }
@@ -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
  }));
@@ -89,7 +90,10 @@ export class ConversationSegmentService {
89
90
  snapshotSequence: page.snapshotSequence,
90
91
  source: page.source,
91
92
  readAt: page.readAt,
92
- latestVisibleAt: page.latestVisibleAt
93
+ latestVisibleAt: page.latestVisibleAt,
94
+ ...(boundary === undefined && page.deferredOlderHistory === true
95
+ ? { deferredOlderHistory: true }
96
+ : {})
93
97
  };
94
98
  const pageSegments = page.turns.flatMap(conversationSegments);
95
99
  let eligible = pageSegments;
@@ -108,6 +112,8 @@ export class ConversationSegmentService {
108
112
  hasOlder = eligible.length > take.length || page.nextCursor !== null;
109
113
  collected.unshift(...take);
110
114
  }
115
+ if (boundary === undefined && page.deferredOlderHistory === true)
116
+ break;
111
117
  if (collected.length >= limit || page.nextCursor === null)
112
118
  break;
113
119
  if (visited.has(page.nextCursor))
@@ -18,7 +18,9 @@ interface ExternalSessionResumeServiceOptions {
18
18
  }
19
19
  export declare class ExternalSessionResumeService {
20
20
  private readonly options;
21
+ private readonly transitions;
21
22
  constructor(options: ExternalSessionResumeServiceOptions);
23
+ transitioning(sessionId: string): boolean;
22
24
  resume(external: NodeAgentSession, force?: boolean): Promise<NodeAgentSession | undefined>;
23
25
  failure(sessionId: string): {
24
26
  readonly code: string;
@@ -1,9 +1,13 @@
1
1
  const RESUME_FAILURE_TTL_MS = 30 * 60_000;
2
2
  export class ExternalSessionResumeService {
3
3
  options;
4
+ transitions = new Set();
4
5
  constructor(options) {
5
6
  this.options = options;
6
7
  }
8
+ transitioning(sessionId) {
9
+ return this.transitions.has(sessionId);
10
+ }
7
11
  async resume(external, force = false) {
8
12
  this.clearFailure(external.id);
9
13
  const priorId = this.options.state.resumedByDirectId.get(external.id);
@@ -15,40 +19,47 @@ export class ExternalSessionResumeService {
15
19
  this.setFailure(external.id, 'RUNNER_UNAVAILABLE');
16
20
  return undefined;
17
21
  }
18
- const result = await runner.resumeExternal(external, {
19
- available: this.options.available(external.runner),
20
- projectionFor: this.options.projectionFor,
21
- managedAliasFor: this.options.managedAliasFor,
22
- currentResumed: (sessionId) => {
23
- const resumedId = this.options.state.resumedByDirectId.get(sessionId);
24
- return resumedId === undefined
25
- ? undefined
26
- : this.options.runtime.getAgentSession(resumedId);
27
- },
28
- promote: (sessionId) => {
29
- this.options.runtime.setNativeControl(sessionId, 'MAR_MANAGED');
30
- return this.options.runtime.getAgentSession(sessionId);
31
- },
32
- ensureConfiguration: this.options.ensureConfiguration,
33
- create: (session) => {
34
- const configuration = this.options.configuration(session);
35
- return this.options.runtime.createAgentSession({
36
- workspaceId: session.workspaceId,
37
- runner: session.runner,
38
- ...(session.externalSessionId === null
39
- ? {}
40
- : { externalSessionId: session.externalSessionId }),
41
- nativeControl: 'MAR_MANAGED',
42
- cwd: session.cwd,
43
- model: configuration.model,
44
- effort: configuration.effort,
45
- access: configuration.access,
46
- ...(session.runnerTitle === null ? {} : { title: session.runnerTitle })
47
- });
48
- },
49
- workspacePath: this.options.workspacePath,
50
- rememberManagedNative: this.options.rememberNative
51
- }, force);
22
+ this.transitions.add(external.id);
23
+ let result;
24
+ try {
25
+ result = await runner.resumeExternal(external, {
26
+ available: this.options.available(external.runner),
27
+ projectionFor: this.options.projectionFor,
28
+ managedAliasFor: this.options.managedAliasFor,
29
+ currentResumed: (sessionId) => {
30
+ const resumedId = this.options.state.resumedByDirectId.get(sessionId);
31
+ return resumedId === undefined
32
+ ? undefined
33
+ : this.options.runtime.getAgentSession(resumedId);
34
+ },
35
+ promote: (sessionId) => {
36
+ this.options.runtime.setNativeControl(sessionId, 'MAR_MANAGED');
37
+ return this.options.runtime.getAgentSession(sessionId);
38
+ },
39
+ ensureConfiguration: this.options.ensureConfiguration,
40
+ create: (session) => {
41
+ const configuration = this.options.configuration(session);
42
+ return this.options.runtime.createAgentSession({
43
+ workspaceId: session.workspaceId,
44
+ runner: session.runner,
45
+ ...(session.externalSessionId === null
46
+ ? {}
47
+ : { externalSessionId: session.externalSessionId }),
48
+ nativeControl: 'MAR_MANAGED',
49
+ cwd: session.cwd,
50
+ model: configuration.model,
51
+ effort: configuration.effort,
52
+ access: configuration.access,
53
+ ...(session.runnerTitle === null ? {} : { title: session.runnerTitle })
54
+ });
55
+ },
56
+ workspacePath: this.options.workspacePath,
57
+ rememberManagedNative: this.options.rememberNative
58
+ }, force);
59
+ }
60
+ finally {
61
+ this.transitions.delete(external.id);
62
+ }
52
63
  if (result.activity !== undefined)
53
64
  this.options.state.externalActivity.set(external.id, result.activity);
54
65
  if (result.session !== undefined) {
@@ -32,6 +32,7 @@ export declare class NativeSessionProjectionService {
32
32
  })[]>;
33
33
  discoverWorkspace(workspaceId: string): Promise<readonly NodeAgentSession[]>;
34
34
  resolve(sessionId: string): Promise<NodeAgentSession | undefined>;
35
+ resolveCurrent(sessionId: string): Promise<NodeAgentSession | undefined>;
35
36
  createMetadataProjection(sessionId: string): Promise<NodeAgentSession | undefined>;
36
37
  resumedConfiguration(session: NodeAgentSession): RunnerDefaultConfiguration;
37
38
  ensureResumedConfiguration(session: NodeAgentSession): NodeAgentSession;
@@ -81,6 +81,10 @@ export class NativeSessionProjectionService {
81
81
  createdAt: 0
82
82
  };
83
83
  }
84
+ async resolveCurrent(sessionId) {
85
+ const direct = await this.resolve(sessionId);
86
+ return direct === undefined ? undefined : (this.projectionForNative(direct) ?? direct);
87
+ }
84
88
  async createMetadataProjection(sessionId) {
85
89
  const direct = await this.resolve(sessionId);
86
90
  if (direct === undefined || direct.externalSessionId === null)
@@ -2,24 +2,37 @@ 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;
9
+ readonly suspended?: (sessionId: string) => boolean;
8
10
  readonly refreshActivity: (session: NodeAgentSession) => Promise<void>;
9
11
  readonly present: (session: NodeAgentSession) => unknown;
10
12
  readonly readPage: (session: NodeAgentSession, limit: number) => Promise<TPage>;
13
+ readonly convergePage: (previous: TPage | undefined, previousObserved: TPage | undefined, next: TPage, previousAuthority: unknown) => {
14
+ readonly page: TPage;
15
+ readonly authority: unknown;
16
+ };
11
17
  readonly projectInitialPage?: (session: NodeAgentSession, page: TPage, unit: 'SEGMENT', limit: number) => unknown;
12
18
  readonly emitSession: (session: unknown) => void;
13
- readonly emitTurn: (turn: unknown) => void;
19
+ readonly emitPage: (session: NodeAgentSession, page: TPage) => void;
20
+ readonly nodeId?: () => string;
21
+ readonly nodeGeneration?: () => string;
22
+ readonly emitWatchEvent?: (payload: unknown) => void;
14
23
  }
15
24
  export declare class NativeSessionWatchService<TPage extends {
16
25
  readonly turns: readonly unknown[];
26
+ readonly source?: unknown;
17
27
  }> {
18
28
  private readonly options;
19
29
  private readonly watches;
30
+ private readonly leases;
31
+ private snapshotSequence;
20
32
  constructor(options: NativeSessionWatchOptions<TPage>);
21
33
  operations(): Readonly<Record<string, NodeOperationHandler>>;
22
34
  private executeWatch;
35
+ private executeUnwatch;
23
36
  watch(session: NodeAgentSession): {
24
37
  readonly expiresAt: number;
25
38
  } | undefined;
@@ -33,6 +46,17 @@ export declare class NativeSessionWatchService<TPage extends {
33
46
  readonly expiresAt: number;
34
47
  } | undefined;
35
48
  get size(): number;
36
- refresh(sessionId: string, limit?: number): Promise<TPage | undefined>;
49
+ refresh(sessionId: string, limit?: number, emit?: boolean): Promise<TPage | undefined>;
37
50
  private performRefresh;
51
+ private sessionHash;
52
+ private ownerKey;
53
+ private accepted;
54
+ private initializeLease;
55
+ private removeLease;
56
+ private emitObservedPage;
57
+ private failSession;
58
+ private releaseSessionLeases;
59
+ private leaseTimer;
60
+ private latestLeaseExpiry;
61
+ private refreshTimer;
38
62
  }