@myagentroam/node 0.11.36 → 0.11.37

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 (142) hide show
  1. package/dist/application/execution-lifecycle.js +54 -0
  2. package/dist/application/node-lifecycle.js +104 -0
  3. package/dist/application/node-operation-admission.js +51 -0
  4. package/dist/application/node-operation-contracts.js +1 -0
  5. package/dist/application/runner-composition.js +22 -0
  6. package/dist/application/session-projection-composition.js +26 -0
  7. package/dist/application/workspace-application.js +61 -0
  8. package/dist/application/workspace-composition.js +21 -0
  9. package/dist/capabilities.js +2 -2
  10. package/dist/codex-app-server.js +1 -471
  11. package/dist/connection/authorization.js +26 -0
  12. package/dist/{service → connection}/direct-transfer-service.js +1 -1
  13. package/dist/connection/endpoints.js +32 -0
  14. package/dist/connector.js +144 -252
  15. package/dist/database.js +34 -14
  16. package/dist/git-askpass.js +1 -1
  17. package/dist/git-execution-helper.js +1 -1
  18. package/dist/hosting/hosted-conversation-file-identity.js +27 -0
  19. package/dist/hosting/hosted-conversation-file-operations.js +44 -0
  20. package/dist/hosting/hosted-conversation-node-service.js +974 -0
  21. package/dist/hosting/hosted-conversation-runs.js +599 -0
  22. package/dist/hosting/hosted-conversation-templates.js +280 -0
  23. package/dist/hosting/hosted-conversation-uploads.js +344 -0
  24. package/dist/hosting/hosted-conversation-watch.js +194 -0
  25. package/dist/hosting/runtime-contracts.js +1 -0
  26. package/dist/runner/abstract-runner.js +4 -13
  27. package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
  28. package/dist/runner/claude/conversation-parser.js +2 -1
  29. package/dist/runner/claude/managed-run-controller.js +6 -7
  30. package/dist/runner/claude/message-parsers.js +2 -1
  31. package/dist/runner/claude/native-transcript.js +2 -1
  32. package/dist/runner/claude/tool-normalizer.js +1 -1
  33. package/dist/runner/claude-code-runner.js +2 -2
  34. package/dist/runner/codex/codex-app-server.js +471 -0
  35. package/dist/runner/codex/conversation-parser.js +4 -2
  36. package/dist/runner/codex/managed-run-controller.js +7 -9
  37. package/dist/runner/codex/native-transcript.js +1 -1
  38. package/dist/runner/codex/tool-normalizer.js +1 -1
  39. package/dist/runner/codex-runner.js +9 -27
  40. package/dist/runner/contracts.js +1 -0
  41. package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
  42. package/dist/runner/mar-agent/history-projection.js +1 -1
  43. package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
  44. package/dist/runner/mar-agent/managed-run-controller.js +85 -61
  45. package/dist/runner/mar-agent-runner.js +1 -1
  46. package/dist/runner/normalization.js +42 -0
  47. package/dist/runner/opencode/conversation-parser.js +1 -1
  48. package/dist/runner/opencode/managed-run-controller.js +6 -7
  49. package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
  50. package/dist/runner/opencode/tool-normalizer.js +1 -1
  51. package/dist/runner/opencode-runner.js +4 -5
  52. package/dist/runner/runner-registry.js +4 -1
  53. package/dist/runtime-state.js +48 -372
  54. package/dist/service/computer-use-enablement-service.js +1 -1
  55. package/dist/service/continuous-run-coordinator.js +52 -7
  56. package/dist/service/conversation-history-service.js +4 -4
  57. package/dist/service/conversation-hosting-capability-service.js +1 -1
  58. package/dist/service/conversation-hosting-enablement-service.js +1 -1
  59. package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
  60. package/dist/service/conversation-hosting-maintenance-service.js +1 -1
  61. package/dist/service/conversation-observation.js +1 -1
  62. package/dist/service/conversation-segment-service.js +1 -1
  63. package/dist/service/external-session-resume-service.js +47 -27
  64. package/dist/service/fake-managed-run-service.js +3 -7
  65. package/dist/service/git-operation-service.js +1 -1
  66. package/dist/service/goal-continuation-coordinator.js +14 -7
  67. package/dist/service/mar-agent-model-transport-test-service.js +1 -1
  68. package/dist/service/native-session-watch-service.js +1 -1
  69. package/dist/service/node-control-channel.js +1 -5
  70. package/dist/service/node-request-service.js +1 -1
  71. package/dist/service/node-terminal-channel.js +2 -1
  72. package/dist/service/node-workspace-coordinator.js +3 -2
  73. package/dist/service/notification-mcp-bridge.js +6 -2
  74. package/dist/service/recent-run-projection-service.js +1 -1
  75. package/dist/service/restart-session-restorer.js +1 -1
  76. package/dist/service/run-event-service.js +49 -42
  77. package/dist/service/run-workbench-service.js +3 -3
  78. package/dist/service/runner-interaction-service.js +6 -5
  79. package/dist/service/runner-service.js +1 -1
  80. package/dist/service/session-recent-activity.js +2 -1
  81. package/dist/service/skill-directory-service.js +1 -1
  82. package/dist/service/skill-install-service.js +2 -2
  83. package/dist/service/terminal-service.js +1 -1
  84. package/dist/service/workbench-event-projection.js +3 -2
  85. package/dist/service/workbench-event-service.js +1 -1
  86. package/dist/service/workspace-service.js +3 -2
  87. package/dist/session/attachments.js +110 -0
  88. package/dist/session/contracts.js +1 -0
  89. package/dist/{util → session}/conversation-identity.js +1 -1
  90. package/dist/session/input-validation.js +15 -0
  91. package/dist/{service → session}/native-session-projection-service.js +40 -11
  92. package/dist/session/pagination.js +77 -0
  93. package/dist/{util → session}/runner-native-session-parsers.js +2 -1
  94. package/dist/session/runtime/approvals.js +90 -0
  95. package/dist/session/runtime/conversation-projection.js +310 -0
  96. package/dist/session/runtime/type-guards.js +19 -0
  97. package/dist/{service → session}/session-catalog-service.js +5 -4
  98. package/dist/{service → session}/session-command-service.js +3 -3
  99. package/dist/{service → session}/session-identity-service.js +1 -1
  100. package/dist/{service → session}/session-lifecycle-service.js +18 -7
  101. package/dist/{service → session}/session-message-service.js +6 -5
  102. package/dist/{service → session}/session-presentation-service.js +1 -1
  103. package/dist/{service → session}/session-query-service.js +2 -2
  104. package/dist/{service → session}/session-schedule-service.js +2 -2
  105. package/dist/util/records.js +3 -0
  106. package/dist/workspace/contracts.js +1 -0
  107. package/dist/workspace/file-index.js +534 -0
  108. package/dist/workspace/files.js +466 -0
  109. package/dist/workspace/git.js +991 -0
  110. package/dist/workspace/path-access.js +184 -0
  111. package/dist/workspace/utf8.js +12 -0
  112. package/dist/{service → workspace}/workspace-change-service.js +45 -21
  113. package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
  114. package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
  115. package/dist/{service → workspace}/workspace-file-service.js +16 -16
  116. package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
  117. package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
  118. package/dist/{service → workspace}/workspace-session-service.js +45 -40
  119. package/dist/{service → workspace}/workspace-upload-service.js +28 -28
  120. package/dist/{service → workspace}/workspace-watch-service.js +29 -28
  121. package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
  122. package/package.json +5 -5
  123. package/dist/service/hosted-conversation-node-service.js +0 -2252
  124. package/dist/service/run-domain-state.js +0 -4
  125. package/dist/service/session-domain-state.js +0 -9
  126. package/dist/service/workspace-domain-state.js +0 -41
  127. package/dist/util/node-operation-parsers.js +0 -320
  128. package/dist/workspace.js +0 -2168
  129. /package/dist/{connector → application}/node-connector-options.js +0 -0
  130. /package/dist/{util → connection}/node-operation-data.js +0 -0
  131. /package/dist/{connector → connection}/node-operation-router.js +0 -0
  132. /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
  133. /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
  134. /package/dist/{util → mcp}/secret-environment.js +0 -0
  135. /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
  136. /package/dist/{util → runner}/personal-instructions.js +0 -0
  137. /package/dist/{util → runner}/runner-error.js +0 -0
  138. /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
  139. /package/dist/{service → session}/session-context-service.js +0 -0
  140. /package/dist/{util → workspace}/git-credential-client.js +0 -0
  141. /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
  142. /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
@@ -1,6 +1,7 @@
1
1
  import { realpath, rmdir } from 'node:fs/promises';
2
- import { createWorkspaceDirectory, inspectWorkspace, listWorkspaceDirectories } from '../workspace.js';
3
- import { ensureWorkspaceGitExclude } from './workspace-git-exclude-service.js';
2
+ import { createWorkspaceDirectory, listWorkspaceDirectories } from '../workspace/files.js';
3
+ import { inspectWorkspace } from '../workspace/git.js';
4
+ import { ensureWorkspaceGitExclude } from '../workspace/workspace-git-exclude-service.js';
4
5
  import { workspaceGitSettingsSchema } from '@myagentroam/protocol';
5
6
  export class WorkspaceService {
6
7
  database;
@@ -0,0 +1,110 @@
1
+ import { serializeComposerTag, parseComposerTags } from '@myagentroam/protocol';
2
+ import { isPlainRecord } from '../util/records.js';
3
+ const MAX_COMPOSER_ATTACHMENT_BYTES = 5 * 1024 * 1024;
4
+ const MAX_COMPOSER_ATTACHMENTS_BYTES = 10 * 1024 * 1024;
5
+ const COMPOSER_IMAGE_MIME_TYPES = new Set([
6
+ 'image/png',
7
+ 'image/jpeg',
8
+ 'image/gif',
9
+ 'image/webp'
10
+ ]);
11
+ export function parseComposerAttachmentUpload(input) {
12
+ if (input.composerAttachmentId === undefined && input.mime === undefined)
13
+ return undefined;
14
+ if (typeof input.composerAttachmentId !== 'string' ||
15
+ !isUuid(input.composerAttachmentId) ||
16
+ typeof input.name !== 'string' ||
17
+ !validComposerAttachmentName(input.name) ||
18
+ typeof input.mime !== 'string' ||
19
+ input.mime.length === 0 ||
20
+ input.mime.length > 120)
21
+ throw new Error('COMPOSER_ATTACHMENT_INVALID');
22
+ return { id: input.composerAttachmentId, name: input.name, mime: input.mime };
23
+ }
24
+ export function parseComposerAttachmentIds(value) {
25
+ if (value === undefined)
26
+ return [];
27
+ if (!Array.isArray(value) || value.length > 5 || value.some((id) => typeof id !== 'string'))
28
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
29
+ const ids = value;
30
+ if (ids.some((id) => !isUuid(id)) || new Set(ids).size !== ids.length)
31
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
32
+ return ids;
33
+ }
34
+ export function validComposerAttachmentName(name) {
35
+ return (name.trim().length > 0 &&
36
+ name.length <= 255 &&
37
+ name !== '.' &&
38
+ name !== '..' &&
39
+ !name.includes('/') &&
40
+ !name.includes('\\') &&
41
+ !Array.from(name).some((character) => {
42
+ const code = character.charCodeAt(0);
43
+ return code <= 31 || code === 127 || '<>:"|?*'.includes(character);
44
+ }));
45
+ }
46
+ export function isUuid(value) {
47
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
48
+ }
49
+ export function parseMessageAttachments(value) {
50
+ if (value === undefined)
51
+ return [];
52
+ if (!Array.isArray(value) || value.length > 5)
53
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
54
+ let totalBytes = 0;
55
+ return value.map((attachment) => {
56
+ if (!isPlainRecord(attachment))
57
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
58
+ if (typeof attachment.name !== 'string' ||
59
+ typeof attachment.mime !== 'string' ||
60
+ typeof attachment.dataBase64 !== 'string')
61
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
62
+ if (!validComposerAttachmentName(attachment.name) ||
63
+ attachment.mime.length === 0 ||
64
+ attachment.mime.length > 120 ||
65
+ !isCanonicalBase64(attachment.dataBase64))
66
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
67
+ const byteSize = Buffer.byteLength(attachment.dataBase64, 'base64');
68
+ if (byteSize === 0 || byteSize > MAX_COMPOSER_ATTACHMENT_BYTES)
69
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
70
+ totalBytes += byteSize;
71
+ if (totalBytes > MAX_COMPOSER_ATTACHMENTS_BYTES)
72
+ throw new Error('MESSAGE_ATTACHMENTS_INVALID');
73
+ return { name: attachment.name, mime: attachment.mime, dataBase64: attachment.dataBase64 };
74
+ });
75
+ }
76
+ export function parseRunnerImageAttachments(value) {
77
+ return parseMessageAttachments(value).map((attachment) => {
78
+ if (!isRunnerImageAttachment(attachment))
79
+ throw new Error('MESSAGE_ATTACHMENT_UNSUPPORTED');
80
+ return attachment;
81
+ });
82
+ }
83
+ export function isRunnerImageAttachment(attachment) {
84
+ return COMPOSER_IMAGE_MIME_TYPES.has(attachment.mime);
85
+ }
86
+ export function isImageAttachment(attachment) {
87
+ return attachment.mime.startsWith('image/');
88
+ }
89
+ export function withNonImageAttachmentPrompt(input, paths, files) {
90
+ if (paths.length === 0)
91
+ return input;
92
+ const tag = serializeComposerTag('mar_attachments', {
93
+ files: files
94
+ ? [...files]
95
+ : paths.map((path) => ({
96
+ name: path.split(/[\\/]/u).at(-1),
97
+ mime: 'application/octet-stream',
98
+ path
99
+ }))
100
+ });
101
+ const body = parseComposerTags(input)
102
+ .filter((part) => part.kind !== 'mar_attachments')
103
+ .map((part) => (part.kind === 'text' ? part.text : part.raw))
104
+ .join('')
105
+ .trimEnd();
106
+ return `${body}\n${tag}`;
107
+ }
108
+ export function isCanonicalBase64(value) {
109
+ return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
110
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- import { isPlainRecord } from './node-operation-parsers.js';
1
+ import { isPlainRecord } from '../util/records.js';
2
2
  export function conversationClientMessageId(turn) {
3
3
  for (const item of turn.items) {
4
4
  if (item.kind !== 'user_message' || !isPlainRecord(item.payload))
@@ -0,0 +1,15 @@
1
+ import { composerVisibleText } from '@myagentroam/protocol';
2
+ export function validSessionOption(value) {
3
+ return (value === undefined ||
4
+ (typeof value === 'string' && value.trim().length > 0 && value.length <= 120));
5
+ }
6
+ export function validSessionEffort(value) {
7
+ return value === undefined || (typeof value === 'string' && value.length <= 120);
8
+ }
9
+ export function validSessionTitle(value) {
10
+ return (value === undefined ||
11
+ (typeof value === 'string' && value.trim().length > 0 && value.length <= 160));
12
+ }
13
+ export function sessionTitleFromFirstMessage(content) {
14
+ return composerVisibleText(content).replace(/\s+/g, ' ').slice(0, 160);
15
+ }
@@ -1,6 +1,10 @@
1
- import { deduplicateDirectSessions } from '../util/runner-native-session-parsers.js';
1
+ import { deduplicateDirectSessions } from './runner-native-session-parsers.js';
2
2
  export class NativeSessionProjectionService {
3
3
  options;
4
+ directNative = new Map();
5
+ cachedNative() {
6
+ return [...this.directNative.values()];
7
+ }
4
8
  constructor(options) {
5
9
  this.options = options;
6
10
  }
@@ -54,7 +58,7 @@ export class NativeSessionProjectionService {
54
58
  return sessions;
55
59
  }
56
60
  async resolve(sessionId, registeredOnly = false) {
57
- const cached = this.options.state.directNative.get(sessionId);
61
+ const cached = this.directNative.get(sessionId);
58
62
  if (cached !== undefined)
59
63
  return cached;
60
64
  const database = this.options.database();
@@ -152,20 +156,45 @@ export class NativeSessionProjectionService {
152
156
  async rename(session, input) {
153
157
  if (session.externalSessionId === null)
154
158
  return;
155
- await this.options.runners
156
- .require(session.runner)
157
- .renameNative(session, input, this.requireDatabase());
159
+ const runner = this.options.runners.require(session.runner);
160
+ const database = this.requireDatabase();
161
+ await runner.renameNative(session, input);
162
+ const runnerTitle = runner.usesRunnerTitleForRename();
163
+ database.saveNativeSessionMetadata({
164
+ runner: session.runner,
165
+ externalSessionId: session.externalSessionId,
166
+ workspaceId: session.workspaceId,
167
+ ...(input.title === undefined
168
+ ? {}
169
+ : runnerTitle
170
+ ? {
171
+ runnerTitle: input.title,
172
+ runnerTitlePending: input.title !== null && input.title.trim().length > 0
173
+ }
174
+ : { customTitle: input.title }),
175
+ ...(input.pinned === undefined ? {} : { pinned: input.pinned })
176
+ });
158
177
  }
159
178
  project(input) {
160
179
  const timestamp = input.lastActivityAt ?? 0;
161
180
  const database = this.options.database();
162
181
  const metadata = database?.getNativeSessionMetadata(input.runner, input.externalSessionId, input.workspace.id);
163
- const runnerTitle = this.options.runners.require(input.runner).projectedRunnerTitle(metadata, {
182
+ const projection = this.options.runners.require(input.runner).projectedRunnerTitle(metadata, {
164
183
  externalSessionId: input.externalSessionId,
165
184
  workspaceId: input.workspace.id,
166
185
  ...(input.title === undefined ? {} : { title: input.title }),
167
186
  ...(input.titleOrigin === undefined ? {} : { titleOrigin: input.titleOrigin })
168
- }, database);
187
+ });
188
+ if (projection.clearPending) {
189
+ database?.saveNativeSessionMetadata({
190
+ runner: input.runner,
191
+ externalSessionId: input.externalSessionId,
192
+ workspaceId: input.workspace.id,
193
+ runnerTitle: projection.title,
194
+ runnerTitlePending: false
195
+ });
196
+ }
197
+ const runnerTitle = projection.title;
169
198
  return {
170
199
  id: input.externalSessionId,
171
200
  nodeId: this.options.nodeId(),
@@ -199,7 +228,7 @@ export class NativeSessionProjectionService {
199
228
  remember(sessions) {
200
229
  const database = this.options.database();
201
230
  for (const session of sessions) {
202
- this.options.state.directNative.set(session.id, session);
231
+ this.directNative.set(session.id, session);
203
232
  if (database === undefined || session.externalSessionId === null)
204
233
  continue;
205
234
  const metadata = database.getNativeSessionMetadata(session.runner, session.externalSessionId, session.workspaceId);
@@ -224,7 +253,7 @@ export class NativeSessionProjectionService {
224
253
  record.metadata.customTitle ?? record.metadata.runnerTitle ?? 'Untitled session'
225
254
  ]));
226
255
  for (const session of [
227
- ...this.options.state.directNative.values(),
256
+ ...this.directNative.values(),
228
257
  ...this.options.runtime.listAgentSessions()
229
258
  ]) {
230
259
  if (session.workspaceId !== workspaceId)
@@ -312,13 +341,13 @@ export class NativeSessionProjectionService {
312
341
  }
313
342
  }
314
343
  const directIds = new Set();
315
- for (const [directId, direct] of this.options.state.directNative) {
344
+ for (const [directId, direct] of this.directNative) {
316
345
  if (direct.runner === session.runner &&
317
346
  direct.cwd === session.cwd &&
318
347
  direct.externalSessionId !== null &&
319
348
  nativeIds.has(direct.externalSessionId)) {
320
349
  directIds.add(directId);
321
- this.options.state.directNative.delete(directId);
350
+ this.directNative.delete(directId);
322
351
  }
323
352
  }
324
353
  for (const projectionId of projectionIds) {
@@ -0,0 +1,77 @@
1
+ export const DEFAULT_SESSION_PAGE_SIZE = 10;
2
+ export const MAX_SESSION_PAGE_SIZE = 100;
3
+ export function sessionPage(sessions, input) {
4
+ const limit = typeof input.limit === 'number' &&
5
+ Number.isInteger(input.limit) &&
6
+ input.limit >= 1 &&
7
+ input.limit <= MAX_SESSION_PAGE_SIZE
8
+ ? input.limit
9
+ : DEFAULT_SESSION_PAGE_SIZE;
10
+ const cursor = typeof input.cursor === 'string' ? input.cursor : undefined;
11
+ const ordered = [...sessions].sort(compareSessionPageEntries);
12
+ const cursorEntry = cursor === undefined ? undefined : parseSessionPageCursor(cursor);
13
+ const afterCursor = cursor === undefined
14
+ ? ordered
15
+ : cursorEntry === undefined
16
+ ? []
17
+ : ordered.filter((session) => compareSessionPageEntries(session, cursorEntry) > 0);
18
+ const pinned = ordered.filter((session) => session.pinOrder !== null && session.pinOrder !== undefined);
19
+ const pinnedIndexes = new Map(pinned.map((session, index) => [session.id, index]));
20
+ const page = afterCursor.slice(0, limit).map((session) => {
21
+ const pinIndex = pinnedIndexes.get(session.id);
22
+ if (pinIndex === undefined)
23
+ return session;
24
+ return {
25
+ ...session,
26
+ canMovePinUp: pinIndex > 0,
27
+ canMovePinDown: pinIndex < pinned.length - 1
28
+ };
29
+ });
30
+ const last = page.at(-1);
31
+ return {
32
+ sessions: page,
33
+ nextCursor: last !== undefined && afterCursor.length > page.length ? createSessionPageCursor(last) : null
34
+ };
35
+ }
36
+ export function parseSessionPageCursor(cursor) {
37
+ try {
38
+ const value = JSON.parse(decodeURIComponent(cursor));
39
+ if (!Array.isArray(value) || value.length !== 3)
40
+ return undefined;
41
+ const [pinOrder, lastActivityAt, id] = value;
42
+ if ((pinOrder !== null && (typeof pinOrder !== 'number' || !Number.isFinite(pinOrder))) ||
43
+ (lastActivityAt !== null &&
44
+ (typeof lastActivityAt !== 'number' || !Number.isFinite(lastActivityAt))) ||
45
+ typeof id !== 'string')
46
+ return undefined;
47
+ return {
48
+ id,
49
+ lastActivityAt: lastActivityAt ?? 0,
50
+ pinOrder,
51
+ externalDiscovered: lastActivityAt !== null
52
+ };
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ export function createSessionPageCursor(session) {
59
+ return encodeURIComponent(JSON.stringify([
60
+ session.pinOrder ?? null,
61
+ session.externalDiscovered === false ? null : session.lastActivityAt,
62
+ session.id
63
+ ]));
64
+ }
65
+ export function compareSessionPageEntries(left, right) {
66
+ const leftPinned = left.pinOrder !== null && left.pinOrder !== undefined;
67
+ const rightPinned = right.pinOrder !== null && right.pinOrder !== undefined;
68
+ if (leftPinned !== rightPinned)
69
+ return leftPinned ? -1 : 1;
70
+ if (leftPinned && rightPinned)
71
+ return (left.pinOrder - right.pinOrder || left.id.localeCompare(right.id));
72
+ const leftFound = left.externalDiscovered !== false;
73
+ const rightFound = right.externalDiscovered !== false;
74
+ if (leftFound !== rightFound)
75
+ return leftFound ? -1 : 1;
76
+ return right.lastActivityAt - left.lastActivityAt || left.id.localeCompare(right.id, 'en');
77
+ }
@@ -1,6 +1,7 @@
1
1
  import { nativeTranscriptAdapter } from '../native-transcript-adapters.js';
2
2
  import { createHash } from 'node:crypto';
3
- import { compactRunnerText, isPlainRecord, nativePageEnd } from './node-operation-parsers.js';
3
+ import { compactRunnerText, nativePageEnd } from '../runner/normalization.js';
4
+ import { isPlainRecord } from '../util/records.js';
4
5
  export async function settleWithin(promise, milliseconds) {
5
6
  let timer;
6
7
  try {
@@ -0,0 +1,90 @@
1
+ export class RuntimeApprovals {
2
+ getRun;
3
+ currentExecutionId;
4
+ canonicalRunId;
5
+ now;
6
+ approvals = new Map();
7
+ constructor(getRun, currentExecutionId, canonicalRunId, now) {
8
+ this.getRun = getRun;
9
+ this.currentExecutionId = currentExecutionId;
10
+ this.canonicalRunId = canonicalRunId;
11
+ this.now = now;
12
+ }
13
+ createApproval(input) {
14
+ const run = this.getRun(input.runId);
15
+ if (run === undefined)
16
+ throw new Error('RUN_NOT_FOUND');
17
+ if (input.runId !== run.id && this.currentExecutionId(run.id) !== input.runId)
18
+ throw new Error('RUN_EXECUTION_NOT_CURRENT');
19
+ if (this.approvals.has(input.approvalId))
20
+ throw new Error('APPROVAL_ALREADY_EXISTS');
21
+ if (input.expiresAt <= this.now())
22
+ throw new Error('APPROVAL_EXPIRED');
23
+ const value = {
24
+ id: input.approvalId,
25
+ runId: input.runId,
26
+ status: 'PENDING',
27
+ payload: input.payload,
28
+ expiresAt: input.expiresAt,
29
+ createdAt: this.now(),
30
+ decidedAt: null
31
+ };
32
+ this.approvals.set(value.id, value);
33
+ return value;
34
+ }
35
+ getApproval(id) {
36
+ const value = this.approvals.get(id);
37
+ if (value?.status === 'PENDING' && value.expiresAt <= this.now())
38
+ return this.expireApproval(id);
39
+ return value;
40
+ }
41
+ listApprovals(runId) {
42
+ const canonicalRunId = this.canonicalRunId(runId);
43
+ return [...this.approvals.values()]
44
+ .filter((value) => this.canonicalRunId(value.runId) === canonicalRunId)
45
+ .map((value) => this.getApproval(value.id));
46
+ }
47
+ expireApproval(id) {
48
+ const value = this.requireApproval(id);
49
+ if (value.status !== 'PENDING')
50
+ return value;
51
+ if (value.expiresAt > this.now())
52
+ throw new Error('APPROVAL_NOT_EXPIRED');
53
+ const next = { ...value, status: 'EXPIRED', decidedAt: this.now() };
54
+ this.approvals.set(id, next);
55
+ return next;
56
+ }
57
+ decideApproval(id, decision) {
58
+ const value = this.getApproval(id);
59
+ if (value === undefined)
60
+ throw new Error('APPROVAL_NOT_FOUND');
61
+ if (value.status === 'EXPIRED')
62
+ throw new Error('APPROVAL_EXPIRED');
63
+ if (value.status !== 'PENDING')
64
+ throw new Error('APPROVAL_ALREADY_DECIDED');
65
+ const next = { ...value, status: decision, decidedAt: this.now() };
66
+ this.approvals.set(id, next);
67
+ return next;
68
+ }
69
+ expireDueApprovals() {
70
+ return [...this.approvals.values()]
71
+ .filter((value) => value.status === 'PENDING' && value.expiresAt <= this.now())
72
+ .map((value) => this.expireApproval(value.id));
73
+ }
74
+ requireApproval(id) {
75
+ const value = this.approvals.get(id);
76
+ if (value === undefined)
77
+ throw new Error('APPROVAL_NOT_FOUND');
78
+ return value;
79
+ }
80
+ removeRuns(runIds, executionIds) {
81
+ for (const [id, approval] of this.approvals)
82
+ if (runIds.has(approval.runId) || executionIds.has(approval.runId))
83
+ this.approvals.delete(id);
84
+ }
85
+ removeRun(runId, executionIds) {
86
+ for (const [id, approval] of this.approvals)
87
+ if (runId === approval.runId || executionIds.has(approval.runId))
88
+ this.approvals.delete(id);
89
+ }
90
+ }