@myagentroam/node 0.10.47 → 0.10.49
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.
- package/dist/connector.js +39 -1
- package/dist/runner/abstract-runner.js +6 -0
- package/dist/runner/mar-agent/conversation-normalizer.js +17 -0
- package/dist/runner/mar-agent/history-projection.js +6 -3
- package/dist/runner/mar-agent/hosted-conversation-runtime.js +15 -1
- package/dist/runner/mar-agent/managed-run-controller.js +32 -2
- package/dist/runner/mar-agent/marctl-skill.js +4 -1
- package/dist/runtime-state.js +47 -4
- package/dist/service/continuous-run-coordinator.js +101 -0
- package/dist/service/hosted-conversation-node-service.js +42 -1
- package/dist/service/hosted-conversation-store.js +5 -0
- package/dist/service/node-run-event-bridge.js +3 -1
- package/dist/service/schedule-mcp-bridge.js +8 -22
- package/dist/service/scheduled-session-store.js +56 -0
- package/dist/service/session-message-service.js +7 -1
- package/dist/service/session-schedule-service.js +55 -2
- package/dist/service/workspace-queue-workbench-service.js +2 -2
- package/package.json +4 -4
package/dist/connector.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeMarAgentUsage } from './runner/mar-agent/conversation-normalizer.js';
|
|
2
|
+
import { isImageAttachment } from './util/node-operation-parsers.js';
|
|
2
3
|
import { ModelUsageOutbox } from './service/model-usage-outbox.js';
|
|
3
4
|
import { NodeModelUsageService } from './service/model-usage-service.js';
|
|
4
5
|
import { dirname, resolve } from 'node:path';
|
|
@@ -801,6 +802,41 @@ export class NodeConnector {
|
|
|
801
802
|
});
|
|
802
803
|
this.continuousRunCoordinator = new ContinuousRunCoordinator({
|
|
803
804
|
runtime: this.runtime,
|
|
805
|
+
steer: (prepared, item, executionId) => {
|
|
806
|
+
const runner = this.runners.optional(prepared.runner);
|
|
807
|
+
if (!runner?.supportsNativeSteer())
|
|
808
|
+
return undefined;
|
|
809
|
+
const images = item.attachments.filter(isImageAttachment).map((image) => ({
|
|
810
|
+
name: image.name,
|
|
811
|
+
mimeType: image.mime,
|
|
812
|
+
dataBase64: image.dataBase64
|
|
813
|
+
}));
|
|
814
|
+
return runner
|
|
815
|
+
.steerManagedRun({
|
|
816
|
+
expectedExecutionId: executionId,
|
|
817
|
+
prompt: item.content,
|
|
818
|
+
modelText: prepared.input,
|
|
819
|
+
clientMessageId: item.clientMessageId,
|
|
820
|
+
model: prepared.model,
|
|
821
|
+
effort: prepared.effort,
|
|
822
|
+
access: prepared.access,
|
|
823
|
+
collaborationMode: prepared.collaborationMode,
|
|
824
|
+
images
|
|
825
|
+
})
|
|
826
|
+
.then((result) => {
|
|
827
|
+
if (result.status !== 'accepted')
|
|
828
|
+
return result;
|
|
829
|
+
const attachments = this.runAttachmentService.append(executionId, item.attachments.filter(isImageAttachment));
|
|
830
|
+
return {
|
|
831
|
+
...result,
|
|
832
|
+
item: {
|
|
833
|
+
...result.item,
|
|
834
|
+
merge: true,
|
|
835
|
+
payload: { ...result.item.payload, ...(attachments.length ? { attachments } : {}) }
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
},
|
|
804
840
|
emitQueue: (workspaceId, sessionId) => {
|
|
805
841
|
this.runEventBridge.emitQueue(workspaceId, sessionId);
|
|
806
842
|
this.runAttachmentService.refreshSession(sessionId);
|
|
@@ -1248,6 +1284,7 @@ export class NodeConnector {
|
|
|
1248
1284
|
respondUserInput: (runId, requestId, answers) => this.openCodeRunController.respondUserInput(runId, requestId, answers)
|
|
1249
1285
|
});
|
|
1250
1286
|
this.marAgentClient.bindControl({
|
|
1287
|
+
steer: (input) => this.marAgentRunController.steer(input),
|
|
1251
1288
|
decideApproval: (input) => this.marAgentRunController.decideApproval(input),
|
|
1252
1289
|
start: (envelope) => this.marAgentRunController.start(envelope),
|
|
1253
1290
|
cancel: (runId, intent) => this.marAgentRunController.cancel(runId, intent),
|
|
@@ -1619,7 +1656,8 @@ export class NodeConnector {
|
|
|
1619
1656
|
emitSession: (session) => this.emitSessionWorkbenchEvent({
|
|
1620
1657
|
session: this.sessionPresentationService.present(session)
|
|
1621
1658
|
}),
|
|
1622
|
-
changed: () => this.scheduledSessions.changed()
|
|
1659
|
+
changed: () => this.scheduledSessions.changed(),
|
|
1660
|
+
proposed: (sessionId, proposalId) => this.emitScheduledProposalNotification(sessionId, proposalId)
|
|
1623
1661
|
});
|
|
1624
1662
|
this.operationRouter.registerAll(this.sessionScheduleService.operations());
|
|
1625
1663
|
this.operationRouter.registerAll(this.sessionMessageService.operations());
|
|
@@ -158,6 +158,12 @@ export class AbstractRunner {
|
|
|
158
158
|
cancelManagedRun(runId, intent) {
|
|
159
159
|
return this.control?.cancel(runId, intent) === true;
|
|
160
160
|
}
|
|
161
|
+
supportsNativeSteer() {
|
|
162
|
+
return this.control?.steer !== undefined;
|
|
163
|
+
}
|
|
164
|
+
steerManagedRun(input) {
|
|
165
|
+
return this.control?.steer?.(input) ?? Promise.resolve({ status: 'notAccepted' });
|
|
166
|
+
}
|
|
161
167
|
decideApproval(input) {
|
|
162
168
|
return this.control?.decideApproval?.(input) === true;
|
|
163
169
|
}
|
|
@@ -7,6 +7,21 @@ export function normalizeMarAgentUsage(usage) {
|
|
|
7
7
|
import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../../util/node-operation-parsers.js';
|
|
8
8
|
import { runnerErrorProjection } from '../../util/runner-error.js';
|
|
9
9
|
export class MarAgentConversationNormalizer {
|
|
10
|
+
input(entry) {
|
|
11
|
+
return {
|
|
12
|
+
itemId: boundedConversationItemId('mar-agent-input', entry.recordId ?? String(entry.sequence)),
|
|
13
|
+
kind: 'user_message',
|
|
14
|
+
status: 'COMPLETED',
|
|
15
|
+
merge: true,
|
|
16
|
+
payload: {
|
|
17
|
+
text: entry.text,
|
|
18
|
+
clientMessageId: entry.clientMessageId ?? null,
|
|
19
|
+
contexts: [],
|
|
20
|
+
delivery: 'ACCEPTED',
|
|
21
|
+
recordId: entry.recordId
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
#tools = new Map();
|
|
11
26
|
#commands = new Map();
|
|
12
27
|
#processOrigins = new Map();
|
|
@@ -18,6 +33,8 @@ export class MarAgentConversationNormalizer {
|
|
|
18
33
|
return itemId('message', nativeItemId);
|
|
19
34
|
}
|
|
20
35
|
normalize(event, options = {}) {
|
|
36
|
+
if (event.type === 'input.received')
|
|
37
|
+
return [this.input(event.entry)];
|
|
21
38
|
if (event.type === 'execution.failed') {
|
|
22
39
|
const projected = runnerErrorProjection({ code: event.code, message: event.message }, 'MAR_AGENT_RUN_FAILED');
|
|
23
40
|
return [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MarAgentConversationNormalizer } from './conversation-normalizer.js';
|
|
2
2
|
import { publicGoalHistoryText } from '../../service/goal-runtime.js';
|
|
3
|
+
import { boundedConversationItemId } from '../../util/node-operation-parsers.js';
|
|
3
4
|
export function projectMarAgentHistory(session, entries, options = {}) {
|
|
4
5
|
const turns = new Map();
|
|
5
6
|
const normalizer = new MarAgentConversationNormalizer();
|
|
@@ -48,9 +49,11 @@ export function projectMarAgentHistory(session, entries, options = {}) {
|
|
|
48
49
|
const turn = ensure(entry.turnId, at);
|
|
49
50
|
// SDK mailbox messages are persisted as additional turn.user records in the same Turn.
|
|
50
51
|
// The first input keeps its existing identity; later inputs use their storage sequence.
|
|
51
|
-
const id =
|
|
52
|
-
?
|
|
53
|
-
:
|
|
52
|
+
const id = entry.recordId
|
|
53
|
+
? boundedConversationItemId('mar-agent-input', entry.recordId)
|
|
54
|
+
: turn.userItemId === null
|
|
55
|
+
? `${entry.turnId}:user`
|
|
56
|
+
: `${entry.turnId}:user:${entry.sequence}`;
|
|
54
57
|
const images = entry.images;
|
|
55
58
|
const registeredImages = images === undefined ? [] : (options.registerInputImages?.(entry) ?? []);
|
|
56
59
|
replace(turn, historyItem(session, turn, id, 'user_message', 'COMPLETED', {
|
|
@@ -134,8 +134,9 @@ export class MarAgentHostedConversationRuntime {
|
|
|
134
134
|
let titleTask;
|
|
135
135
|
const metadata = await agent.getSession(input.sessionId).catch(() => undefined);
|
|
136
136
|
let execution;
|
|
137
|
+
let session;
|
|
137
138
|
try {
|
|
138
|
-
|
|
139
|
+
session = await agent.openSession(input.sessionId);
|
|
139
140
|
execution = session.run({
|
|
140
141
|
prompt: input.prompt,
|
|
141
142
|
clientMessageId: input.clientMessageId,
|
|
@@ -228,6 +229,19 @@ export class MarAgentHostedConversationRuntime {
|
|
|
228
229
|
completedAt: null
|
|
229
230
|
},
|
|
230
231
|
completion,
|
|
232
|
+
steer: (request) => {
|
|
233
|
+
if (settled ||
|
|
234
|
+
this.#active.get(input.sessionId)?.handle !== handle ||
|
|
235
|
+
(request.modelId !== undefined && request.modelId !== input.modelId) ||
|
|
236
|
+
(request.runtimeScopeId !== undefined && request.runtimeScopeId !== input.runtimeScopeId))
|
|
237
|
+
return Promise.resolve({ status: 'notAccepted' });
|
|
238
|
+
return session.steer({
|
|
239
|
+
expectedExecutionId: execution.executionId,
|
|
240
|
+
prompt: request.prompt,
|
|
241
|
+
modelText: request.modelText,
|
|
242
|
+
clientMessageId: request.clientMessageId
|
|
243
|
+
});
|
|
244
|
+
},
|
|
231
245
|
cancel: () => {
|
|
232
246
|
if (settled || this.#active.get(input.sessionId)?.handle !== handle)
|
|
233
247
|
return false;
|
|
@@ -9,7 +9,7 @@ import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
|
|
|
9
9
|
import { nodeLog } from '../../operational.js';
|
|
10
10
|
import { BUNDLED_RIPGREP_VERSION, bundledRipgrepPath, prependBundledRipgrepToPath } from '../../ripgrep-binary.js';
|
|
11
11
|
import { LocalAgentHost, createMarAgent, generateSessionTitle } from '@myagentroam/agent';
|
|
12
|
-
import { marAgentReasoningEffortSchema, marAgentRuntimeModelConfigurationSchema, marAgentRuntimeCompactConfigurationSchema } from '@myagentroam/protocol';
|
|
12
|
+
import { marAgentReasoningEffortSchema, resolveMarAgentDefaultReasoningEffort, marAgentRuntimeModelConfigurationSchema, marAgentRuntimeCompactConfigurationSchema } from '@myagentroam/protocol';
|
|
13
13
|
import { boundedConversationItemId } from '../codex/conversation-parser.js';
|
|
14
14
|
import { parseSecretEnvironment } from '../../util/secret-environment.js';
|
|
15
15
|
import { canonicalJson } from '../../util/canonical-json.js';
|
|
@@ -32,6 +32,7 @@ export class MarAgentManagedRunController {
|
|
|
32
32
|
host;
|
|
33
33
|
#mcpClients = new MarAgentMcpClientCache();
|
|
34
34
|
#executions = new Map();
|
|
35
|
+
#steerTargets = new Map();
|
|
35
36
|
#sessionRuntimes = new Map();
|
|
36
37
|
#runtimeAcquisitions = new Map();
|
|
37
38
|
#runtimeCreations = new Map();
|
|
@@ -165,6 +166,10 @@ export class MarAgentManagedRunController {
|
|
|
165
166
|
states: this.host.commandStates(session.id)
|
|
166
167
|
};
|
|
167
168
|
}
|
|
169
|
+
steer(input) {
|
|
170
|
+
return (this.#steerTargets.get(input.expectedExecutionId)?.(input) ??
|
|
171
|
+
Promise.resolve({ status: 'notAccepted' }));
|
|
172
|
+
}
|
|
168
173
|
start(envelope) {
|
|
169
174
|
const payload = envelope.payload;
|
|
170
175
|
if (payload.runner !== 'mar-agent' ||
|
|
@@ -320,8 +325,10 @@ export class MarAgentManagedRunController {
|
|
|
320
325
|
runtime.activeRunId = input.runId;
|
|
321
326
|
await runtime.mcpLease.pool.refreshForExecution();
|
|
322
327
|
controller.signal.throwIfAborted();
|
|
328
|
+
const marctlSkill = marctlHostSkill(input.secretEnvironment);
|
|
323
329
|
const executionMcpServers = input.mcpServers
|
|
324
330
|
.filter(isExecutionScopedMcp)
|
|
331
|
+
.filter((server) => marctlSkill === undefined || server.name !== NODE_MCP.schedule.name)
|
|
325
332
|
.map((server) => ({ ...server, lifecycle: 'EXECUTION' }));
|
|
326
333
|
computerUseBinding = await this.#computerUse?.bind({
|
|
327
334
|
runId: input.runId,
|
|
@@ -371,7 +378,6 @@ export class MarAgentManagedRunController {
|
|
|
371
378
|
? {}
|
|
372
379
|
: { webFetch: (request, signal) => this.host.webFetch(request, signal) })
|
|
373
380
|
}));
|
|
374
|
-
const marctlSkill = marctlHostSkill(input.secretEnvironment);
|
|
375
381
|
executionMcpBinding = runtime.host.bindExecutionExtensions({
|
|
376
382
|
...extensions,
|
|
377
383
|
environment: marctlExecutionEnvironment(input.secretEnvironment),
|
|
@@ -444,6 +450,29 @@ export class MarAgentManagedRunController {
|
|
|
444
450
|
? session.compact(runInput)
|
|
445
451
|
: session.run(runInput);
|
|
446
452
|
cancelExecution = () => handle.cancel();
|
|
453
|
+
const steerNormalizer = runtime.normalizer;
|
|
454
|
+
if (input.mode !== 'compact')
|
|
455
|
+
this.#steerTargets.set(input.runId, async (request) => {
|
|
456
|
+
if (controller.signal.aborted ||
|
|
457
|
+
(request.model && request.model !== input.model.id) ||
|
|
458
|
+
(request.effort &&
|
|
459
|
+
request.effort !==
|
|
460
|
+
(reasoningEffort ??
|
|
461
|
+
resolveMarAgentDefaultReasoningEffort(input.model.defaultReasoningEffort, input.model.reasoningEfforts))) ||
|
|
462
|
+
(request.access !== null && request.access !== (input.access ?? 'auto')) ||
|
|
463
|
+
request.collaborationMode !== (input.mode === 'plan' ? 'plan' : 'default'))
|
|
464
|
+
return { status: 'notAccepted' };
|
|
465
|
+
const result = await session.steer({
|
|
466
|
+
expectedExecutionId: handle.executionId,
|
|
467
|
+
prompt: request.prompt,
|
|
468
|
+
modelText: request.modelText,
|
|
469
|
+
clientMessageId: request.clientMessageId,
|
|
470
|
+
images: request.images
|
|
471
|
+
});
|
|
472
|
+
return result.status === 'accepted'
|
|
473
|
+
? { status: 'accepted', item: steerNormalizer.input(result.entry) }
|
|
474
|
+
: result;
|
|
475
|
+
});
|
|
447
476
|
this.host.emit(input.runId, 'run.running', {}, 'RUNNING');
|
|
448
477
|
for await (const event of handle) {
|
|
449
478
|
if (event.type === 'execution.started' && metadata?.automaticTitlePending && !titleTask) {
|
|
@@ -558,6 +587,7 @@ export class MarAgentManagedRunController {
|
|
|
558
587
|
await this.#finishRuntimeExecution(input.runId, runtime).catch(() => undefined);
|
|
559
588
|
this.host.active.delete(input.runId);
|
|
560
589
|
this.#executions.delete(input.runId);
|
|
590
|
+
this.#steerTargets.delete(input.runId);
|
|
561
591
|
if (cancelledDuringPreparation)
|
|
562
592
|
this.host.emit(input.runId, 'run.cancelled', { status: 'CANCELLED' }, 'CANCELLED');
|
|
563
593
|
}
|
|
@@ -10,7 +10,7 @@ export function marctlHostSkill(environment = {}) {
|
|
|
10
10
|
return undefined;
|
|
11
11
|
return {
|
|
12
12
|
name: 'marctl',
|
|
13
|
-
description: 'You are in a MAR-managed Workspace with marctl on PATH. Use this skill to inspect, create, message, stop or coordinate independent Sessions, or use authorized Node terminals and file transfer. Load before cross-Session coordination.',
|
|
13
|
+
description: 'You are in a MAR-managed Workspace with marctl on PATH. Use this skill to inspect, create, rename, configure, message, stop or coordinate independent Sessions, manage their scheduled tasks, or use authorized Node terminals and file transfer. Load before cross-Session coordination.',
|
|
14
14
|
requiredTools: ['exec'],
|
|
15
15
|
load: async () => marctlInstructions
|
|
16
16
|
};
|
|
@@ -27,6 +27,9 @@ const marctlInstructions = [
|
|
|
27
27
|
'Create workers only as tasks need them, named fixer#<task-id> <description> or reviewer#<task-or-change-id> <description>. These are conventions, not product roles or a fixed pool. Put role, scope, acceptance criteria and delivery requirements in the first message. Follow the repository workflow for development and review. Session Prompt is edited in the Web UI only; marctl has no editing command, and sends to existing Sessions use normal prompt injection.',
|
|
28
28
|
'Reuse recorded Session/Run IDs and receipts. Refresh volatile Run, queue and Goal state with sessions show only when the next action depends on it. Select needed JSON fields instead of dumping full Session lists or tool transcripts. Hidden reasoning, credentials and raw model requests are not collaboration artifacts.',
|
|
29
29
|
'show --json fields: session.activity, session.model, session.effort, session.customTitle or session.runnerTitle, runs[] (id/status), queue.items[], and goal.status (goal may be null). Run status and Goal status are separate.',
|
|
30
|
+
'Rename any authorized Workspace Session with sessions rename <session-id> --title <name> --json; parent relationships do not restrict renaming.',
|
|
31
|
+
'show/config/history/graph/rename and schedule get/create/update may omit the Session ID to use the current Session. Without a current Session, specify an ID. send/goal/stop/wait always require explicit target IDs.',
|
|
32
|
+
'Use sessions schedule list/get/create/update for the same tasks as the built-in schedule MCP, which is hidden while this skill is available. list is Workspace-scoped. create requires --cron and text (-m/--message-file, up to 20,000 characters). update requires --expected-revision from get and at least cron or text. Other-owner edits return a PENDING proposal, not an applied change; scheduled execution is read-only. Follow existing approval rules and do not retry conflicts or uncertain writes blindly.',
|
|
30
33
|
'Read results with sessions history <session-id> --limit 10 --json (default: latest 10 SEGMENTs, not messages; --limit 1..100). In segments[].items[], select kind === "assistant_message" and read payload.text; keep segment runId and user_message.payload.clientMessageId for queued-input correlation. Reuse the returned page; follow --cursor <nextCursor> only for missing evidence.',
|
|
31
34
|
'Use sessions send <session-id> --steer -m "Additional context: focus on the resume path." --json for decision-changing updates rather than routine reports: it may switch the current execution. Node decides delivery from actual state; an idle Session receives ordinary input. Choose exactly one of --steer or --queue, even when idle; missing, conflicting, repeated, or obsolete options are rejected locally before input is read or a request is sent. Use --queue to place input in a subsequent turn; with an active Goal, ordinary queued input waits until the Goal exits. start/send return clientMessageId and the accepted/queued result. A queued result may refer to the currently active Run: associate queueItemId/clientMessageId with history before deciding that input finished. There are no queue-item edit/delete/reorder commands; show includes queue state.',
|
|
32
35
|
'Wait with sessions wait <session-id> --run <run-id> --timeout 5m --json through the existing Watch. Prefer --timeout 5m when blocked on task completion; use 30s for a short deadline. Continue useful independent work before waiting. If exec returns a running process or cell, reuse its process/cell handle rather than starting another wait or polling show/list. Timeout or cancelled waiting does not stop the task. Use sessions stop <session-id> --run <run-id> --json only to stop that active Run; ownership rules still apply.',
|
package/dist/runtime-state.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { permissionStopSchema } from '@myagentroam/protocol';
|
|
3
|
+
/** User inputs arrive with a durable Session identity, unlike execution-local output IDs. */
|
|
4
|
+
export function runConversationItemId(runId, itemId, kind) {
|
|
5
|
+
return kind === 'user_message' ? itemId : `${runId}:${itemId}`;
|
|
6
|
+
}
|
|
3
7
|
/**
|
|
4
8
|
* Deliberately process-local runtime domain. No method in this class writes
|
|
5
9
|
* into Node SQLite: a Node restart loses active runs, approvals, leases and
|
|
@@ -32,6 +36,7 @@ export class NodeRuntimeState {
|
|
|
32
36
|
sessionHistoryMutations = new Set();
|
|
33
37
|
/** Session FIFO metadata is deliberately process-local with the rest of run state. */
|
|
34
38
|
queuedItems = new Map();
|
|
39
|
+
receivingQueueItems = new Set();
|
|
35
40
|
queueVersions = new Map();
|
|
36
41
|
pausedQueues = new Set();
|
|
37
42
|
/** Bounded tombstones reject stale internal start envelopes after terminal cleanup. */
|
|
@@ -441,6 +446,32 @@ export class NodeRuntimeState {
|
|
|
441
446
|
getQueueItem(id) {
|
|
442
447
|
return this.queueItems.get(id);
|
|
443
448
|
}
|
|
449
|
+
reserveQueueItem(itemId) {
|
|
450
|
+
this.requireQueueItem(itemId);
|
|
451
|
+
this.assertQueueItemMutable(itemId);
|
|
452
|
+
this.receivingQueueItems.add(itemId);
|
|
453
|
+
return () => {
|
|
454
|
+
this.receivingQueueItems.delete(itemId);
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
assertQueueItemMutable(itemId) {
|
|
458
|
+
if (this.receivingQueueItems.has(itemId))
|
|
459
|
+
throw new Error('QUEUE_ITEM_RECEIVING');
|
|
460
|
+
}
|
|
461
|
+
consumeSteeredQueueItem(itemId, runId) {
|
|
462
|
+
const item = this.requireQueueItem(itemId);
|
|
463
|
+
const run = this.getRun(runId);
|
|
464
|
+
if (!run || run.sessionId !== item.sessionId || !this.receivingQueueItems.has(itemId))
|
|
465
|
+
throw new Error('QUEUE_ITEM_INVALID');
|
|
466
|
+
this.queuedItems.set(item.sessionId, (this.queuedItems.get(item.sessionId) ?? []).filter((id) => id !== itemId));
|
|
467
|
+
this.queueItems.delete(itemId);
|
|
468
|
+
this.bumpQueueVersion(item.sessionId);
|
|
469
|
+
this.messageSubmissions.set(`${item.sessionId}\u0000${item.clientMessageId}`, {
|
|
470
|
+
kind: 'run',
|
|
471
|
+
id: run.id
|
|
472
|
+
});
|
|
473
|
+
this.recentUserMessages.set(run.id, item.content);
|
|
474
|
+
}
|
|
444
475
|
claimNextQueueItem(sessionId, content) {
|
|
445
476
|
if (this.pausedQueues.has(sessionId))
|
|
446
477
|
return undefined;
|
|
@@ -451,6 +482,8 @@ export class NodeRuntimeState {
|
|
|
451
482
|
const itemId = queue[0];
|
|
452
483
|
if (itemId === undefined)
|
|
453
484
|
return undefined;
|
|
485
|
+
if (this.receivingQueueItems.has(itemId))
|
|
486
|
+
return undefined;
|
|
454
487
|
const item = this.requireQueueItem(itemId);
|
|
455
488
|
const run = {
|
|
456
489
|
...active,
|
|
@@ -508,6 +541,8 @@ export class NodeRuntimeState {
|
|
|
508
541
|
}
|
|
509
542
|
reorderSessionQueue(sessionId, expectedVersion, itemIds) {
|
|
510
543
|
const current = this.queuedItems.get(sessionId) ?? [];
|
|
544
|
+
for (const id of current)
|
|
545
|
+
this.assertQueueItemMutable(id);
|
|
511
546
|
if (this.sessionQueue(sessionId).version !== expectedVersion ||
|
|
512
547
|
current.length !== itemIds.length ||
|
|
513
548
|
new Set(current).size !== current.length ||
|
|
@@ -519,6 +554,7 @@ export class NodeRuntimeState {
|
|
|
519
554
|
this.bumpQueueVersion(sessionId);
|
|
520
555
|
}
|
|
521
556
|
prioritizeQueueItem(itemId) {
|
|
557
|
+
this.assertQueueItemMutable(itemId);
|
|
522
558
|
const item = this.requireQueueItem(itemId);
|
|
523
559
|
const queue = this.queuedItems.get(item.sessionId) ?? [];
|
|
524
560
|
if (!queue.includes(itemId))
|
|
@@ -531,6 +567,7 @@ export class NodeRuntimeState {
|
|
|
531
567
|
return item;
|
|
532
568
|
}
|
|
533
569
|
updateQueueItem(itemId, content) {
|
|
570
|
+
this.assertQueueItemMutable(itemId);
|
|
534
571
|
const item = this.requireQueueItem(itemId);
|
|
535
572
|
if (!(this.queuedItems.get(item.sessionId) ?? []).includes(itemId))
|
|
536
573
|
throw new Error('QUEUE_ITEM_INVALID');
|
|
@@ -540,6 +577,7 @@ export class NodeRuntimeState {
|
|
|
540
577
|
return next;
|
|
541
578
|
}
|
|
542
579
|
updateQueueItemAttachments(itemId, attachments, content) {
|
|
580
|
+
this.assertQueueItemMutable(itemId);
|
|
543
581
|
const item = this.requireQueueItem(itemId);
|
|
544
582
|
const next = { ...item, attachments, ...(content === undefined ? {} : { content }) };
|
|
545
583
|
this.queueItems.set(itemId, next);
|
|
@@ -548,6 +586,7 @@ export class NodeRuntimeState {
|
|
|
548
586
|
return next;
|
|
549
587
|
}
|
|
550
588
|
deleteQueueItem(itemId) {
|
|
589
|
+
this.assertQueueItemMutable(itemId);
|
|
551
590
|
const item = this.requireQueueItem(itemId);
|
|
552
591
|
const queue = this.queuedItems.get(item.sessionId) ?? [];
|
|
553
592
|
if (!queue.includes(itemId))
|
|
@@ -1324,13 +1363,16 @@ export class NodeRuntimeState {
|
|
|
1324
1363
|
}
|
|
1325
1364
|
const projected = conversationItemFromEvent(event);
|
|
1326
1365
|
if (projected !== undefined) {
|
|
1366
|
+
const itemId = runConversationItemId(event.runId, projected.itemId, projected.kind);
|
|
1327
1367
|
const trailingItem = turn.items.at(-1);
|
|
1328
|
-
const orderedItems =
|
|
1368
|
+
const orderedItems = projected.kind !== 'user_message' &&
|
|
1369
|
+
trailingItem?.kind === 'assistant_message' &&
|
|
1370
|
+
activeConversationItem(trailingItem.status)
|
|
1329
1371
|
? turn.items.map((item) => item.id === trailingItem.id
|
|
1330
1372
|
? { ...item, status: 'COMPLETED', completedAt: event.createdAt }
|
|
1331
1373
|
: item)
|
|
1332
1374
|
: turn.items;
|
|
1333
|
-
const existing = orderedItems.find((item) => item.id ===
|
|
1375
|
+
const existing = orderedItems.find((item) => item.id === itemId) ??
|
|
1334
1376
|
(projected.kind === 'assistant_message' &&
|
|
1335
1377
|
projected.itemId.startsWith('codex:') &&
|
|
1336
1378
|
trailingItem?.kind === 'assistant_message' &&
|
|
@@ -1340,7 +1382,7 @@ export class NodeRuntimeState {
|
|
|
1340
1382
|
: undefined);
|
|
1341
1383
|
const status = projected.status;
|
|
1342
1384
|
const item = {
|
|
1343
|
-
id:
|
|
1385
|
+
id: itemId,
|
|
1344
1386
|
sessionId: run.sessionId,
|
|
1345
1387
|
turnId: turn.id,
|
|
1346
1388
|
runId: event.runId,
|
|
@@ -1480,7 +1522,8 @@ function conversationItemFromEvent(event) {
|
|
|
1480
1522
|
};
|
|
1481
1523
|
}
|
|
1482
1524
|
function projectableConversationKind(value) {
|
|
1483
|
-
return (value === '
|
|
1525
|
+
return (value === 'user_message' ||
|
|
1526
|
+
value === 'assistant_message' ||
|
|
1484
1527
|
value === 'reasoning_summary' ||
|
|
1485
1528
|
value === 'tool_call' ||
|
|
1486
1529
|
value === 'command_execution' ||
|
|
@@ -2,6 +2,7 @@ import { withNonImageAttachmentPrompt } from '../util/node-operation-parsers.js'
|
|
|
2
2
|
import { safeErrorCode } from '../util/safe-error.js';
|
|
3
3
|
import { withSessionPrompt } from '../util/personal-instructions.js';
|
|
4
4
|
import { prepareComposerResourceInput } from './composer-message-input.js';
|
|
5
|
+
import { nodeLog } from '../operational.js';
|
|
5
6
|
export class ContinuousRunCoordinator {
|
|
6
7
|
options;
|
|
7
8
|
preparedInputs = new Map();
|
|
@@ -10,6 +11,7 @@ export class ContinuousRunCoordinator {
|
|
|
10
11
|
startedExecutions = new Set();
|
|
11
12
|
scheduledSessions = new Map();
|
|
12
13
|
immediateQueueItems = new Set();
|
|
14
|
+
steerInFlight = new Map();
|
|
13
15
|
suspended = false;
|
|
14
16
|
constructor(options) {
|
|
15
17
|
this.options = options;
|
|
@@ -108,6 +110,25 @@ export class ContinuousRunCoordinator {
|
|
|
108
110
|
this.options.emitRun(run.id, 'run.rejected', { code }, 'FAILED');
|
|
109
111
|
}
|
|
110
112
|
settle(input) {
|
|
113
|
+
const item = this.options.runtime.getQueueItem(input.submissionId);
|
|
114
|
+
const active = item && this.options.runtime.activeSessionRun(item.sessionId);
|
|
115
|
+
const turn = active && this.options.runtime.conversationTurnForRun(active.id);
|
|
116
|
+
if (input.steerTarget &&
|
|
117
|
+
active &&
|
|
118
|
+
active.id === input.activeRunId &&
|
|
119
|
+
turn?.id === input.steerTarget.turnId &&
|
|
120
|
+
this.options.runtime.currentExecutionId(active.id) === input.steerTarget.executionId) {
|
|
121
|
+
const attempt = this.tryNativeSteer(input.submissionId, active, input.onNativeAccepted);
|
|
122
|
+
if (attempt)
|
|
123
|
+
return attempt.then((accepted) => accepted
|
|
124
|
+
? 'RESTARTING'
|
|
125
|
+
: this.suspended || this.options.stopped()
|
|
126
|
+
? 'QUEUED'
|
|
127
|
+
: this.settleFallback(input));
|
|
128
|
+
}
|
|
129
|
+
return this.settleFallback(input);
|
|
130
|
+
}
|
|
131
|
+
settleFallback(input) {
|
|
111
132
|
const prepared = this.preparedInputs.get(input.submissionId);
|
|
112
133
|
const item = this.options.runtime.getQueueItem(input.submissionId);
|
|
113
134
|
const sessionId = prepared?.sessionId ?? item?.sessionId;
|
|
@@ -187,6 +208,84 @@ export class ContinuousRunCoordinator {
|
|
|
187
208
|
return 'QUEUED';
|
|
188
209
|
}
|
|
189
210
|
executeQueueItem(itemId) {
|
|
211
|
+
const item = this.requirePreparedItem(itemId);
|
|
212
|
+
const active = this.options.runtime.activeSessionRun(item.sessionId);
|
|
213
|
+
if (active) {
|
|
214
|
+
const attempt = this.tryNativeSteer(itemId, active);
|
|
215
|
+
if (attempt)
|
|
216
|
+
return attempt.then((accepted) => accepted
|
|
217
|
+
? { run: active, delivery: 'RESTARTING' }
|
|
218
|
+
: this.suspended || this.options.stopped()
|
|
219
|
+
? { run: active, delivery: 'QUEUED' }
|
|
220
|
+
: this.executeQueueItemFallback(itemId));
|
|
221
|
+
}
|
|
222
|
+
return this.executeQueueItemFallback(itemId);
|
|
223
|
+
}
|
|
224
|
+
tryNativeSteer(itemId, active, onAccepted) {
|
|
225
|
+
const previous = this.steerInFlight.get(itemId);
|
|
226
|
+
if (previous)
|
|
227
|
+
return previous.task;
|
|
228
|
+
if (this.suspended || this.options.stopped() || !this.options.steer || !this.canSteer(active))
|
|
229
|
+
return undefined;
|
|
230
|
+
const executionId = this.options.runtime.currentExecutionId(active.id);
|
|
231
|
+
if (!executionId)
|
|
232
|
+
return undefined;
|
|
233
|
+
this.prepareSteering(itemId);
|
|
234
|
+
const prepared = this.preparedInputs.get(itemId);
|
|
235
|
+
const item = this.options.runtime.getQueueItem(itemId);
|
|
236
|
+
const release = this.options.runtime.reserveQueueItem(itemId);
|
|
237
|
+
let attempt;
|
|
238
|
+
try {
|
|
239
|
+
attempt = this.options.steer(prepared, item, executionId);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
release();
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
if (!attempt) {
|
|
246
|
+
release();
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const task = attempt
|
|
250
|
+
.then((result) => {
|
|
251
|
+
if (result.status === 'notAccepted')
|
|
252
|
+
return false;
|
|
253
|
+
onAccepted?.();
|
|
254
|
+
this.options.runtime.consumeSteeredQueueItem(itemId, active.id);
|
|
255
|
+
this.remove(itemId);
|
|
256
|
+
this.immediateQueueItems.delete(itemId);
|
|
257
|
+
this.options.runtime.resumeSessionQueue(item.sessionId);
|
|
258
|
+
this.options.clearImages(itemId);
|
|
259
|
+
// Publication is best effort after ownership transfers; it cannot undo SDK acceptance.
|
|
260
|
+
for (const notify of [
|
|
261
|
+
() => {
|
|
262
|
+
if (result.status === 'accepted')
|
|
263
|
+
this.options.emitRun(executionId, 'conversation.item', result.item);
|
|
264
|
+
},
|
|
265
|
+
() => this.publish(item.sessionId)
|
|
266
|
+
]) {
|
|
267
|
+
try {
|
|
268
|
+
notify();
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
nodeLog('queue.native_steer.notification_failed', {
|
|
272
|
+
level: 'warn',
|
|
273
|
+
sessionId: item.sessionId,
|
|
274
|
+
executionId
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
})
|
|
280
|
+
.finally(() => {
|
|
281
|
+
release();
|
|
282
|
+
this.steerInFlight.delete(itemId);
|
|
283
|
+
this.schedule(item.sessionId);
|
|
284
|
+
});
|
|
285
|
+
this.steerInFlight.set(itemId, { sessionId: item.sessionId, task });
|
|
286
|
+
return task;
|
|
287
|
+
}
|
|
288
|
+
executeQueueItemFallback(itemId) {
|
|
190
289
|
const item = this.requirePreparedItem(itemId);
|
|
191
290
|
const active = this.options.runtime.activeSessionRun(item.sessionId);
|
|
192
291
|
if (active !== undefined && this.canSteer(active))
|
|
@@ -320,6 +419,8 @@ export class ContinuousRunCoordinator {
|
|
|
320
419
|
async dispatch(sessionId) {
|
|
321
420
|
if (this.suspended || this.options.stopped())
|
|
322
421
|
return;
|
|
422
|
+
if ([...this.steerInFlight.values()].some((entry) => entry.sessionId === sessionId))
|
|
423
|
+
return;
|
|
323
424
|
const active = this.options.runtime.activeSessionRun(sessionId);
|
|
324
425
|
if (active === undefined)
|
|
325
426
|
return;
|
|
@@ -37,6 +37,7 @@ export class HostedConversationNodeService {
|
|
|
37
37
|
pendingMessages = new Map();
|
|
38
38
|
activeRuns = new Map();
|
|
39
39
|
runSettlements = new Map();
|
|
40
|
+
nativeAdmissions = new Map();
|
|
40
41
|
activities = new Map();
|
|
41
42
|
expectedInterruptions = new Set();
|
|
42
43
|
uploads = new Map();
|
|
@@ -448,6 +449,45 @@ export class HostedConversationNodeService {
|
|
|
448
449
|
return this.startMessage(payload, input);
|
|
449
450
|
}
|
|
450
451
|
const steerPrompt = steerComposerMessage(input.prompt);
|
|
452
|
+
if (handle.steer) {
|
|
453
|
+
const barrier = Promise.withResolvers();
|
|
454
|
+
this.nativeAdmissions.set(handle, barrier.promise);
|
|
455
|
+
let result;
|
|
456
|
+
try {
|
|
457
|
+
const attachments = await Promise.all(input.attachmentIds.map((fileId) => this.requireAttachment(payload.userId, conversation.id, fileId)));
|
|
458
|
+
result = await handle.steer({
|
|
459
|
+
prompt: input.prompt,
|
|
460
|
+
modelText: attachmentPrompt(steerPrompt, attachments),
|
|
461
|
+
clientMessageId: input.clientMessageId,
|
|
462
|
+
...(input.modelId === undefined ? {} : { modelId: input.modelId }),
|
|
463
|
+
...(payload.runtimeScopeId === undefined
|
|
464
|
+
? {}
|
|
465
|
+
: { runtimeScopeId: payload.runtimeScopeId })
|
|
466
|
+
});
|
|
467
|
+
if (result.status !== 'notAccepted') {
|
|
468
|
+
const now = this.now();
|
|
469
|
+
this.options.store.createMessage({
|
|
470
|
+
conversationId: conversation.id,
|
|
471
|
+
clientMessageId: input.clientMessageId,
|
|
472
|
+
runId: handle.run.id,
|
|
473
|
+
userTextPresent: result.status === 'accepted' && sessionTitleFromFirstMessage(input.prompt).length > 0,
|
|
474
|
+
status: 'RUNNING',
|
|
475
|
+
createdAt: handle.run.startedAt,
|
|
476
|
+
updatedAt: now
|
|
477
|
+
});
|
|
478
|
+
const accepted = await this.options.store.saveMessageAcceptance({
|
|
479
|
+
conversationId: conversation.id,
|
|
480
|
+
modelId: conversation.modelId,
|
|
481
|
+
activityAt: now
|
|
482
|
+
});
|
|
483
|
+
return { conversation: this.summary(accepted.conversation), run: handle.run };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
finally {
|
|
487
|
+
barrier.resolve();
|
|
488
|
+
this.nativeAdmissions.delete(handle);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
451
491
|
if (!this.interruptActiveRun(payload.userId, conversation, handle))
|
|
452
492
|
throw new Error('HOSTED_CONVERSATION_RUN_ACTIVE');
|
|
453
493
|
await this.runSettlements.get(conversation.id)?.catch(() => undefined);
|
|
@@ -637,6 +677,7 @@ export class HostedConversationNodeService {
|
|
|
637
677
|
status: 'FAILED',
|
|
638
678
|
completedAt: this.now()
|
|
639
679
|
}));
|
|
680
|
+
await this.nativeAdmissions.get(handle);
|
|
640
681
|
await this.finishRun(payload.userId, conversation.id, input.clientMessageId, handle.run, completion);
|
|
641
682
|
}
|
|
642
683
|
catch {
|
|
@@ -736,7 +777,7 @@ export class HostedConversationNodeService {
|
|
|
736
777
|
const expectedInterruption = this.expectedInterruptions.has(interruptionKey);
|
|
737
778
|
let conversation;
|
|
738
779
|
try {
|
|
739
|
-
this.options.store.
|
|
780
|
+
this.options.store.finishRunMessages(conversationId, run.id, completion.status, completion.completedAt);
|
|
740
781
|
this.publishHostedEvent(conversationId, {
|
|
741
782
|
conversationId,
|
|
742
783
|
kind: 'run',
|
|
@@ -649,6 +649,11 @@ export class HostedConversationStore {
|
|
|
649
649
|
WHERE conversation_id=? AND client_message_id=?`)
|
|
650
650
|
.run(status, updatedAt, conversationId, clientMessageId);
|
|
651
651
|
}
|
|
652
|
+
finishRunMessages(conversationId, runId, status, updatedAt) {
|
|
653
|
+
this.options.database.raw
|
|
654
|
+
.prepare(`UPDATE hosted_conversation_messages SET status=?,updated_at=? WHERE conversation_id=? AND run_id=?`)
|
|
655
|
+
.run(status, updatedAt, conversationId, runId);
|
|
656
|
+
}
|
|
652
657
|
async rebuildConversationIndex() {
|
|
653
658
|
const existing = this.existingFilesByConversation();
|
|
654
659
|
const rebuilt = await this.scanConversationRoot(this.usersDirectory, existing);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { conversationSegmentIdentityForItem, conversationSegments } from './conversation-segment-service.js';
|
|
2
|
+
import { runConversationItemId } from '../runtime-state.js';
|
|
2
3
|
import { isNodeAgentRun, isNodeConversationTurn, isPlainRecord } from '../runner/codex/conversation-parser.js';
|
|
3
4
|
export function workbenchPayloadSessionId(runtime, category, payload) {
|
|
4
5
|
if (!isPlainRecord(payload))
|
|
@@ -59,7 +60,8 @@ export class NodeRunEventBridge {
|
|
|
59
60
|
? turn.items.find((candidate) => candidate.kind === 'assistant_message' && candidate.revision === sequence)
|
|
60
61
|
: nativeItemId === undefined
|
|
61
62
|
? undefined
|
|
62
|
-
: turn.items.find((candidate) => candidate.id ===
|
|
63
|
+
: turn.items.find((candidate) => candidate.id ===
|
|
64
|
+
runConversationItemId(runId, nativeItemId, typeof rawPayload?.kind === 'string' ? rawPayload.kind : undefined));
|
|
63
65
|
if (text !== undefined && item?.kind === 'assistant_message') {
|
|
64
66
|
const segment = conversationSegmentIdentityForItem(turn, item.id, item.kind);
|
|
65
67
|
payload = {
|
|
@@ -139,32 +139,18 @@ export class ScheduleMcpBridge {
|
|
|
139
139
|
if (task === undefined)
|
|
140
140
|
throw new Error('SCHEDULE_NOT_FOUND');
|
|
141
141
|
const args = record(request.arguments);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const patch = {
|
|
149
|
-
...(objective === undefined ? {} : { objective: objective }),
|
|
150
|
-
...(cron === undefined ? {} : { cron: cron })
|
|
151
|
-
};
|
|
152
|
-
if (capability.actorUserId === task.ownerUserId) {
|
|
153
|
-
const updated = this.store().update(task.sessionId, task.revision, {
|
|
154
|
-
...patch,
|
|
155
|
-
ownerUserId: capability.actorUserId
|
|
156
|
-
});
|
|
142
|
+
const result = this.store().updateFromAgent(task.sessionId, task.revision, capability.actorUserId, args);
|
|
143
|
+
if ('proposalId' in result) {
|
|
144
|
+
this.proposed(task.sessionId, result.proposalId);
|
|
145
|
+
socket.end(`${JSON.stringify({ result })}\n`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
157
148
|
this.#capabilities.set(request.token, {
|
|
158
149
|
...capability,
|
|
159
|
-
revision:
|
|
150
|
+
revision: result.revision
|
|
160
151
|
});
|
|
161
152
|
this.changed(task.sessionId);
|
|
162
|
-
socket.end(`${JSON.stringify({ result
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
const proposalId = this.store().propose(task, capability.actorUserId, patch);
|
|
166
|
-
this.proposed(task.sessionId, proposalId);
|
|
167
|
-
socket.end(`${JSON.stringify({ result: { proposalId, status: 'PENDING' } })}\n`);
|
|
153
|
+
socket.end(`${JSON.stringify({ result })}\n`);
|
|
168
154
|
}
|
|
169
155
|
}
|
|
170
156
|
catch (error) {
|
|
@@ -25,6 +25,62 @@ export class ScheduledSessionStore {
|
|
|
25
25
|
.prepare('SELECT * FROM scheduled_session_tasks WHERE enabled=1 AND next_run_at<=? ORDER BY next_run_at,id')
|
|
26
26
|
.all(at).map(taskFromRow);
|
|
27
27
|
}
|
|
28
|
+
list(workspaceId, cursor, limit = 20) {
|
|
29
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
30
|
+
throw new Error('SCHEDULE_INVALID');
|
|
31
|
+
let after = '';
|
|
32
|
+
if (cursor !== undefined) {
|
|
33
|
+
try {
|
|
34
|
+
if (cursor.length > 4096)
|
|
35
|
+
throw new Error();
|
|
36
|
+
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
37
|
+
if (decoded.workspaceId !== workspaceId ||
|
|
38
|
+
typeof decoded.after !== 'string' ||
|
|
39
|
+
decoded.after.length < 1 ||
|
|
40
|
+
decoded.after.length > 512)
|
|
41
|
+
throw new Error();
|
|
42
|
+
after = decoded.after;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new Error('SCHEDULE_INVALID');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const rows = this.database
|
|
49
|
+
.prepare(`SELECT t.*, COALESCE(json_extract(s.metadata,'$.customTitle'),
|
|
50
|
+
json_extract(s.metadata,'$.runnerTitle'),s.id) AS session_title
|
|
51
|
+
FROM scheduled_session_tasks t JOIN agent_sessions s ON s.id=t.session_id
|
|
52
|
+
WHERE t.workspace_id=? AND t.id>? ORDER BY t.id LIMIT ?`)
|
|
53
|
+
.all(workspaceId, after, limit + 1);
|
|
54
|
+
const tasks = rows
|
|
55
|
+
.slice(0, limit)
|
|
56
|
+
.map((row) => ({ ...taskFromRow(row), sessionTitle: row['session_title'] }));
|
|
57
|
+
return {
|
|
58
|
+
tasks,
|
|
59
|
+
nextCursor: rows.length > limit
|
|
60
|
+
? Buffer.from(JSON.stringify({ workspaceId, after: tasks.at(-1).id })).toString('base64url')
|
|
61
|
+
: null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
updateFromAgent(sessionId, expectedRevision, actorUserId, input) {
|
|
65
|
+
if (!Number.isSafeInteger(actorUserId) || actorUserId < 1)
|
|
66
|
+
throw new Error('USER_AUTHORIZATION_REQUIRED');
|
|
67
|
+
const task = this.get(sessionId);
|
|
68
|
+
if (task === undefined)
|
|
69
|
+
throw new Error('SCHEDULE_NOT_FOUND');
|
|
70
|
+
if (task.revision !== expectedRevision)
|
|
71
|
+
throw new Error('SCHEDULE_REVISION_CONFLICT');
|
|
72
|
+
if ((input.objective !== undefined && typeof input.objective !== 'string') ||
|
|
73
|
+
(input.cron !== undefined && typeof input.cron !== 'string') ||
|
|
74
|
+
(input.objective === undefined && input.cron === undefined))
|
|
75
|
+
throw new Error('SCHEDULE_UPDATE_INVALID');
|
|
76
|
+
const patch = {
|
|
77
|
+
...(input.objective === undefined ? {} : { objective: input.objective }),
|
|
78
|
+
...(input.cron === undefined ? {} : { cron: input.cron })
|
|
79
|
+
};
|
|
80
|
+
if (actorUserId === task.ownerUserId)
|
|
81
|
+
return this.update(sessionId, expectedRevision, patch);
|
|
82
|
+
return { proposalId: this.propose(task, actorUserId, patch), status: 'PENDING' };
|
|
83
|
+
}
|
|
28
84
|
nextDueAt() {
|
|
29
85
|
const row = this.database
|
|
30
86
|
.prepare('SELECT MIN(next_run_at) AS value FROM scheduled_session_tasks WHERE enabled=1')
|
|
@@ -206,6 +206,7 @@ export class SessionMessageService {
|
|
|
206
206
|
this.options.coordinator.publishCurrentTurn(created.run.id);
|
|
207
207
|
const prepare = async () => {
|
|
208
208
|
let reservation;
|
|
209
|
+
let nativeAccepted = false;
|
|
209
210
|
try {
|
|
210
211
|
const currentSession = this.options.runtime.getAgentSession(session.id) ?? session;
|
|
211
212
|
if (attachmentIds.length > 0)
|
|
@@ -301,7 +302,10 @@ export class SessionMessageService {
|
|
|
301
302
|
this.options.attachments.retainSessionFiles(currentSession.id, submissionId);
|
|
302
303
|
const delivery = internalRestore
|
|
303
304
|
? 'QUEUED'
|
|
304
|
-
: this.options.coordinator.settle({
|
|
305
|
+
: await this.options.coordinator.settle({
|
|
306
|
+
onNativeAccepted: () => {
|
|
307
|
+
nativeAccepted = true;
|
|
308
|
+
},
|
|
305
309
|
submissionId,
|
|
306
310
|
intent,
|
|
307
311
|
queueBefore: queueBeforeCreate,
|
|
@@ -313,6 +317,8 @@ export class SessionMessageService {
|
|
|
313
317
|
return delivery;
|
|
314
318
|
}
|
|
315
319
|
catch (error) {
|
|
320
|
+
if (nativeAccepted)
|
|
321
|
+
throw error;
|
|
316
322
|
reservation?.release();
|
|
317
323
|
if (goalMessage !== undefined)
|
|
318
324
|
this.options.goalPreparationFailed(session, goalMessage.goal, goalMessage.activated);
|
|
@@ -21,11 +21,64 @@ export class SessionScheduleService {
|
|
|
21
21
|
operations() {
|
|
22
22
|
return {
|
|
23
23
|
'session.schedule.get': (data) => this.get(record(data)),
|
|
24
|
+
'session.schedule.list': (data) => this.list(record(data)),
|
|
25
|
+
'session.schedule.agent.save': (data) => this.saveFromAgent(record(data)),
|
|
24
26
|
'session.schedule.save': (data) => this.save(record(data)),
|
|
25
27
|
'session.schedule.remove': (data) => this.remove(record(data)),
|
|
26
28
|
'session.schedule.proposal.resolve': (data) => this.resolveProposal(record(data))
|
|
27
29
|
};
|
|
28
30
|
}
|
|
31
|
+
list(input) {
|
|
32
|
+
if (typeof input.workspaceId !== 'string' ||
|
|
33
|
+
this.options.database().getWorkspace(input.workspaceId) === undefined)
|
|
34
|
+
throw new Error('WORKSPACE_NOT_FOUND');
|
|
35
|
+
if ((input.cursor !== undefined && typeof input.cursor !== 'string') ||
|
|
36
|
+
(input.limit !== undefined && typeof input.limit !== 'number'))
|
|
37
|
+
throw new Error('SCHEDULE_INVALID');
|
|
38
|
+
const page = this.store().list(input.workspaceId, input.cursor, input.limit);
|
|
39
|
+
return {
|
|
40
|
+
...page,
|
|
41
|
+
tasks: page.tasks.map((task) => {
|
|
42
|
+
const session = this.options.runtime.getAgentSession(task.sessionId);
|
|
43
|
+
return {
|
|
44
|
+
...task,
|
|
45
|
+
sessionTitle: session?.customTitle ?? session?.runnerTitle ?? task.sessionTitle
|
|
46
|
+
};
|
|
47
|
+
})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async saveFromAgent(input) {
|
|
51
|
+
const session = await this.requireSession(input, input.mode === 'create');
|
|
52
|
+
const actor = input.__requestUserId;
|
|
53
|
+
if (!Number.isSafeInteger(actor) || actor < 1)
|
|
54
|
+
throw new Error('USER_AUTHORIZATION_REQUIRED');
|
|
55
|
+
const sessionId = session.externalSessionId ?? session.id;
|
|
56
|
+
if (input.mode === 'create') {
|
|
57
|
+
if (this.store().get(sessionId) !== undefined)
|
|
58
|
+
throw new Error('SCHEDULE_ALREADY_EXISTS');
|
|
59
|
+
if (typeof input.objective !== 'string' || typeof input.cron !== 'string')
|
|
60
|
+
throw new Error('SCHEDULE_INVALID');
|
|
61
|
+
return {
|
|
62
|
+
task: await this.createFromMcp({
|
|
63
|
+
sessionId: session.id,
|
|
64
|
+
workspaceId: session.workspaceId,
|
|
65
|
+
actorUserId: actor,
|
|
66
|
+
objective: input.objective,
|
|
67
|
+
cron: input.cron
|
|
68
|
+
})
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (input.mode !== 'update')
|
|
72
|
+
throw new Error('SCHEDULE_INVALID');
|
|
73
|
+
const result = this.store().updateFromAgent(sessionId, input.expectedRevision, actor, input);
|
|
74
|
+
if ('proposalId' in result) {
|
|
75
|
+
this.options.proposed?.(sessionId, result.proposalId);
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
this.options.emitSession(this.options.runtime.getAgentSession(session.id) ?? session);
|
|
79
|
+
this.options.changed?.();
|
|
80
|
+
return { task: result };
|
|
81
|
+
}
|
|
29
82
|
async get(input) {
|
|
30
83
|
const session = await this.requireSession(input);
|
|
31
84
|
const store = this.store();
|
|
@@ -128,14 +181,14 @@ export class SessionScheduleService {
|
|
|
128
181
|
this.options.changed?.();
|
|
129
182
|
return { task, proposals: this.store().pendingProposals(task.id) };
|
|
130
183
|
}
|
|
131
|
-
async requireSession(input) {
|
|
184
|
+
async requireSession(input, allowPendingNative = false) {
|
|
132
185
|
if (typeof input.sessionId !== 'string' || typeof input.workspaceId !== 'string')
|
|
133
186
|
throw new Error('SCHEDULE_INVALID');
|
|
134
187
|
const session = this.options.runtime.getAgentSession(input.sessionId) ??
|
|
135
188
|
(await this.options.projection.createMetadataProjection(input.sessionId));
|
|
136
189
|
if (session === undefined ||
|
|
137
190
|
session.workspaceId !== input.workspaceId ||
|
|
138
|
-
session.externalSessionId === null)
|
|
191
|
+
(session.externalSessionId === null && !allowPendingNative))
|
|
139
192
|
throw new Error('SESSION_NOT_FOUND');
|
|
140
193
|
return session;
|
|
141
194
|
}
|
|
@@ -144,12 +144,12 @@ export class WorkspaceQueueWorkbenchService {
|
|
|
144
144
|
auditReceipt: this.auditReceipt(item.sessionId, item.id, 'DELETE_QUEUE')
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
|
-
execute(input) {
|
|
147
|
+
async execute(input) {
|
|
148
148
|
if (typeof input.queueItemId !== 'string')
|
|
149
149
|
throw new Error('QUEUE_INVALID');
|
|
150
150
|
const item = this.requireItem(input.queueItemId, input.workspaceId);
|
|
151
151
|
assertQueueItemOwner(item, input.__requestUserId);
|
|
152
|
-
const result = this.options.coordinator.executeQueueItem(input.queueItemId);
|
|
152
|
+
const result = await this.options.coordinator.executeQueueItem(input.queueItemId);
|
|
153
153
|
if (isPlainRecord(result))
|
|
154
154
|
return {
|
|
155
155
|
...result,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myagentroam/node",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.49",
|
|
4
4
|
"description": "MyAgentRoam Node runtime CLI.",
|
|
5
5
|
"license": "Apache-2.0 AND GPL-2.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"playwright": "1.62.1",
|
|
38
38
|
"ws": "^8.21.3",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@myagentroam/agent": "0.10.
|
|
40
|
+
"@myagentroam/agent": "0.10.49",
|
|
41
41
|
"@myagentroam/code-navigation": "0.10.1",
|
|
42
|
-
"@myagentroam/
|
|
43
|
-
"@myagentroam/
|
|
42
|
+
"@myagentroam/marctl": "0.10.10",
|
|
43
|
+
"@myagentroam/protocol": "0.10.49"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/ws": "^8.18.1"
|