@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
@@ -1,7 +1,10 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { isImageAttachment, isRunnerImageAttachment, parseComposerAttachmentIds, parseMessageAttachments, sessionTitleFromFirstMessage, withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js';
2
3
  import { parseSecretEnvironment } from '../util/secret-environment.js';
4
+ import { personalInstructionsForRunner } from '../util/personal-instructions.js';
3
5
  export class SessionMessageService {
4
6
  options;
7
+ pendingMessages = new Map();
5
8
  constructor(options) {
6
9
  this.options = options;
7
10
  }
@@ -19,20 +22,19 @@ export class SessionMessageService {
19
22
  typeof input.content !== 'string' ||
20
23
  input.content.length === 0)
21
24
  throw new Error('MESSAGE_INVALID');
25
+ const clientMessageId = input.clientMessageId;
26
+ const content = input.content;
22
27
  const inlineAttachments = parseMessageAttachments(input.attachments);
23
28
  const secretEnvironment = parseSecretEnvironment(input.secretEnvironment);
24
29
  const attachmentIds = parseComposerAttachmentIds(input.attachmentIds);
25
30
  if (inlineAttachments.length > 0 && attachmentIds.length > 0)
26
31
  throw new Error('MESSAGE_ATTACHMENTS_INVALID');
27
- const session = this.options.runtime.setTemporaryTitleIfMissing((await this.options.resolve(input.sessionId)).id, sessionTitleFromFirstMessage(input.content));
28
- this.options.emitSession(session);
29
- const intent = input.deliveryIntent;
30
- if (intent !== undefined &&
31
- intent !== 'SEND' &&
32
- intent !== 'QUEUE' &&
33
- intent !== 'REPLACE_CURRENT')
32
+ const session = await this.options.resolve(input.sessionId);
33
+ const personalInstructions = personalInstructionsForRunner(input.personalInstructions, session.runner);
34
+ const intent = input.deliveryIntent ?? 'SEND';
35
+ if (intent !== 'SEND' && intent !== 'QUEUE' && intent !== 'REPLACE_CURRENT')
34
36
  throw new Error('MESSAGE_INVALID');
35
- const existing = this.options.runtime.findMessageRun(session.id, input.clientMessageId);
37
+ const existing = this.options.runtime.findMessageRun(session.id, clientMessageId);
36
38
  if (existing !== undefined)
37
39
  return {
38
40
  session: this.options.present(session),
@@ -40,84 +42,103 @@ export class SessionMessageService {
40
42
  delivery: this.options.runtime.isQueuedRun(existing.id) ? 'QUEUED' : 'STARTED',
41
43
  idempotent: true
42
44
  };
43
- const uploaded = await this.options.uploads.resolve(session.workspaceId, attachmentIds);
44
- const attachments = attachmentIds.length === 0
45
- ? inlineAttachments
46
- : uploaded.map((attachment) => attachment.attachment);
47
- const imageAttachments = attachments.filter(isImageAttachment);
48
- const runnerImageAttachments = attachments.filter(isRunnerImageAttachment);
49
- const active = this.options.runtime
50
- .listRunsForSession(session.id)
51
- .find((run) => run.status === 'STARTING' || run.status === 'RUNNING');
52
- const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
53
- const queueWasPaused = this.options.runtime.sessionQueue(session.id).paused;
54
- const queuedBeforeCreate = this.options.runtime.hasActiveSessionLease(session.id) || intent === 'QUEUE';
55
- const created = this.options.runtime.createMessageRun({
56
- sessionId: session.id,
57
- clientMessageId: input.clientMessageId,
58
- content: input.content,
59
- ...(attachments.length === 0 ? {} : { attachments })
60
- });
61
- if (!created.created)
62
- return {
63
- session: this.options.present(session),
64
- run: created.run,
65
- delivery: this.options.runtime.isQueuedRun(created.run.id) ? 'QUEUED' : 'STARTED',
66
- idempotent: true
67
- };
68
- this.options.attachments.cache(created.run.id, imageAttachments);
69
- let attachmentPaths;
45
+ const pendingKey = `${session.id}\u0000${clientMessageId}`;
46
+ const pending = this.pendingMessages.get(pendingKey);
47
+ if (pending !== undefined)
48
+ return pending;
49
+ const operation = (async () => {
50
+ const runId = randomUUID();
51
+ let enqueued = false;
52
+ try {
53
+ const uploaded = await this.options.uploads.resolve(session.workspaceId, attachmentIds);
54
+ const attachments = attachmentIds.length === 0
55
+ ? inlineAttachments
56
+ : uploaded.map((attachment) => attachment.attachment);
57
+ const imageAttachments = attachments.filter(isImageAttachment);
58
+ const runnerImageAttachments = attachments.filter(isRunnerImageAttachment);
59
+ const attachmentPaths = await this.persistAttachments(session, runId, attachments);
60
+ const runnerInput = withNonImageAttachmentPrompt(content, attachmentPaths);
61
+ const planActive = this.options.commands
62
+ .list(session.id)
63
+ .some((state) => state.commandId === 'plan');
64
+ const runner = this.options.runners.require(session.runner);
65
+ const active = this.options.runtime
66
+ .listRunsForSession(session.id)
67
+ .find((run) => run.status === 'STARTING' || run.status === 'RUNNING');
68
+ const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
69
+ const queueBeforeCreate = this.options.runtime.sessionQueue(session.id);
70
+ const leaseWasActive = this.options.runtime.hasActiveSessionLease(session.id);
71
+ const created = this.options.queue.enqueue({
72
+ runId,
73
+ sessionId: session.id,
74
+ workspaceId: session.workspaceId,
75
+ runner: session.runner,
76
+ input: runner.prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
77
+ attachments: runnerImageAttachments,
78
+ attachmentPaths,
79
+ cwd: session.cwd,
80
+ externalSessionId: session.externalSessionId,
81
+ model: session.model,
82
+ effort: session.effort,
83
+ access: session.access,
84
+ collaborationMode: planActive ? 'plan' : 'default',
85
+ secretEnvironment,
86
+ ...(personalInstructions === undefined ? {} : { personalInstructions }),
87
+ ...(runner.serviceTier() === 'fast' ? { serviceTier: 'fast' } : {})
88
+ }, {
89
+ runId,
90
+ sessionId: session.id,
91
+ clientMessageId,
92
+ content: runnerInput,
93
+ ...(Number.isSafeInteger(input.__requestUserId)
94
+ ? { initiatedByUserId: input.__requestUserId }
95
+ : {}),
96
+ ...(attachments.length === 0 ? {} : { attachments })
97
+ });
98
+ enqueued = created.created;
99
+ if (!created.created) {
100
+ this.options.attachments.cleanup(runId);
101
+ return {
102
+ session: this.options.present(session),
103
+ run: created.run,
104
+ delivery: this.options.runtime.isQueuedRun(created.run.id) ? 'QUEUED' : 'STARTED',
105
+ idempotent: true
106
+ };
107
+ }
108
+ const acceptedSession = this.options.runtime.setTemporaryTitleIfMissing(session.id, sessionTitleFromFirstMessage(content));
109
+ this.options.emitSession(acceptedSession);
110
+ this.options.attachments.cache(created.run.id, imageAttachments);
111
+ void this.options.uploads.consume(attachmentIds).catch(() => undefined);
112
+ const delivery = this.options.queue.settleEnqueue({
113
+ runId: created.run.id,
114
+ intent,
115
+ queueBefore: queueBeforeCreate,
116
+ leaseWasActive,
117
+ ...(replaceCurrentRun === undefined ? {} : { activeRunId: replaceCurrentRun.id })
118
+ });
119
+ return {
120
+ session: this.options.present(acceptedSession),
121
+ run: created.run,
122
+ delivery,
123
+ idempotent: false
124
+ };
125
+ }
126
+ catch (error) {
127
+ if (!enqueued) {
128
+ this.options.attachments.clear(runId);
129
+ this.options.attachments.cleanup(runId);
130
+ }
131
+ throw error;
132
+ }
133
+ })();
134
+ this.pendingMessages.set(pendingKey, operation);
70
135
  try {
71
- attachmentPaths = await this.persistAttachments(session, created.run.id, attachments);
72
- await this.options.uploads.consume(attachmentIds);
136
+ return await operation;
73
137
  }
74
- catch (error) {
75
- this.options.runtime.deleteQueuedRun(created.run.id);
76
- this.options.attachments.clear(created.run.id);
77
- this.options.attachments.cleanup(created.run.id);
78
- throw error;
138
+ finally {
139
+ if (this.pendingMessages.get(pendingKey) === operation)
140
+ this.pendingMessages.delete(pendingKey);
79
141
  }
80
- const runnerInput = withNonImageAttachmentPrompt(input.content, attachmentPaths);
81
- this.options.runtime.updateQueuedMessage(created.run.id, runnerInput);
82
- const planActive = this.options.commands
83
- .list(session.id)
84
- .some((state) => state.commandId === 'plan');
85
- this.options.queue.remember({
86
- runId: created.run.id,
87
- sessionId: created.run.sessionId,
88
- workspaceId: created.run.workspaceId,
89
- runner: created.run.runner,
90
- input: this.options.runners
91
- .require(created.run.runner)
92
- .prepareMessageInput(runnerInput, planActive ? 'plan' : 'default'),
93
- attachments: runnerImageAttachments,
94
- attachmentPaths,
95
- cwd: session.cwd,
96
- externalSessionId: session.externalSessionId,
97
- model: session.model,
98
- effort: session.effort,
99
- access: session.access,
100
- collaborationMode: planActive ? 'plan' : 'default',
101
- secretEnvironment,
102
- ...(this.options.runners.require(created.run.runner).serviceTier() === 'fast'
103
- ? { serviceTier: 'fast' }
104
- : {})
105
- });
106
- if (queueWasPaused && active === undefined && intent !== 'QUEUE')
107
- this.options.runtime.prioritizeQueuedRun(created.run.id);
108
- if (!queueWasPaused || intent !== 'QUEUE')
109
- this.options.runtime.resumeSessionQueue(session.id);
110
- this.options.emitQueue(session.workspaceId, session.id);
111
- if (replaceCurrentRun !== undefined)
112
- this.options.queue.replace(created.run.id, replaceCurrentRun.id);
113
- else if (intent !== 'QUEUE' || !queuedBeforeCreate)
114
- this.options.queue.schedule(session.id);
115
- return {
116
- session: this.options.present(session),
117
- run: created.run,
118
- delivery: replaceCurrentRun !== undefined ? 'RESTARTING' : queuedBeforeCreate ? 'QUEUED' : 'STARTED',
119
- idempotent: false
120
- };
121
142
  }
122
143
  async persistAttachments(session, runId, attachments) {
123
144
  return this.options.attachments.persistFiles(session.cwd, runId, attachments.filter((attachment) => !isRunnerImageAttachment(attachment)));
@@ -4,6 +4,7 @@ export interface LocalSkillInspection {
4
4
  readonly name: string;
5
5
  readonly description: string;
6
6
  readonly localStatus: LocalSkillStatus;
7
+ readonly skillFilePath: string;
7
8
  readonly install?: SkillInstallMetadata;
8
9
  }
9
10
  export interface LocalSkillTargetInspection {
@@ -53,13 +53,25 @@ async function scanRoot(root, source) {
53
53
  const itemPath = path.join(root, entry.name);
54
54
  const stat = await lstat(itemPath);
55
55
  if (stat.isSymbolicLink() || (!stat.isDirectory() && process.platform === 'win32')) {
56
- results.push({ name: entry.name, description: '', localStatus: 'INVALID_LINK', source });
56
+ results.push({
57
+ name: entry.name,
58
+ description: '',
59
+ localStatus: 'INVALID_LINK',
60
+ skillFilePath: path.join(itemPath, 'SKILL.md'),
61
+ source
62
+ });
57
63
  continue;
58
64
  }
59
65
  if (!stat.isDirectory())
60
66
  continue;
61
67
  if (!SKILL_NAME.test(entry.name) || WINDOWS_DEVICE_NAME.test(entry.name)) {
62
- results.push({ name: entry.name, description: '', localStatus: 'INVALID', source });
68
+ results.push({
69
+ name: entry.name,
70
+ description: '',
71
+ localStatus: 'INVALID',
72
+ skillFilePath: path.join(itemPath, 'SKILL.md'),
73
+ source
74
+ });
63
75
  continue;
64
76
  }
65
77
  results.push({ ...(await inspectSkill(itemPath, entry.name)), source });
@@ -72,17 +84,28 @@ async function inspectSkill(directory, directoryName) {
72
84
  if (frontmatter.name !== directoryName ||
73
85
  !SKILL_NAME.test(frontmatter.name) ||
74
86
  WINDOWS_DEVICE_NAME.test(frontmatter.name))
75
- return { name: directoryName, description: frontmatter.description, localStatus: 'INVALID' };
87
+ return {
88
+ name: directoryName,
89
+ description: frontmatter.description,
90
+ localStatus: 'INVALID',
91
+ skillFilePath: path.join(directory, 'SKILL.md')
92
+ };
76
93
  const install = await readInstallMetadata(path.join(directory, '.mar-skill-install.json'));
77
94
  return {
78
95
  name: directoryName,
79
96
  description: frontmatter.description,
80
97
  localStatus: install === undefined ? 'UNKNOWN_VERSION' : 'VALID',
98
+ skillFilePath: path.join(directory, 'SKILL.md'),
81
99
  ...(install === undefined ? {} : { install })
82
100
  };
83
101
  }
84
102
  catch {
85
- return { name: directoryName, description: '', localStatus: 'INVALID' };
103
+ return {
104
+ name: directoryName,
105
+ description: '',
106
+ localStatus: 'INVALID',
107
+ skillFilePath: path.join(directory, 'SKILL.md')
108
+ };
86
109
  }
87
110
  }
88
111
  async function readInstallMetadata(file) {
@@ -149,7 +172,12 @@ function mergeWorkspaceCopies(agents, claude) {
149
172
  };
150
173
  if (copies.agents.localStatus !== copies.claude.localStatus ||
151
174
  JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
152
- return { name, description: copies.agents.description, localStatus: 'PARTIAL' };
175
+ return {
176
+ name,
177
+ description: copies.agents.description,
178
+ localStatus: 'PARTIAL',
179
+ skillFilePath: copies.agents.skillFilePath
180
+ };
153
181
  return withoutSource(copies.agents);
154
182
  });
155
183
  }
@@ -167,7 +195,12 @@ function mergeNodeCompatibilityRoots(agents, claude) {
167
195
  if (copies.agents.description !== copies.claude.description ||
168
196
  copies.agents.localStatus !== copies.claude.localStatus ||
169
197
  JSON.stringify(copies.agents.install) !== JSON.stringify(copies.claude.install))
170
- return { name, description: copies.agents.description, localStatus: 'PARTIAL' };
198
+ return {
199
+ name,
200
+ description: copies.agents.description,
201
+ localStatus: 'PARTIAL',
202
+ skillFilePath: copies.agents.skillFilePath
203
+ };
171
204
  return withoutSource(copies.agents);
172
205
  });
173
206
  }
@@ -176,6 +209,7 @@ function withoutSource(entry) {
176
209
  name: entry.name,
177
210
  description: entry.description,
178
211
  localStatus: entry.localStatus,
212
+ skillFilePath: entry.skillFilePath,
179
213
  ...(entry.install === undefined ? {} : { install: entry.install })
180
214
  };
181
215
  }
@@ -60,7 +60,7 @@ export class WorkspaceChangeService {
60
60
  additions: null,
61
61
  deletions: null,
62
62
  binary: false,
63
- diffAvailable: change.state !== 'UNTRACKED'
63
+ diffAvailable: true
64
64
  });
65
65
  const value = {
66
66
  branch: root.branch,
@@ -5,6 +5,8 @@ export interface WorkspaceWatch {
5
5
  readonly workspaceId: string;
6
6
  readonly watchId: string;
7
7
  readonly sessionHash: string;
8
+ readonly workbenchConnectionId: string;
9
+ readonly nodeGeneration: string;
8
10
  expiresAt: number;
9
11
  readonly topics: {
10
12
  readonly sessions: boolean;
@@ -56,6 +58,7 @@ export declare class WorkspaceDomainState {
56
58
  readonly fileIndexes: Map<string, WorkspaceFileIndex>;
57
59
  readonly watches: Map<string, Map<string, WorkspaceWatch>>;
58
60
  readonly watchRevisions: Map<string, number>;
61
+ watchSnapshotSequence: number;
59
62
  readonly sessionWatchTimers: Map<string, NodeJS.Timeout>;
60
63
  readonly sessionWatchSignatures: Map<string, string>;
61
64
  readonly sessionWatchReads: Map<string, Promise<void>>;
@@ -7,6 +7,7 @@ export class WorkspaceDomainState {
7
7
  fileIndexes = new Map();
8
8
  watches = new Map();
9
9
  watchRevisions = new Map();
10
+ watchSnapshotSequence = 0;
10
11
  sessionWatchTimers = new Map();
11
12
  sessionWatchSignatures = new Map();
12
13
  sessionWatchReads = new Map();
@@ -9,6 +9,7 @@ export declare class WorkspaceFileService {
9
9
  constructor(state: WorkspaceDomainState, allowedRoots: () => readonly string[], onInvalidated: (workspaceId: string) => void);
10
10
  list(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
11
11
  read(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
12
+ readContent(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
12
13
  search(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
13
14
  mutate(workspace: NodeWorkspace, input: Record<string, unknown>): Promise<unknown>;
14
15
  private serializeMutation;
@@ -1,5 +1,5 @@
1
1
  import { isAbsolute } from 'node:path';
2
- import { listWorkspaceFiles, mutateWorkspaceFile, readAllowedTextFile, readWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
2
+ import { listWorkspaceFiles, mutateWorkspaceFile, readWorkspaceFileContentRange, readAllowedTextFile, readWorkspaceTextFile, WorkspaceFileIndex } from '../workspace.js';
3
3
  const INDEX_CACHE_LIMIT = 8;
4
4
  export class WorkspaceFileService {
5
5
  state;
@@ -36,6 +36,19 @@ export class WorkspaceFileService {
36
36
  ? readAllowedTextFile(input.path, this.allowedRoots(), offset, limit)
37
37
  : readWorkspaceTextFile(workspace.path, input.path, offset, limit);
38
38
  }
39
+ async readContent(workspace, input) {
40
+ if (typeof input.path !== 'string' ||
41
+ (input.offset !== undefined &&
42
+ (!Number.isInteger(input.offset) || input.offset < 0)) ||
43
+ (input.limit !== undefined &&
44
+ (!Number.isInteger(input.limit) ||
45
+ input.limit < 1 ||
46
+ input.limit > 512 * 1024)))
47
+ throw new Error('FILE_RANGE_INVALID');
48
+ if (isAbsolute(input.path))
49
+ throw new Error('FILE_PATH_ESCAPE');
50
+ return readWorkspaceFileContentRange(workspace.path, input.path, typeof input.offset === 'number' ? input.offset : 0, typeof input.limit === 'number' ? input.limit : 512 * 1024);
51
+ }
39
52
  async search(workspace, input) {
40
53
  if (typeof input.query !== 'string' ||
41
54
  (input.cursor !== undefined && typeof input.cursor !== 'string') ||
@@ -1,7 +1,7 @@
1
1
  import type { NodeOperationHandler } from '../connector/node-operation-router.js';
2
2
  import type { NodeRuntimeState } from '../runtime-state.js';
3
3
  import type { RunnerName } from '@myagentroam/protocol';
4
- import type { NodeMcpInstallation } from '../database.js';
4
+ import type { NodeAgentRun, NodeMcpInstallation, NodeMessageAttachmentInput } from '../database.js';
5
5
  export interface QueuedRunStart<TAttachment = unknown> {
6
6
  readonly runId: string;
7
7
  readonly sessionId: string;
@@ -18,6 +18,17 @@ export interface QueuedRunStart<TAttachment = unknown> {
18
18
  readonly collaborationMode: 'default' | 'plan';
19
19
  readonly serviceTier?: 'fast';
20
20
  readonly secretEnvironment: Readonly<Record<string, string>>;
21
+ readonly personalInstructions?: string;
22
+ }
23
+ interface EnqueuedRunDisposition {
24
+ readonly runId: string;
25
+ readonly intent: 'SEND' | 'QUEUE' | 'REPLACE_CURRENT';
26
+ readonly queueBefore: {
27
+ readonly paused: boolean;
28
+ readonly runIds: readonly string[];
29
+ };
30
+ readonly leaseWasActive: boolean;
31
+ readonly activeRunId?: string;
21
32
  }
22
33
  export interface WorkspaceQueueWorkbenchServiceOptions {
23
34
  readonly runtime: NodeRuntimeState;
@@ -35,12 +46,26 @@ export interface WorkspaceQueueWorkbenchServiceOptions {
35
46
  export declare class WorkspaceQueueWorkbenchService<TAttachment> {
36
47
  private readonly options;
37
48
  private readonly starts;
49
+ private readonly scheduledSessions;
38
50
  constructor(options: WorkspaceQueueWorkbenchServiceOptions);
39
- remember(start: QueuedRunStart<TAttachment>): void;
51
+ enqueue(start: QueuedRunStart<TAttachment>, message: {
52
+ readonly runId: string;
53
+ readonly sessionId: string;
54
+ readonly clientMessageId: string;
55
+ readonly content: string;
56
+ readonly initiatedByUserId?: number;
57
+ readonly attachments?: readonly NodeMessageAttachmentInput[];
58
+ }): {
59
+ readonly run: NodeAgentRun;
60
+ readonly created: boolean;
61
+ };
40
62
  pending(runId: string): QueuedRunStart<TAttachment> | undefined;
41
63
  remove(runId: string): void;
42
64
  clear(): void;
65
+ publish(sessionId: string): void;
43
66
  migrateSession(previousId: string, nextId: string): void;
67
+ cancelQueued(runId: string): void;
68
+ pauseForInterrupt(sessionId: string): void;
44
69
  operations(): Readonly<Record<string, NodeOperationHandler>>;
45
70
  private get;
46
71
  private pauseOrResume;
@@ -52,7 +77,9 @@ export declare class WorkspaceQueueWorkbenchService<TAttachment> {
52
77
  private requireRun;
53
78
  replace(queuedRunId: string, activeRunId: string, prioritize?: boolean): void;
54
79
  schedule(sessionId: string): void;
80
+ settleEnqueue(input: EnqueuedRunDisposition): 'STARTED' | 'QUEUED' | 'RESTARTING';
55
81
  dispatch(sessionId: string): Promise<void>;
56
82
  present(workspaceId: string, sessionId: string): unknown;
57
83
  private updatePending;
58
84
  }
85
+ export {};
@@ -5,11 +5,17 @@ import { safeErrorCode } from '../util/safe-error.js';
5
5
  export class WorkspaceQueueWorkbenchService {
6
6
  options;
7
7
  starts = new Map();
8
+ scheduledSessions = new Map();
8
9
  constructor(options) {
9
10
  this.options = options;
10
11
  }
11
- remember(start) {
12
- this.starts.set(start.runId, start);
12
+ enqueue(start, message) {
13
+ if (message.runId !== start.runId || message.sessionId !== start.sessionId)
14
+ throw new Error('QUEUE_RUN_MISMATCH');
15
+ const created = this.options.runtime.createMessageRun(message);
16
+ if (created.created)
17
+ this.starts.set(start.runId, start);
18
+ return created;
13
19
  }
14
20
  pending(runId) {
15
21
  return this.starts.get(runId);
@@ -19,12 +25,44 @@ export class WorkspaceQueueWorkbenchService {
19
25
  }
20
26
  clear() {
21
27
  this.starts.clear();
28
+ for (const timer of this.scheduledSessions.values())
29
+ clearTimeout(timer);
30
+ this.scheduledSessions.clear();
31
+ }
32
+ publish(sessionId) {
33
+ const session = this.options.runtime.getAgentSession(sessionId);
34
+ if (session === undefined)
35
+ throw new Error('SESSION_NOT_FOUND');
36
+ this.options.emit(session.workspaceId, session.id);
22
37
  }
23
38
  migrateSession(previousId, nextId) {
24
39
  for (const [runId, pending] of this.starts) {
25
40
  if (pending.sessionId === previousId)
26
41
  this.starts.set(runId, { ...pending, sessionId: nextId, externalSessionId: nextId });
27
42
  }
43
+ const timer = this.scheduledSessions.get(previousId);
44
+ if (timer !== undefined) {
45
+ clearTimeout(timer);
46
+ this.scheduledSessions.delete(previousId);
47
+ this.schedule(nextId);
48
+ }
49
+ }
50
+ cancelQueued(runId) {
51
+ const run = this.requireRun(runId);
52
+ if (!this.options.runtime.isQueuedRun(run.id))
53
+ throw new Error('QUEUE_ITEM_INVALID');
54
+ this.options.runtime.deleteQueuedRun(run.id);
55
+ this.remove(run.id);
56
+ this.options.clearImages(run.id);
57
+ this.options.cleanupAttachments(run.id);
58
+ this.options.emit(run.workspaceId, run.sessionId);
59
+ }
60
+ pauseForInterrupt(sessionId) {
61
+ const session = this.options.runtime.getAgentSession(sessionId);
62
+ if (session === undefined)
63
+ throw new Error('SESSION_NOT_FOUND');
64
+ this.options.runtime.pauseSessionQueue(session.id);
65
+ this.publish(session.id);
28
66
  }
29
67
  operations() {
30
68
  return {
@@ -77,11 +115,7 @@ export class WorkspaceQueueWorkbenchService {
77
115
  if (typeof input.runId !== 'string')
78
116
  throw new Error('QUEUE_INVALID');
79
117
  const run = this.requireRun(input.runId);
80
- this.options.runtime.deleteQueuedRun(run.id);
81
- this.remove(run.id);
82
- this.options.clearImages(run.id);
83
- this.options.cleanupAttachments(run.id);
84
- this.options.emit(run.workspaceId, run.sessionId);
118
+ this.cancelQueued(run.id);
85
119
  return { queue: this.present(run.workspaceId, run.sessionId) };
86
120
  }
87
121
  execute(input) {
@@ -100,7 +134,6 @@ export class WorkspaceQueueWorkbenchService {
100
134
  .listRunsForSession(session.id)
101
135
  .find((run) => run.status === 'STARTING' || run.status === 'RUNNING');
102
136
  this.options.runtime.prioritizeQueuedRun(queued.id);
103
- this.options.emit(queued.workspaceId, session.id);
104
137
  if (active !== undefined) {
105
138
  this.replace(queued.id, active.id, false);
106
139
  return { run: queued, delivery: 'RESTARTING' };
@@ -132,17 +165,46 @@ export class WorkspaceQueueWorkbenchService {
132
165
  throw new Error('QUEUE_ITEM_INVALID');
133
166
  if (prioritize)
134
167
  this.options.runtime.prioritizeQueuedRun(queued.id);
168
+ this.options.emit(queued.workspaceId, queued.sessionId);
135
169
  this.options.markInterrupted(active.id);
136
170
  this.options.runtime.transitionRun(active.id, 'CANCELLING');
137
171
  this.options.control('run.interrupt', { runId: active.id });
138
172
  }
139
173
  schedule(sessionId) {
174
+ if (this.scheduledSessions.has(sessionId))
175
+ return;
140
176
  const timer = setTimeout(() => {
177
+ this.scheduledSessions.delete(sessionId);
141
178
  if (!this.options.stopped())
142
179
  void this.dispatch(sessionId);
143
180
  }, 0);
181
+ this.scheduledSessions.set(sessionId, timer);
144
182
  timer.unref();
145
183
  }
184
+ settleEnqueue(input) {
185
+ const run = this.requireRun(input.runId);
186
+ const session = this.options.runtime.getAgentSession(run.sessionId);
187
+ if (session === undefined)
188
+ throw new Error('SESSION_NOT_FOUND');
189
+ if (input.queueBefore.paused && input.activeRunId === undefined && input.intent !== 'QUEUE')
190
+ this.options.runtime.prioritizeQueuedRun(run.id);
191
+ if (!input.queueBefore.paused || input.intent !== 'QUEUE')
192
+ this.options.runtime.resumeSessionQueue(session.id);
193
+ if (input.activeRunId !== undefined) {
194
+ this.replace(run.id, input.activeRunId);
195
+ return 'RESTARTING';
196
+ }
197
+ const leaseIsActive = input.leaseWasActive && this.options.runtime.hasActiveSessionLease(session.id);
198
+ const dispatchNow = !leaseIsActive && !(input.queueBefore.paused && input.intent === 'QUEUE');
199
+ const startsNow = dispatchNow &&
200
+ (input.queueBefore.runIds.length === 0 ||
201
+ (input.queueBefore.paused && input.intent !== 'QUEUE'));
202
+ if (dispatchNow)
203
+ this.schedule(session.id);
204
+ else
205
+ this.options.emit(session.workspaceId, session.id);
206
+ return startsNow ? 'STARTED' : 'QUEUED';
207
+ }
146
208
  async dispatch(sessionId) {
147
209
  const run = this.options.runtime.claimNextQueuedRun(sessionId);
148
210
  if (run === undefined)
@@ -181,6 +243,9 @@ export class WorkspaceQueueWorkbenchService {
181
243
  collaborationMode: pending.collaborationMode,
182
244
  ...(pending.serviceTier === undefined ? {} : { serviceTier: pending.serviceTier }),
183
245
  secretEnvironment: pending.secretEnvironment,
246
+ ...(pending.personalInstructions === undefined
247
+ ? {}
248
+ : { personalInstructions: pending.personalInstructions }),
184
249
  mcpInstallations: this.options.mcps(pending.workspaceId)
185
250
  });
186
251
  }
@@ -7,12 +7,17 @@ export declare class WorkspaceWatchService {
7
7
  parseTopics(input: Record<string, unknown>): WorkspaceWatch['topics'];
8
8
  key(input: Record<string, unknown>): string;
9
9
  sessionHash(input: Record<string, unknown>): string;
10
- create(workspaceId: string, watchId: string, sessionHash: string, topics: WorkspaceWatch['topics']): WorkspaceWatch;
11
- renew(workspaceId: string, key: string): WorkspaceWatch | undefined;
10
+ connectionId(input: Record<string, unknown>): string;
11
+ create(workspaceId: string, watchId: string, sessionHash: string, workbenchConnectionId: string, nodeGeneration: string, topics: WorkspaceWatch['topics']): WorkspaceWatch;
12
+ renew(workspaceId: string, key: string, watchId?: string): WorkspaceWatch | undefined;
13
+ matches(workspaceId: string, key: string, watchId: string): boolean;
14
+ existing(workspaceId: string, key: string): WorkspaceWatch | undefined;
12
15
  remove(workspaceId: string, key: string): void;
13
16
  clear(workspaceId: string): void;
14
17
  clearAll(): void;
15
18
  nextRevision(workspaceId: string): number;
19
+ nextSnapshotSequence(): number;
16
20
  topicActive(workspaceId: string, topic: 'files' | 'changes' | 'sessions'): boolean;
21
+ activeWatches(workspaceId: string): readonly WorkspaceWatch[];
17
22
  private expiryTimer;
18
23
  }