@myagentroam/node 0.11.36 → 0.11.38

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 (145) 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 -3
  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 +145 -253
  15. package/dist/control-outbound-scheduler.js +3 -0
  16. package/dist/database.js +34 -14
  17. package/dist/git-askpass.js +1 -1
  18. package/dist/git-execution-helper.js +1 -1
  19. package/dist/hosting/hosted-conversation-file-identity.js +27 -0
  20. package/dist/hosting/hosted-conversation-file-operations.js +44 -0
  21. package/dist/hosting/hosted-conversation-node-service.js +974 -0
  22. package/dist/hosting/hosted-conversation-runs.js +599 -0
  23. package/dist/hosting/hosted-conversation-templates.js +280 -0
  24. package/dist/hosting/hosted-conversation-uploads.js +344 -0
  25. package/dist/hosting/hosted-conversation-watch.js +194 -0
  26. package/dist/hosting/runtime-contracts.js +1 -0
  27. package/dist/runner/abstract-runner.js +4 -13
  28. package/dist/{claude-agent-sdk.js → runner/claude/claude-agent-sdk.js} +2 -2
  29. package/dist/runner/claude/conversation-parser.js +2 -1
  30. package/dist/runner/claude/managed-run-controller.js +6 -7
  31. package/dist/runner/claude/message-parsers.js +2 -1
  32. package/dist/runner/claude/native-transcript.js +2 -1
  33. package/dist/runner/claude/tool-normalizer.js +1 -1
  34. package/dist/runner/claude-code-runner.js +2 -2
  35. package/dist/runner/codex/codex-app-server.js +471 -0
  36. package/dist/runner/codex/conversation-parser.js +4 -2
  37. package/dist/runner/codex/managed-run-controller.js +7 -9
  38. package/dist/runner/codex/native-transcript.js +1 -1
  39. package/dist/runner/codex/tool-normalizer.js +1 -1
  40. package/dist/runner/codex-runner.js +9 -27
  41. package/dist/runner/contracts.js +1 -0
  42. package/dist/runner/mar-agent/conversation-normalizer.js +2 -2
  43. package/dist/runner/mar-agent/history-projection.js +1 -1
  44. package/dist/runner/mar-agent/hosted-conversation-runtime.js +6 -10
  45. package/dist/runner/mar-agent/managed-run-controller.js +85 -61
  46. package/dist/runner/mar-agent/runtime-model-factory.js +0 -3
  47. package/dist/runner/mar-agent-runner.js +1 -1
  48. package/dist/runner/normalization.js +42 -0
  49. package/dist/runner/opencode/conversation-parser.js +1 -1
  50. package/dist/runner/opencode/managed-run-controller.js +6 -7
  51. package/dist/{opencode-server.js → runner/opencode/opencode-server.js} +2 -2
  52. package/dist/runner/opencode/tool-normalizer.js +1 -1
  53. package/dist/runner/opencode-runner.js +4 -5
  54. package/dist/runner/runner-registry.js +4 -1
  55. package/dist/runtime-state.js +48 -372
  56. package/dist/service/computer-use-enablement-service.js +1 -1
  57. package/dist/service/continuous-run-coordinator.js +52 -7
  58. package/dist/service/conversation-history-service.js +4 -4
  59. package/dist/service/conversation-hosting-capability-service.js +1 -1
  60. package/dist/service/conversation-hosting-enablement-service.js +1 -1
  61. package/dist/service/conversation-hosting-lifecycle-service.js +3 -3
  62. package/dist/service/conversation-hosting-maintenance-service.js +1 -1
  63. package/dist/service/conversation-observation.js +1 -1
  64. package/dist/service/conversation-segment-service.js +1 -1
  65. package/dist/service/external-session-resume-service.js +47 -27
  66. package/dist/service/fake-managed-run-service.js +3 -7
  67. package/dist/service/git-operation-service.js +1 -1
  68. package/dist/service/goal-continuation-coordinator.js +14 -7
  69. package/dist/service/mar-agent-model-transport-test-service.js +1 -1
  70. package/dist/service/native-session-watch-service.js +1 -1
  71. package/dist/service/node-connection-lifecycle-service.js +6 -3
  72. package/dist/service/node-control-channel.js +48 -11
  73. package/dist/service/node-request-service.js +1 -1
  74. package/dist/service/node-terminal-channel.js +2 -1
  75. package/dist/service/node-workspace-coordinator.js +3 -2
  76. package/dist/service/notification-mcp-bridge.js +6 -2
  77. package/dist/service/recent-run-projection-service.js +1 -1
  78. package/dist/service/restart-session-restorer.js +1 -1
  79. package/dist/service/run-event-service.js +49 -42
  80. package/dist/service/run-workbench-service.js +3 -3
  81. package/dist/service/runner-interaction-service.js +6 -5
  82. package/dist/service/runner-service.js +1 -1
  83. package/dist/service/session-recent-activity.js +2 -1
  84. package/dist/service/skill-directory-service.js +1 -1
  85. package/dist/service/skill-install-service.js +2 -2
  86. package/dist/service/terminal-service.js +1 -1
  87. package/dist/service/workbench-event-projection.js +3 -2
  88. package/dist/service/workbench-event-service.js +1 -1
  89. package/dist/service/workspace-service.js +3 -2
  90. package/dist/session/attachments.js +110 -0
  91. package/dist/session/contracts.js +1 -0
  92. package/dist/{util → session}/conversation-identity.js +1 -1
  93. package/dist/session/input-validation.js +15 -0
  94. package/dist/{service → session}/native-session-projection-service.js +40 -11
  95. package/dist/session/pagination.js +77 -0
  96. package/dist/{util → session}/runner-native-session-parsers.js +2 -1
  97. package/dist/session/runtime/approvals.js +90 -0
  98. package/dist/session/runtime/conversation-projection.js +310 -0
  99. package/dist/session/runtime/type-guards.js +19 -0
  100. package/dist/{service → session}/session-catalog-service.js +5 -4
  101. package/dist/{service → session}/session-command-service.js +3 -3
  102. package/dist/{service → session}/session-identity-service.js +1 -1
  103. package/dist/{service → session}/session-lifecycle-service.js +18 -7
  104. package/dist/{service → session}/session-message-service.js +6 -5
  105. package/dist/{service → session}/session-presentation-service.js +1 -1
  106. package/dist/{service → session}/session-query-service.js +2 -2
  107. package/dist/{service → session}/session-schedule-service.js +2 -2
  108. package/dist/util/records.js +3 -0
  109. package/dist/workspace/contracts.js +1 -0
  110. package/dist/workspace/file-index.js +534 -0
  111. package/dist/workspace/files.js +466 -0
  112. package/dist/workspace/git.js +991 -0
  113. package/dist/workspace/path-access.js +184 -0
  114. package/dist/workspace/utf8.js +12 -0
  115. package/dist/{service → workspace}/workspace-change-service.js +45 -21
  116. package/dist/{service → workspace}/workspace-code-navigation-service.js +6 -6
  117. package/dist/{service → workspace}/workspace-file-manager-service.js +1 -1
  118. package/dist/{service → workspace}/workspace-file-service.js +16 -16
  119. package/dist/{service → workspace}/workspace-git-exclude-service.js +1 -1
  120. package/dist/{service → workspace}/workspace-queue-workbench-service.js +3 -2
  121. package/dist/{service → workspace}/workspace-session-service.js +45 -40
  122. package/dist/{service → workspace}/workspace-upload-service.js +28 -28
  123. package/dist/{service → workspace}/workspace-watch-service.js +29 -28
  124. package/dist/{service → workspace}/workspace-workbench-service.js +4 -3
  125. package/package.json +4 -4
  126. package/dist/service/hosted-conversation-node-service.js +0 -2252
  127. package/dist/service/run-domain-state.js +0 -4
  128. package/dist/service/session-domain-state.js +0 -9
  129. package/dist/service/workspace-domain-state.js +0 -41
  130. package/dist/util/node-operation-parsers.js +0 -320
  131. package/dist/workspace.js +0 -2168
  132. /package/dist/{connector → application}/node-connector-options.js +0 -0
  133. /package/dist/{util → connection}/node-operation-data.js +0 -0
  134. /package/dist/{connector → connection}/node-operation-router.js +0 -0
  135. /package/dist/{service → hosting}/hosted-conversation-sandbox-profile.js +0 -0
  136. /package/dist/{service → hosting}/hosted-conversation-store.js +0 -0
  137. /package/dist/{util → mcp}/secret-environment.js +0 -0
  138. /package/dist/{claude-channel.js → runner/claude/claude-channel.js} +0 -0
  139. /package/dist/{util → runner}/personal-instructions.js +0 -0
  140. /package/dist/{util → runner}/runner-error.js +0 -0
  141. /package/dist/{service/conversation-work-process.js → session/runtime/work-process.js} +0 -0
  142. /package/dist/{service → session}/session-context-service.js +0 -0
  143. /package/dist/{util → workspace}/git-credential-client.js +0 -0
  144. /package/dist/{service → workspace}/workspace-content-search-service.js +0 -0
  145. /package/dist/{service → workspace}/workspace-watch-snapshot-service.js +0 -0
@@ -1,5 +1,6 @@
1
- import { denyPermission } from '../claude-agent-sdk.js';
2
- import { boundedConversationItemId, isTerminalRunStatus } from '../util/node-operation-parsers.js';
1
+ import { denyPermission } from '../runner/claude/claude-agent-sdk.js';
2
+ import { boundedConversationItemId } from '../runner/normalization.js';
3
+ import { isTerminalRunStatus } from '../session/runtime/type-guards.js';
3
4
  import { claudeUserInputQuestions } from '../runner/claude/conversation-parser.js';
4
5
  export class RunnerInteractionService {
5
6
  options;
@@ -223,7 +224,6 @@ export class RunnerInteractionService {
223
224
  finally {
224
225
  codex.execution.runs.delete(runId);
225
226
  codex.releaseEnvironment(runId);
226
- this.options.state.active.delete(runId);
227
227
  }
228
228
  }
229
229
  respondUserInput(runId, requestId, rawAnswers) {
@@ -260,13 +260,14 @@ export class RunnerInteractionService {
260
260
  if (run === undefined)
261
261
  return false;
262
262
  if (run.status === 'CANCELLING') {
263
- const status = this.options.state.interruptRequested.has(runId) ? 'INTERRUPTED' : 'CANCELLED';
263
+ const status = this.options.runtime.isExecutionInterruptRequested(runId)
264
+ ? 'INTERRUPTED'
265
+ : 'CANCELLED';
264
266
  this.options.emitRun(runId, 'run.completed', { status }, status);
265
267
  }
266
268
  else if (!isTerminalRunStatus(run.status))
267
269
  return false;
268
270
  this.options.codex().execution.runs.delete(runId);
269
- this.options.state.active.delete(runId);
270
271
  return true;
271
272
  }
272
273
  respondCodexUserInput(runId, requestId, rawAnswers) {
@@ -1,5 +1,5 @@
1
1
  import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
2
- import { parseSecretEnvironment } from '../util/secret-environment.js';
2
+ import { parseSecretEnvironment } from '../mcp/secret-environment.js';
3
3
  export class RunnerService {
4
4
  database;
5
5
  capabilities;
@@ -1,4 +1,5 @@
1
- import { isPlainRecord, isTerminalRunStatus } from '../util/node-operation-parsers.js';
1
+ import { isPlainRecord } from '../util/records.js';
2
+ import { isTerminalRunStatus } from '../session/runtime/type-guards.js';
2
3
  /** Projects only already-observed, process-local public facts. Never reads native history. */
3
4
  function activity(runtime, sessionId) {
4
5
  const candidate = runtime.activeSessionRun(sessionId);
@@ -1,7 +1,7 @@
1
1
  import { lstat, open, readdir, readlink, realpath } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { skillInstallMetadataSchema } from '@myagentroam/protocol';
4
- import { hasBrokenWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
4
+ import { hasBrokenWorkspaceSkillGitExclude } from '../workspace/workspace-git-exclude-service.js';
5
5
  const MAX_METADATA_BYTES = 64 * 1024;
6
6
  const SKILL_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
7
7
  const WINDOWS_DEVICE_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/u;
@@ -4,8 +4,8 @@ import { promisify } from 'node:util';
4
4
  import { lstat, mkdir, readFile, readdir, readlink, realpath, rename, rm, symlink, writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
6
  import { skillInstallMetadataSchema } from '@myagentroam/protocol';
7
- import { GitCommandError, resolveWorkspaceGitRoot } from '../workspace.js';
8
- import { rewriteWorkspaceGitExclude, updateWorkspaceSkillGitExclude } from './workspace-git-exclude-service.js';
7
+ import { GitCommandError, resolveWorkspaceGitRoot } from '../workspace/git.js';
8
+ import { rewriteWorkspaceGitExclude, updateWorkspaceSkillGitExclude } from '../workspace/workspace-git-exclude-service.js';
9
9
  export class SkillInstallService {
10
10
  async recoverNodeHome(home) {
11
11
  const agentsRoot = path.join(home, '.agents', 'skills');
@@ -1,4 +1,4 @@
1
- import { parseSecretEnvironment } from '../util/secret-environment.js';
1
+ import { parseSecretEnvironment } from '../mcp/secret-environment.js';
2
2
  export class TerminalService {
3
3
  database;
4
4
  terminalManager;
@@ -1,6 +1,7 @@
1
1
  import { conversationSegmentIdentityForItem, conversationSegments } from './conversation-segment-service.js';
2
- import { runConversationItemId } from '../runtime-state.js';
3
- import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../util/node-operation-parsers.js';
2
+ import { runConversationItemId } from '../session/runtime/conversation-projection.js';
3
+ import { isNodeAgentRun, isNodeConversationTurn } from '../session/runtime/type-guards.js';
4
+ import { isPlainRecord } from '../util/records.js';
4
5
  export function workbenchPayloadSessionId(getRun, category, payload) {
5
6
  if (!isPlainRecord(payload))
6
7
  return undefined;
@@ -1,5 +1,5 @@
1
1
  import { createEnvelope } from '@myagentroam/protocol';
2
- import { isPlainRecord } from '../util/node-operation-parsers.js';
2
+ import { isPlainRecord } from '../util/records.js';
3
3
  import { projectWorkbenchPayload, workbenchPayloadSessionId } from './workbench-event-projection.js';
4
4
  export class WorkbenchEventService {
5
5
  options;
@@ -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
+ }