@myagentroam/node 0.9.122 → 0.9.125

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 (31) hide show
  1. package/dist/codex-app-server.js +6 -79
  2. package/dist/connector.js +217 -6
  3. package/dist/database.js +2 -1
  4. package/dist/migrations/v014.js +107 -0
  5. package/dist/runner/abstract-runner.js +11 -5
  6. package/dist/runner/claude-code-runner.js +3 -0
  7. package/dist/runner/codex/conversation-parser.js +142 -2
  8. package/dist/runner/codex/managed-run-controller.js +26 -18
  9. package/dist/runner/codex-runner.js +127 -57
  10. package/dist/runner/opencode-runner.js +14 -12
  11. package/dist/runtime-state.js +6 -1
  12. package/dist/service/continuous-run-coordinator.js +8 -2
  13. package/dist/service/conversation-history-service.js +51 -10
  14. package/dist/service/managed-runner-session-service.js +2 -1
  15. package/dist/service/native-session-projection-service.js +3 -1
  16. package/dist/service/node-control-message-service.js +2 -0
  17. package/dist/service/run-event-service.js +18 -2
  18. package/dist/service/runner-interaction-service.js +9 -8
  19. package/dist/service/scheduled-run-authorization-control.js +72 -0
  20. package/dist/service/scheduled-session-coordinator.js +95 -0
  21. package/dist/service/scheduled-session-recurrence.js +72 -0
  22. package/dist/service/scheduled-session-store.js +234 -0
  23. package/dist/service/scheduled-task-mcp-server.js +179 -0
  24. package/dist/service/scheduled-task-mcp-stdio.js +63 -0
  25. package/dist/service/session-catalog-service.js +3 -1
  26. package/dist/service/session-lifecycle-service.js +6 -0
  27. package/dist/service/session-message-service.js +25 -2
  28. package/dist/service/session-presentation-service.js +19 -2
  29. package/dist/service/session-schedule-service.js +153 -0
  30. package/dist/util/runner-native-session-parsers.js +19 -11
  31. package/package.json +4 -4
@@ -0,0 +1,234 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { assertScheduleTimezone, nextScheduledAt, parseScheduledCron } from './scheduled-session-recurrence.js';
3
+ export class ScheduledSessionStore {
4
+ database;
5
+ now;
6
+ constructor(database, now = Date.now) {
7
+ this.database = database;
8
+ this.now = now;
9
+ }
10
+ get(sessionId) {
11
+ const row = this.database
12
+ .prepare('SELECT * FROM scheduled_session_tasks WHERE session_id=?')
13
+ .get(sessionId);
14
+ return row === undefined ? undefined : taskFromRow(row);
15
+ }
16
+ getById(taskId) {
17
+ const row = this.database
18
+ .prepare('SELECT * FROM scheduled_session_tasks WHERE id=?')
19
+ .get(taskId);
20
+ return row === undefined ? undefined : taskFromRow(row);
21
+ }
22
+ listDue(at = this.now()) {
23
+ return this.database
24
+ .prepare('SELECT * FROM scheduled_session_tasks WHERE enabled=1 AND next_run_at<=? ORDER BY next_run_at,id')
25
+ .all(at).map(taskFromRow);
26
+ }
27
+ nextDueAt() {
28
+ const row = this.database
29
+ .prepare('SELECT MIN(next_run_at) AS value FROM scheduled_session_tasks WHERE enabled=1')
30
+ .get();
31
+ return typeof row['value'] === 'number' ? row['value'] : null;
32
+ }
33
+ create(input) {
34
+ validate(input.objective, input.cron, input.timezone);
35
+ if (!Number.isSafeInteger(input.ownerUserId) || input.ownerUserId < 1)
36
+ throw new Error('SCHEDULE_OWNER_INVALID');
37
+ const now = this.now();
38
+ const task = {
39
+ sessionId: input.sessionId,
40
+ workspaceId: input.workspaceId,
41
+ runner: input.runner,
42
+ ownerUserId: input.ownerUserId,
43
+ cron: input.cron,
44
+ timezone: input.timezone,
45
+ objective: input.objective.trim(),
46
+ id: randomUUID(),
47
+ enabled: input.enabled !== false,
48
+ revision: 1,
49
+ nextRunAt: input.enabled === false ? null : nextScheduledAt(input.cron, input.timezone, now),
50
+ createdAt: now,
51
+ updatedAt: now
52
+ };
53
+ this.database
54
+ .prepare(`INSERT INTO scheduled_session_tasks
55
+ (id,session_id,workspace_id,runner,owner_user_id,objective,cron,timezone,enabled,revision,next_run_at,created_at,updated_at)
56
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`)
57
+ .run(task.id, task.sessionId, task.workspaceId, task.runner, task.ownerUserId, task.objective, task.cron, task.timezone, task.enabled ? 1 : 0, 1, task.nextRunAt, now, now);
58
+ return task;
59
+ }
60
+ update(sessionId, expectedRevision, patch) {
61
+ const current = this.get(sessionId);
62
+ if (current === undefined)
63
+ throw new Error('SCHEDULE_NOT_FOUND');
64
+ if (current.revision !== expectedRevision)
65
+ throw new Error('SCHEDULE_REVISION_CONFLICT');
66
+ const objective = patch.objective ?? current.objective;
67
+ const cron = patch.cron ?? current.cron;
68
+ const timezone = patch.timezone ?? current.timezone;
69
+ validate(objective, cron, timezone);
70
+ const enabled = patch.enabled ?? current.enabled;
71
+ const nextRunAt = enabled
72
+ ? patch.enabled === true || patch.cron !== undefined || patch.timezone !== undefined
73
+ ? nextScheduledAt(cron, timezone, this.now())
74
+ : current.nextRunAt
75
+ : null;
76
+ const now = this.now();
77
+ const result = this.database
78
+ .prepare(`UPDATE scheduled_session_tasks SET objective=?,cron=?,timezone=?,enabled=?,revision=?,next_run_at=?,updated_at=?
79
+ WHERE session_id=? AND revision=?`)
80
+ .run(objective.trim(), cron, timezone, enabled ? 1 : 0, expectedRevision + 1, nextRunAt, now, sessionId, expectedRevision);
81
+ if (result.changes !== 1)
82
+ throw new Error('SCHEDULE_REVISION_CONFLICT');
83
+ this.database
84
+ .prepare(`UPDATE scheduled_session_proposals SET status='STALE',resolved_at=?
85
+ WHERE task_id=? AND status='PENDING' AND base_revision<?`)
86
+ .run(now, current.id, expectedRevision + 1);
87
+ this.database
88
+ .prepare("DELETE FROM scheduled_session_proposals WHERE task_id=? AND status='STALE'")
89
+ .run(current.id);
90
+ return this.get(sessionId);
91
+ }
92
+ remove(sessionId, expectedRevision) {
93
+ const result = this.database
94
+ .prepare('DELETE FROM scheduled_session_tasks WHERE session_id=? AND revision=?')
95
+ .run(sessionId, expectedRevision);
96
+ if (result.changes !== 1)
97
+ throw new Error('SCHEDULE_REVISION_CONFLICT');
98
+ }
99
+ claim(sessionId, dueAt, expectedRevision) {
100
+ const current = this.get(sessionId);
101
+ if (current === undefined ||
102
+ !current.enabled ||
103
+ current.revision !== expectedRevision ||
104
+ current.nextRunAt !== dueAt)
105
+ return undefined;
106
+ const next = nextScheduledAt(current.cron, current.timezone, Math.max(this.now(), dueAt));
107
+ const id = `${current.id}:${dueAt}`;
108
+ this.database.exec('BEGIN IMMEDIATE');
109
+ try {
110
+ const result = this.database
111
+ .prepare('UPDATE scheduled_session_tasks SET next_run_at=?,updated_at=? WHERE id=? AND revision=? AND next_run_at=?')
112
+ .run(next, this.now(), current.id, expectedRevision, dueAt);
113
+ if (result.changes !== 1) {
114
+ this.database.exec('ROLLBACK');
115
+ return undefined;
116
+ }
117
+ this.database
118
+ .prepare(`INSERT INTO scheduled_session_occurrences
119
+ (id,task_id,due_at,status,created_at,updated_at) VALUES (?,?,?,'CLAIMED',?,?)`)
120
+ .run(id, current.id, dueAt, this.now(), this.now());
121
+ this.database.exec('COMMIT');
122
+ return id;
123
+ }
124
+ catch (error) {
125
+ this.database.exec('ROLLBACK');
126
+ throw error;
127
+ }
128
+ }
129
+ finish(occurrenceId, status, code, runId) {
130
+ const result = this.database
131
+ .prepare(`UPDATE scheduled_session_occurrences SET status=?,code=?,run_id=?,updated_at=?
132
+ WHERE id=? AND status IN ('CLAIMED','SUBMITTED')`)
133
+ .run(status, code ?? null, runId ?? null, this.now(), occurrenceId);
134
+ if (result.changes === 0 || status === 'SUBMITTED')
135
+ return;
136
+ this.database
137
+ .prepare(`DELETE FROM scheduled_session_occurrences
138
+ WHERE task_id=(SELECT task_id FROM scheduled_session_occurrences WHERE id=?)
139
+ AND status NOT IN ('CLAIMED','SUBMITTED')
140
+ AND id NOT IN (SELECT id FROM scheduled_session_occurrences
141
+ WHERE task_id=(SELECT task_id FROM scheduled_session_occurrences WHERE id=?)
142
+ AND status NOT IN ('CLAIMED','SUBMITTED') ORDER BY due_at DESC LIMIT 100)`)
143
+ .run(occurrenceId, occurrenceId);
144
+ }
145
+ recent(taskId, limit = 20) {
146
+ return this.database
147
+ .prepare('SELECT * FROM scheduled_session_occurrences WHERE task_id=? ORDER BY due_at DESC LIMIT ?')
148
+ .all(taskId, Math.max(1, Math.min(limit, 100)));
149
+ }
150
+ pendingProposals(taskId) {
151
+ return this.database
152
+ .prepare(`SELECT * FROM scheduled_session_proposals
153
+ WHERE task_id=? AND status='PENDING' ORDER BY created_at DESC LIMIT 50`)
154
+ .all(taskId);
155
+ }
156
+ propose(task, actorUserId, patch) {
157
+ if (!Number.isSafeInteger(actorUserId) || actorUserId < 1 || actorUserId === task.ownerUserId)
158
+ throw new Error('SCHEDULE_PROPOSAL_INVALID');
159
+ if (patch.objective === undefined && patch.cron === undefined && patch.timezone === undefined)
160
+ throw new Error('SCHEDULE_PROPOSAL_INVALID');
161
+ validate(patch.objective ?? task.objective, patch.cron ?? task.cron, patch.timezone ?? task.timezone);
162
+ const pending = this.database
163
+ .prepare("SELECT COUNT(*) AS count FROM scheduled_session_proposals WHERE task_id=? AND status='PENDING'")
164
+ .get(task.id);
165
+ if (pending.count >= 50)
166
+ throw new Error('SCHEDULE_PROPOSAL_LIMIT');
167
+ const id = randomUUID();
168
+ this.database
169
+ .prepare(`INSERT INTO scheduled_session_proposals
170
+ (id,task_id,proposed_by_user_id,base_revision,objective,cron,timezone,status,created_at)
171
+ VALUES (?,?,?,?,?,?,?,'PENDING',?)`)
172
+ .run(id, task.id, actorUserId, task.revision, patch.objective ?? null, patch.cron ?? null, patch.timezone ?? null, this.now());
173
+ return id;
174
+ }
175
+ resolveProposal(sessionId, proposalId, ownerUserId, accept) {
176
+ const task = this.get(sessionId);
177
+ if (task === undefined)
178
+ throw new Error('SCHEDULE_NOT_FOUND');
179
+ if (task.ownerUserId !== ownerUserId)
180
+ throw new Error('SCHEDULE_OWNER_REQUIRED');
181
+ const proposal = this.database
182
+ .prepare('SELECT * FROM scheduled_session_proposals WHERE id=? AND task_id=?')
183
+ .get(proposalId, task.id);
184
+ if (proposal === undefined || proposal['status'] !== 'PENDING')
185
+ throw new Error('SCHEDULE_PROPOSAL_NOT_FOUND');
186
+ if (proposal['base_revision'] !== task.revision)
187
+ throw new Error('SCHEDULE_REVISION_CONFLICT');
188
+ this.database.exec('BEGIN IMMEDIATE');
189
+ try {
190
+ const updated = accept
191
+ ? this.update(sessionId, task.revision, {
192
+ ...(proposal['objective'] === null
193
+ ? {}
194
+ : { objective: proposal['objective'] }),
195
+ ...(proposal['cron'] === null ? {} : { cron: proposal['cron'] }),
196
+ ...(proposal['timezone'] === null ? {} : { timezone: proposal['timezone'] })
197
+ })
198
+ : task;
199
+ this.database
200
+ .prepare('UPDATE scheduled_session_proposals SET status=?,resolved_at=? WHERE id=?')
201
+ .run(accept ? 'ACCEPTED' : 'REJECTED', this.now(), proposalId);
202
+ this.database.prepare('DELETE FROM scheduled_session_proposals WHERE id=?').run(proposalId);
203
+ this.database.exec('COMMIT');
204
+ return updated;
205
+ }
206
+ catch (error) {
207
+ this.database.exec('ROLLBACK');
208
+ throw error;
209
+ }
210
+ }
211
+ }
212
+ function validate(objective, cron, timezone) {
213
+ if (objective.trim().length < 1 || objective.trim().length > 20_000)
214
+ throw new Error('SCHEDULE_OBJECTIVE_INVALID');
215
+ parseScheduledCron(cron);
216
+ assertScheduleTimezone(timezone);
217
+ }
218
+ function taskFromRow(row) {
219
+ return {
220
+ id: row['id'],
221
+ sessionId: row['session_id'],
222
+ workspaceId: row['workspace_id'],
223
+ runner: row['runner'],
224
+ ownerUserId: Number(row['owner_user_id']),
225
+ objective: row['objective'],
226
+ cron: row['cron'],
227
+ timezone: row['timezone'],
228
+ enabled: row['enabled'] === 1,
229
+ revision: Number(row['revision']),
230
+ nextRunAt: row['next_run_at'] === null ? null : Number(row['next_run_at']),
231
+ createdAt: Number(row['created_at']),
232
+ updatedAt: Number(row['updated_at'])
233
+ };
234
+ }
@@ -0,0 +1,179 @@
1
+ import { randomBytes, randomUUID } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { unlink } from 'node:fs/promises';
4
+ import { createServer } from 'node:net';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ export class ScheduledTaskMcpServer {
9
+ store;
10
+ changed;
11
+ proposed;
12
+ create;
13
+ resolveSessionId;
14
+ #capabilities = new Map();
15
+ #sockets = new Set();
16
+ #endpoint = process.platform === 'win32'
17
+ ? `\\\\.\\pipe\\mar-schedule-${process.pid}-${randomUUID()}`
18
+ : join(tmpdir(), `mar-schedule-${process.pid}-${randomUUID()}.sock`);
19
+ #listening;
20
+ constructor(store, changed, proposed, create, resolveSessionId) {
21
+ this.store = store;
22
+ this.changed = changed;
23
+ this.proposed = proposed;
24
+ this.create = create;
25
+ this.resolveSessionId = resolveSessionId;
26
+ }
27
+ async installation(capability) {
28
+ await this.#listen();
29
+ const token = randomBytes(32).toString('base64url');
30
+ const secretName = `MAR_SCHEDULE_MCP_${randomBytes(16).toString('hex').toUpperCase()}`;
31
+ this.#capabilities.set(token, capability);
32
+ return {
33
+ installation: {
34
+ __marScheduled: true,
35
+ serverName: 'mar_schedule',
36
+ enabled: true,
37
+ command: process.execPath,
38
+ args: scheduledMcpStdioArgs(),
39
+ endpoint: this.#endpoint,
40
+ secretName,
41
+ readOnly: capability.readonly
42
+ },
43
+ secrets: { [secretName]: token }
44
+ };
45
+ }
46
+ revoke(executionId) {
47
+ for (const [token, capability] of this.#capabilities)
48
+ if (capability.executionId === executionId)
49
+ this.#capabilities.delete(token);
50
+ }
51
+ async close() {
52
+ if (this.#listening === undefined)
53
+ return;
54
+ const server = await this.#listening;
55
+ this.#capabilities.clear();
56
+ const closed = new Promise((resolve, reject) => server.close((error) => (error === undefined ? resolve() : reject(error))));
57
+ for (const socket of this.#sockets)
58
+ socket.destroy();
59
+ await closed;
60
+ if (process.platform !== 'win32')
61
+ await unlink(this.#endpoint).catch(() => undefined);
62
+ }
63
+ #listen() {
64
+ this.#listening ??= new Promise((resolve, reject) => {
65
+ const server = createServer((socket) => this.#handle(socket));
66
+ server.once('error', reject);
67
+ server.listen(this.#endpoint, () => resolve(server));
68
+ });
69
+ return this.#listening;
70
+ }
71
+ #handle(socket) {
72
+ this.#sockets.add(socket);
73
+ socket.once('close', () => this.#sockets.delete(socket));
74
+ let body = '';
75
+ socket.setEncoding('utf8');
76
+ socket.on('data', (chunk) => {
77
+ body += chunk;
78
+ if (body.length > 64_000) {
79
+ socket.destroy();
80
+ return;
81
+ }
82
+ if (body.includes('\n'))
83
+ void this.#respond(socket, body.slice(0, body.indexOf('\n')));
84
+ });
85
+ }
86
+ async #respond(socket, body) {
87
+ try {
88
+ const request = JSON.parse(body);
89
+ const capability = typeof request.token === 'string' ? this.#capabilities.get(request.token) : undefined;
90
+ if (capability === undefined)
91
+ throw new Error('SCHEDULE_CAPABILITY_INVALID');
92
+ const sessionId = this.resolveSessionId?.(capability.sessionId) ?? capability.sessionId;
93
+ const task = this.store().get(sessionId);
94
+ if ((task === undefined && capability.taskId !== undefined) ||
95
+ (task !== undefined &&
96
+ (task.id !== capability.taskId || task.revision !== capability.revision)))
97
+ throw new Error('SCHEDULE_CAPABILITY_STALE');
98
+ if (request.tool === 'mar_get_scheduled_task') {
99
+ socket.end(`${JSON.stringify({ result: task ?? null })}\n`);
100
+ return;
101
+ }
102
+ if (request.tool === 'mar_create_scheduled_task') {
103
+ if (capability.readonly)
104
+ throw new Error('SCHEDULE_READ_ONLY');
105
+ if (task !== undefined)
106
+ throw new Error('SCHEDULE_ALREADY_EXISTS');
107
+ const args = record(request.arguments);
108
+ if (typeof args.objective !== 'string' ||
109
+ typeof args.cron !== 'string' ||
110
+ capability.workspaceId === undefined ||
111
+ this.create === undefined)
112
+ throw new Error('SCHEDULE_INVALID');
113
+ const created = await this.create({
114
+ sessionId: capability.sessionId,
115
+ workspaceId: capability.workspaceId,
116
+ actorUserId: capability.actorUserId,
117
+ objective: args.objective,
118
+ cron: args.cron
119
+ });
120
+ this.#capabilities.set(request.token, {
121
+ ...capability,
122
+ sessionId: created.sessionId,
123
+ taskId: created.id,
124
+ revision: created.revision
125
+ });
126
+ socket.end(`${JSON.stringify({ result: created })}\n`);
127
+ return;
128
+ }
129
+ if (request.tool !== 'mar_update_scheduled_task')
130
+ throw new Error('SCHEDULE_TOOL_INVALID');
131
+ if (capability.readonly)
132
+ throw new Error('SCHEDULE_READ_ONLY');
133
+ if (task === undefined)
134
+ throw new Error('SCHEDULE_NOT_FOUND');
135
+ const args = record(request.arguments);
136
+ const objective = args.objective;
137
+ const cron = args.cron;
138
+ if ((objective !== undefined && typeof objective !== 'string') ||
139
+ (cron !== undefined && typeof cron !== 'string') ||
140
+ (objective === undefined && cron === undefined))
141
+ throw new Error('SCHEDULE_UPDATE_INVALID');
142
+ const patch = {
143
+ ...(objective === undefined ? {} : { objective: objective }),
144
+ ...(cron === undefined ? {} : { cron: cron })
145
+ };
146
+ if (capability.actorUserId === task.ownerUserId) {
147
+ const updated = this.store().update(task.sessionId, task.revision, patch);
148
+ this.#capabilities.set(request.token, {
149
+ ...capability,
150
+ revision: updated.revision
151
+ });
152
+ this.changed(task.sessionId);
153
+ socket.end(`${JSON.stringify({ result: updated })}\n`);
154
+ }
155
+ else {
156
+ const proposalId = this.store().propose(task, capability.actorUserId, patch);
157
+ this.proposed(task.sessionId, proposalId);
158
+ socket.end(`${JSON.stringify({ result: { proposalId, status: 'PENDING' } })}\n`);
159
+ }
160
+ }
161
+ catch (error) {
162
+ socket.end(`${JSON.stringify({ error: error instanceof Error ? error.message : 'SCHEDULE_IPC_FAILED' })}\n`);
163
+ }
164
+ }
165
+ }
166
+ function record(value) {
167
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
168
+ ? value
169
+ : {};
170
+ }
171
+ function scheduledMcpStdioArgs() {
172
+ const javascript = fileURLToPath(new URL('./scheduled-task-mcp-stdio.js', import.meta.url));
173
+ if (existsSync(javascript))
174
+ return [javascript];
175
+ const typescript = fileURLToPath(new URL('./scheduled-task-mcp-stdio.ts', import.meta.url));
176
+ if (!existsSync(typescript))
177
+ throw new Error('SCHEDULE_MCP_STDIO_ENTRY_UNAVAILABLE');
178
+ return ['--import', import.meta.resolve('tsx'), typescript];
179
+ }
@@ -0,0 +1,63 @@
1
+ import { connect } from 'node:net';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { z } from 'zod';
5
+ const endpoint = process.env['MAR_SCHEDULE_MCP_ENDPOINT'];
6
+ const token = process.env['MAR_SCHEDULE_MCP_TOKEN'];
7
+ if (endpoint === undefined || token === undefined)
8
+ throw new Error('SCHEDULE_CAPABILITY_INVALID');
9
+ const server = new McpServer({ name: 'mar_schedule', version: '1.0.0' });
10
+ server.registerTool('mar_get_scheduled_task', {
11
+ description: 'Read the MAR scheduled task for this MAR-managed Workspace conversation, or null if none exists.'
12
+ }, async () => result(await request('mar_get_scheduled_task', {})));
13
+ if (process.env['MAR_SCHEDULE_MCP_READ_ONLY'] !== '1') {
14
+ server.registerTool('mar_create_scheduled_task', {
15
+ description: 'Create a MAR scheduled task for this MAR-managed Workspace conversation. MAR runs it in this conversation using the connected Node time zone.',
16
+ inputSchema: {
17
+ objective: z.string().min(1).max(20_000).describe('Prompt to run at each trigger.'),
18
+ cron: z
19
+ .string()
20
+ .min(9)
21
+ .max(64)
22
+ .describe('Five-field cron expression interpreted in the connected MAR Node time zone.')
23
+ }
24
+ }, async (args) => result(await request('mar_create_scheduled_task', args)));
25
+ server.registerTool('mar_update_scheduled_task', {
26
+ description: 'Update the MAR scheduled task for this MAR-managed Workspace conversation. Its cron expression uses the connected Node time zone.',
27
+ inputSchema: {
28
+ objective: z.string().min(1).max(20_000).describe('Replacement prompt.').optional(),
29
+ cron: z
30
+ .string()
31
+ .min(9)
32
+ .max(64)
33
+ .describe('Replacement five-field cron expression in the connected MAR Node time zone.')
34
+ .optional()
35
+ }
36
+ }, async (args) => result(await request('mar_update_scheduled_task', args)));
37
+ }
38
+ await server.connect(new StdioServerTransport());
39
+ function request(tool, args) {
40
+ return new Promise((resolve, reject) => {
41
+ const socket = connect(endpoint);
42
+ let body = '';
43
+ socket.setEncoding('utf8');
44
+ socket.once('connect', () => socket.end(`${JSON.stringify({ token, tool, arguments: args })}\n`));
45
+ socket.on('data', (chunk) => (body += chunk));
46
+ socket.once('error', reject);
47
+ socket.once('end', () => {
48
+ try {
49
+ const payload = JSON.parse(body);
50
+ if (payload.error !== undefined)
51
+ reject(new Error(payload.error));
52
+ else
53
+ resolve(payload.result);
54
+ }
55
+ catch (error) {
56
+ reject(error);
57
+ }
58
+ });
59
+ });
60
+ }
61
+ function result(value) {
62
+ return { content: [{ type: 'text', text: JSON.stringify(value) }] };
63
+ }
@@ -38,7 +38,9 @@ export class SessionCatalogService {
38
38
  source?.runnerTitle === undefined ||
39
39
  source.runnerTitle === null
40
40
  ? session
41
- : this.options.runtime.setRunnerTitleFromNative(session.id, source.runnerTitle);
41
+ : source.titleSource === 'AUTO'
42
+ ? this.options.runtime.setTemporaryTitleIfMissing(session.id, source.runnerTitle)
43
+ : this.options.runtime.setRunnerTitleFromNative(session.id, source.runnerTitle);
42
44
  return this.options.present(source === undefined
43
45
  ? synchronized
44
46
  : {
@@ -1,6 +1,7 @@
1
1
  import { validSessionEffort, validSessionOption, validSessionTitle } from '../util/node-operation-parsers.js';
2
2
  import { isPlainRecord } from '../util/node-operation-parsers.js';
3
3
  import { parseSecretEnvironment } from '../util/secret-environment.js';
4
+ import { ScheduledSessionStore } from './scheduled-session-store.js';
4
5
  export class SessionLifecycleService {
5
6
  options;
6
7
  starts = new Map();
@@ -34,6 +35,11 @@ export class SessionLifecycleService {
34
35
  (await this.options.projection.createMetadataProjection(input.sessionId));
35
36
  if (current === undefined)
36
37
  throw new Error('SESSION_NOT_FOUND');
38
+ if (input.pinned === false) {
39
+ const task = new ScheduledSessionStore(this.options.database().raw).get(current.externalSessionId ?? current.id);
40
+ if (task?.enabled === true)
41
+ throw new Error('SCHEDULE_PIN_REQUIRED');
42
+ }
37
43
  const nativeCodexRename = this.options.runners.require(current.runner).usesRunnerTitleForRename() &&
38
44
  current.externalSessionId !== null &&
39
45
  input.customTitle !== undefined;
@@ -5,6 +5,7 @@ import { personalInstructionsForRunner } from '../util/personal-instructions.js'
5
5
  import { safeErrorCode } from '../util/safe-error.js';
6
6
  import { publicGoalStartText } from './goal-runtime.js';
7
7
  const INTERNAL_GOAL = Symbol('internalGoal');
8
+ const INTERNAL_SCHEDULE = Symbol('internalSchedule');
8
9
  export class SessionMessageService {
9
10
  options;
10
11
  pendingMessages = new Map();
@@ -37,6 +38,22 @@ export class SessionMessageService {
37
38
  marAgentRuntimeScopeId: input.marAgentRuntimeScopeId
38
39
  });
39
40
  }
41
+ submitScheduled(input) {
42
+ return this.message({
43
+ [INTERNAL_SCHEDULE]: true,
44
+ sessionId: input.sessionId,
45
+ clientMessageId: input.occurrenceId,
46
+ content: input.objective,
47
+ __runnerContent: `Scheduled task execution:\n${input.objective}`,
48
+ __requestUserId: input.initiatedByUserId,
49
+ deliveryIntent: 'SEND',
50
+ secretEnvironment: input.secretEnvironment,
51
+ personalInstructions: input.personalInstructions,
52
+ marAgentModel: input.marAgentModel,
53
+ marAgentModels: input.marAgentModels,
54
+ marAgentRuntimeScopeId: input.marAgentRuntimeScopeId
55
+ });
56
+ }
40
57
  async message(input) {
41
58
  if (typeof input.sessionId !== 'string' ||
42
59
  typeof input.clientMessageId !== 'string' ||
@@ -47,7 +64,10 @@ export class SessionMessageService {
47
64
  const clientMessageId = input.clientMessageId;
48
65
  const content = input.content;
49
66
  const internalGoal = input[INTERNAL_GOAL] === true;
50
- const internalRunnerContent = internalGoal && typeof input.__runnerContent === 'string' ? input.__runnerContent : content;
67
+ const internalSchedule = input[INTERNAL_SCHEDULE] === true;
68
+ const internalRunnerContent = (internalGoal || internalSchedule) && typeof input.__runnerContent === 'string'
69
+ ? input.__runnerContent
70
+ : content;
51
71
  const inlineAttachments = parseMessageAttachments(input.attachments);
52
72
  const secretEnvironment = parseSecretEnvironment(input.secretEnvironment);
53
73
  const attachmentIds = parseComposerAttachmentIds(input.attachmentIds);
@@ -80,7 +100,7 @@ export class SessionMessageService {
80
100
  idempotent: true,
81
101
  auditReceipt: auditReceipt(session, existing.kind === 'queue' ? 'QUEUE' : 'SEND', existing.run.id, existing.kind === 'queue' ? existing.item.id : undefined)
82
102
  };
83
- const goalMessage = internalGoal ? undefined : this.options.goalMessage(session, content);
103
+ const goalMessage = internalGoal || internalSchedule ? undefined : this.options.goalMessage(session, content);
84
104
  const runnerContent = goalMessage?.runnerContent ?? internalRunnerContent;
85
105
  const publicContent = goalMessage?.activated === true ? publicGoalStartText(content) : content;
86
106
  const intent = goalMessage === undefined ? requestedIntent : 'REPLACE_CURRENT';
@@ -90,12 +110,15 @@ export class SessionMessageService {
90
110
  const runId = randomUUID();
91
111
  const queueBeforeCreate = this.options.runtime.sessionQueue(session.id);
92
112
  const active = this.options.runtime.activeSessionRun(session.id);
113
+ if (internalSchedule && (active !== undefined || queueBeforeCreate.itemIds.length > 0))
114
+ throw new Error('SCHEDULE_SESSION_BUSY');
93
115
  const replaceCurrentRun = intent === 'REPLACE_CURRENT' ? active : undefined;
94
116
  const created = this.options.coordinator.accept({
95
117
  runId,
96
118
  sessionId: session.id,
97
119
  clientMessageId,
98
120
  content: publicContent,
121
+ ...(internalSchedule ? { source: 'SCHEDULED' } : {}),
99
122
  ...(Number.isSafeInteger(input.__requestUserId)
100
123
  ? { initiatedByUserId: input.__requestUserId }
101
124
  : {}),
@@ -1,3 +1,4 @@
1
+ import { ScheduledSessionStore } from './scheduled-session-store.js';
1
2
  export class SessionPresentationService {
2
3
  options;
3
4
  constructor(options) {
@@ -18,7 +19,21 @@ export class SessionPresentationService {
18
19
  });
19
20
  return {
20
21
  ...session,
21
- ...presentation
22
+ ...presentation,
23
+ runtime: {
24
+ ...presentation.runtime,
25
+ capabilities: session.externalSessionId !== null && database !== undefined
26
+ ? [...presentation.runtime.capabilities, 'session.schedule']
27
+ : presentation.runtime.capabilities
28
+ },
29
+ schedule: database === undefined
30
+ ? null
31
+ : (() => {
32
+ const task = new ScheduledSessionStore(database.raw).get(session.externalSessionId ?? session.id);
33
+ return task === undefined
34
+ ? null
35
+ : { enabled: task.enabled, nextRunAt: task.nextRunAt };
36
+ })()
22
37
  };
23
38
  }
24
39
  presentDiscovered(sessions) {
@@ -28,7 +43,9 @@ export class SessionPresentationService {
28
43
  return this.present(session);
29
44
  const synchronized = session.runnerTitle === null
30
45
  ? projected
31
- : this.options.runtime.setRunnerTitleFromNative(projected.id, session.runnerTitle);
46
+ : session.titleSource === 'AUTO'
47
+ ? this.options.runtime.setTemporaryTitleIfMissing(projected.id, session.runnerTitle)
48
+ : this.options.runtime.setRunnerTitleFromNative(projected.id, session.runnerTitle);
32
49
  return this.present({
33
50
  ...synchronized,
34
51
  lastActivityAt: latestSessionActivityAt(synchronized.lastActivityAt, session.lastActivityAt)