@dotdrelle/wiki-manager 0.12.1 → 0.12.8
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/README.md +49 -8
- package/bin/wiki-manager +23 -12
- package/docker-compose.yml +0 -1
- package/mcp.endpoints.example.json +0 -6
- package/package.json +3 -2
- package/src/activity/activityAggregator.js +34 -3
- package/src/activity/activityAggregator.test.js +32 -0
- package/src/agent/graph.js +272 -22
- package/src/agent/graph.test.js +357 -1
- package/src/cli/wiki-manager.js +33 -2
- package/src/commands/slash.js +74 -1
- package/src/commands/slash.test.js +36 -0
- package/src/core/activity.js +4 -0
- package/src/core/activity.test.js +9 -1
- package/src/core/agentEvents.js +30 -0
- package/src/core/agentEvents.test.js +42 -1
- package/src/core/buildInfo.js +58 -0
- package/src/core/buildInfo.json +4 -0
- package/src/core/buildInfo.test.js +14 -0
- package/src/core/env.js +30 -1
- package/src/core/mcp.js +21 -1
- package/src/core/mcp.test.js +25 -0
- package/src/core/runtimeLog.js +6 -1
- package/src/core/runtimeLog.test.js +3 -1
- package/src/runtime/client.js +22 -0
- package/src/runtime/controlMessages.js +2 -2
- package/src/runtime/recoveryManager.js +54 -0
- package/src/runtime/recoveryManager.test.js +73 -0
- package/src/runtime/runner.js +49 -13
- package/src/runtime/runner.test.js +196 -0
- package/src/runtime/server.js +67 -8
- package/src/runtime/server.test.js +223 -6
- package/src/runtime/store.js +48 -3
- package/src/runtime/store.test.js +32 -0
- package/src/runtime/supervisor.js +77 -1
- package/src/shell/RightPane.tsx +124 -31
- package/src/shell/SetupWizard.tsx +13 -1
- package/src/shell/repl.js +78 -9
- package/src/shell/repl.test.js +114 -1
- package/src/shell/tui.tsx +4 -1
- package/src/shell/useAgent.ts +32 -3
- package/src/shell/useSession.ts +14 -1
- package/wiki-workspace +33 -0
package/src/runtime/runner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
2
|
-
import { sessionActivities, terminalFailures } from '../core/activity.js';
|
|
2
|
+
import { isCancelledStatus, sessionActivities, terminalFailures } from '../core/activity.js';
|
|
3
3
|
import { runAgenticLoop, throwIfAborted } from '../core/agentLoop.js';
|
|
4
|
-
import { formatPlanStatus } from '../core/plan.js';
|
|
4
|
+
import { formatPlanStatus, formatPlanStep } from '../core/plan.js';
|
|
5
5
|
import { readyPlanTasks, sanitizePlanForExecution } from '../core/planPatch.js';
|
|
6
6
|
import { createAssignmentManager } from '../orchestrator/assignmentManager.js';
|
|
7
7
|
import { createAttemptManager } from '../orchestrator/attemptManager.js';
|
|
@@ -134,6 +134,19 @@ export async function runRuntimeAgenticWorkflow(agent, session, input, {
|
|
|
134
134
|
continue;
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
+
if (!trigger && isCancelledOnlyLoopResult(result)) {
|
|
138
|
+
dispatchAgentEvent(session, createAgentEvent('run_cancelled', {
|
|
139
|
+
origin: 'runtime',
|
|
140
|
+
runId,
|
|
141
|
+
payload: {
|
|
142
|
+
runId,
|
|
143
|
+
cancelled: true,
|
|
144
|
+
message: 'Runtime run cancelled by user.',
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
emitRuntimeLog(session, 'runtime: run ended by user cancellation (no replan)');
|
|
148
|
+
return { ok: false, result, cancelled: true };
|
|
149
|
+
}
|
|
137
150
|
dispatchAgentEvent(session, createAgentEvent('run_error', {
|
|
138
151
|
origin: 'runtime',
|
|
139
152
|
runId,
|
|
@@ -800,6 +813,7 @@ export async function replanRuntimeRun(session, input, trigger, {
|
|
|
800
813
|
'Given the original objective, current plan, and failure reason, return only the remaining steps required.',
|
|
801
814
|
'Do not include steps that are already done.',
|
|
802
815
|
'Return only JSON with this exact shape: {"steps":["..."]}.',
|
|
816
|
+
'Each step MUST be a plain string, not an object.',
|
|
803
817
|
].join('\n'),
|
|
804
818
|
tools: [],
|
|
805
819
|
messages: [{ role: 'user', content: buildReplanPrompt(input, session, trigger) }],
|
|
@@ -825,10 +839,12 @@ export async function replanRuntimeRun(session, input, trigger, {
|
|
|
825
839
|
steps: mergedSteps,
|
|
826
840
|
},
|
|
827
841
|
}));
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
842
|
+
// Every replan requires approval: deciding "is this step mutating?" with
|
|
843
|
+
// a verb regex was a safety judgement made by pattern-matching — a
|
|
844
|
+
// missed verb silently skipped the approval gate. Replans are rare
|
|
845
|
+
// (technical failures only) and re-executing work deserves a human OK.
|
|
846
|
+
session._runApprovalRequired = true;
|
|
847
|
+
session._runApprovalResolved = false;
|
|
832
848
|
return { ok: true, steps };
|
|
833
849
|
} catch (err) {
|
|
834
850
|
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
@@ -847,7 +863,12 @@ function replanTriggerFromLoopResult(result) {
|
|
|
847
863
|
};
|
|
848
864
|
}
|
|
849
865
|
const failures = terminalFailures(result.completed ?? []);
|
|
850
|
-
const
|
|
866
|
+
const technical = failures.filter((failure) => !isCancelledStatus(failure.status));
|
|
867
|
+
const cancelled = failures.filter((failure) => isCancelledStatus(failure.status));
|
|
868
|
+
if (cancelled.length > 0 && technical.length === 0 && (result.failures ?? []).every(isCancelledTaskFailure)) {
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
const failure = technical[0];
|
|
851
872
|
if (failure) {
|
|
852
873
|
return {
|
|
853
874
|
kind: 'activity_error',
|
|
@@ -859,7 +880,7 @@ function replanTriggerFromLoopResult(result) {
|
|
|
859
880
|
// The parallel scheduler can fail a task before it ever produces an
|
|
860
881
|
// _activity (e.g. a thrown error on the first turn) — that failure lives
|
|
861
882
|
// in result.failures, not in any activity, so it must be checked too.
|
|
862
|
-
const taskFailure = (result.failures ?? [])
|
|
883
|
+
const taskFailure = (result.failures ?? []).find((failure) => !isCancelledTaskFailure(failure));
|
|
863
884
|
if (!taskFailure) return null;
|
|
864
885
|
return {
|
|
865
886
|
kind: 'task_error',
|
|
@@ -869,6 +890,25 @@ function replanTriggerFromLoopResult(result) {
|
|
|
869
890
|
};
|
|
870
891
|
}
|
|
871
892
|
|
|
893
|
+
function isCancelledTaskFailure(failure) {
|
|
894
|
+
return failure?.cancelled === true
|
|
895
|
+
|| failure?.result?.cancelled === true
|
|
896
|
+
|| isCancelledStatus(failure?.status)
|
|
897
|
+
|| isCancelledStatus(failure?.result?.status);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function isCancelledOnlyLoopResult(result) {
|
|
901
|
+
const failures = terminalFailures(result.completed ?? []);
|
|
902
|
+
const technicalActivities = failures.filter((failure) => !isCancelledStatus(failure.status));
|
|
903
|
+
const cancelledActivities = failures.filter((failure) => isCancelledStatus(failure.status));
|
|
904
|
+
const taskFailures = result.failures ?? [];
|
|
905
|
+
const technicalTasks = taskFailures.filter((failure) => !isCancelledTaskFailure(failure));
|
|
906
|
+
const cancelledTasks = taskFailures.filter(isCancelledTaskFailure);
|
|
907
|
+
return technicalActivities.length === 0
|
|
908
|
+
&& technicalTasks.length === 0
|
|
909
|
+
&& (cancelledActivities.length > 0 || cancelledTasks.length > 0);
|
|
910
|
+
}
|
|
911
|
+
|
|
872
912
|
function runtimeLoopErrorMessage(result) {
|
|
873
913
|
if (result.timedOut) return 'Runtime agentic loop timed out.';
|
|
874
914
|
if (result.maxTurns) return 'Runtime agentic loop reached max turns.';
|
|
@@ -912,15 +952,11 @@ function buildReplannedRunPrompt(input, trigger, steps) {
|
|
|
912
952
|
function normalizeReplan(steps) {
|
|
913
953
|
if (!Array.isArray(steps)) return [];
|
|
914
954
|
return steps
|
|
915
|
-
.map((step) =>
|
|
955
|
+
.map((step) => formatPlanStep(step).trim())
|
|
916
956
|
.filter(Boolean)
|
|
917
957
|
.slice(0, 12);
|
|
918
958
|
}
|
|
919
959
|
|
|
920
|
-
function hasMutatingReplanStep(steps) {
|
|
921
|
-
return steps.some((step) => /\b(build|copy|ingest|import|export|polish|pipeline|write|create|delete|update|send|deploy|publish|generate|construire|copier|importer|exporter|publier|envoyer|supprimer|modifier|creer|générer|generer)\b/i.test(step));
|
|
922
|
-
}
|
|
923
|
-
|
|
924
960
|
function mergeReplanWithCompleted(plan, steps) {
|
|
925
961
|
const completed = (plan ?? [])
|
|
926
962
|
.filter((step) => step.status === 'done')
|
|
@@ -259,6 +259,30 @@ test('replanRuntimeRun preserves completed outputs when replacing remaining work
|
|
|
259
259
|
assert.deepEqual(session.headlessPlan[1].dependsOn, ['export']);
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
+
test('replanRuntimeRun normalizes object steps without leaking object strings', async () => {
|
|
263
|
+
const session = {
|
|
264
|
+
activities: {},
|
|
265
|
+
headlessPlan: [{ step: 1, id: 'done', description: 'Done', status: 'done' }],
|
|
266
|
+
llm: {
|
|
267
|
+
async completeWithTools({ system }) {
|
|
268
|
+
assert.match(system, /Each step MUST be a plain string, not an object\./);
|
|
269
|
+
return { content: '{"steps":["a",{"description":"b"},{"id":"c"},{},null]}' };
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const result = await replanRuntimeRun(session, 'Build deliverable', {
|
|
275
|
+
kind: 'activity_error',
|
|
276
|
+
reason: 'Build failed.',
|
|
277
|
+
}, { runId: 'run-replan-object-steps', replansLeft: 1 });
|
|
278
|
+
|
|
279
|
+
assert.equal(result.ok, true);
|
|
280
|
+
assert.deepEqual(result.steps, ['a', 'b', 'c']);
|
|
281
|
+
assert.deepEqual(session.agentProjection.replans[0].plan, ['a', 'b', 'c']);
|
|
282
|
+
assert.deepEqual(session.headlessPlan.slice(1).map((step) => step.description), ['a', 'b', 'c']);
|
|
283
|
+
assert.equal(JSON.stringify(session.headlessPlan).includes('[object Object]'), false);
|
|
284
|
+
});
|
|
285
|
+
|
|
262
286
|
test('replanRuntimeRun requires runtime approval for mutating replanned work', async () => {
|
|
263
287
|
const session = {
|
|
264
288
|
activities: {},
|
|
@@ -370,6 +394,154 @@ test('runRuntimeAgenticWorkflow replans after terminal activity error', async ()
|
|
|
370
394
|
}
|
|
371
395
|
});
|
|
372
396
|
|
|
397
|
+
test('runRuntimeAgenticWorkflow treats user-cancelled activity as terminal cancellation without replan', async () => {
|
|
398
|
+
const originalFetch = globalThis.fetch;
|
|
399
|
+
let pollAttempts = 0;
|
|
400
|
+
const events = [];
|
|
401
|
+
globalThis.fetch = async () => {
|
|
402
|
+
pollAttempts += 1;
|
|
403
|
+
return mcpActivityResponse({
|
|
404
|
+
id: 'job-cancelled',
|
|
405
|
+
source: 'production',
|
|
406
|
+
label: 'Production build',
|
|
407
|
+
status: 'cancelled',
|
|
408
|
+
terminal: true,
|
|
409
|
+
});
|
|
410
|
+
};
|
|
411
|
+
const session = {
|
|
412
|
+
mcp: {
|
|
413
|
+
production: {
|
|
414
|
+
status: 'connected',
|
|
415
|
+
url: 'http://127.0.0.1:3000/mcp/',
|
|
416
|
+
retry: { maxAttempts: 1, backoffMs: 0 },
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
activities: {},
|
|
420
|
+
headlessPlan: null,
|
|
421
|
+
llm: {
|
|
422
|
+
async completeWithTools() {
|
|
423
|
+
assert.fail('cancelled activity must not call evaluator or replanner');
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
_onAgentEvent: (event) => events.push(event),
|
|
427
|
+
};
|
|
428
|
+
const agent = {
|
|
429
|
+
async invoke({ session: turnSession }) {
|
|
430
|
+
dispatchAgentEvent(turnSession, createAgentEvent('activity_upserted', {
|
|
431
|
+
payload: {
|
|
432
|
+
activity: runningPollableActivity('job-cancelled', 'Production build'),
|
|
433
|
+
},
|
|
434
|
+
}));
|
|
435
|
+
return { response: 'Started build.' };
|
|
436
|
+
},
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
const result = await runRuntimeAgenticWorkflow(agent, session, 'Build workspace', {
|
|
441
|
+
runId: 'run-cancelled-activity',
|
|
442
|
+
timeoutMs: 1000,
|
|
443
|
+
maxTurns: 1,
|
|
444
|
+
maxReplans: 1,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
assert.equal(result.ok, false);
|
|
448
|
+
assert.equal(result.cancelled, true);
|
|
449
|
+
assert.equal(pollAttempts, 1);
|
|
450
|
+
assert.equal(events.some((event) => event.type === 'run_replanned'), false);
|
|
451
|
+
assert.ok(events.some((event) => event.type === 'run_cancelled' && event.payload?.cancelled === true));
|
|
452
|
+
assert.equal(session.agentProjection.status, 'cancelled');
|
|
453
|
+
assert.match(session.agentProjection.logs.join('\n'), /run ended by user cancellation \(no replan\)/);
|
|
454
|
+
} finally {
|
|
455
|
+
globalThis.fetch = originalFetch;
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test('runRuntimeAgenticWorkflow replans from failed activity when another activity is cancelled', async () => {
|
|
460
|
+
const originalFetch = globalThis.fetch;
|
|
461
|
+
const events = [];
|
|
462
|
+
const pollStatuses = new Map([
|
|
463
|
+
['job-failed', {
|
|
464
|
+
id: 'job-failed',
|
|
465
|
+
source: 'production',
|
|
466
|
+
label: 'Production build',
|
|
467
|
+
status: 'failed',
|
|
468
|
+
terminal: true,
|
|
469
|
+
error: 'build failed',
|
|
470
|
+
}],
|
|
471
|
+
['job-cancelled', {
|
|
472
|
+
id: 'job-cancelled',
|
|
473
|
+
source: 'production',
|
|
474
|
+
label: 'Production cleanup',
|
|
475
|
+
status: 'cancelled',
|
|
476
|
+
terminal: true,
|
|
477
|
+
}],
|
|
478
|
+
]);
|
|
479
|
+
globalThis.fetch = async (_url, options = {}) => {
|
|
480
|
+
const body = JSON.parse(String(options.body ?? '{}'));
|
|
481
|
+
return mcpActivityResponse(pollStatuses.get(body.params?.arguments?.jobId));
|
|
482
|
+
};
|
|
483
|
+
const session = {
|
|
484
|
+
mcp: {
|
|
485
|
+
production: {
|
|
486
|
+
status: 'connected',
|
|
487
|
+
url: 'http://127.0.0.1:3000/mcp/',
|
|
488
|
+
retry: { maxAttempts: 1, backoffMs: 0 },
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
activities: {},
|
|
492
|
+
headlessPlan: null,
|
|
493
|
+
llm: {
|
|
494
|
+
async completeWithTools({ system }) {
|
|
495
|
+
if (/replanner/.test(system)) return { content: '{"steps":["Retry build"]}' };
|
|
496
|
+
return { content: '{"ok":true,"reason":"Build complete.","suggestedAction":null}' };
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
_onAgentEvent: (event) => events.push(event),
|
|
500
|
+
};
|
|
501
|
+
let turns = 0;
|
|
502
|
+
const agent = {
|
|
503
|
+
async invoke({ session: turnSession }) {
|
|
504
|
+
turns += 1;
|
|
505
|
+
if (turns === 1) {
|
|
506
|
+
dispatchAgentEvent(turnSession, createAgentEvent('activity_upserted', {
|
|
507
|
+
payload: { activity: runningPollableActivity('job-failed', 'Production build') },
|
|
508
|
+
}));
|
|
509
|
+
dispatchAgentEvent(turnSession, createAgentEvent('activity_upserted', {
|
|
510
|
+
payload: { activity: runningPollableActivity('job-cancelled', 'Production cleanup') },
|
|
511
|
+
}));
|
|
512
|
+
return { response: 'Started build.' };
|
|
513
|
+
}
|
|
514
|
+
const pending = turnSession.headlessPlan?.find((step) => step.status === 'pending');
|
|
515
|
+
if (pending) {
|
|
516
|
+
dispatchAgentEvent(turnSession, createAgentEvent('plan_step_updated', {
|
|
517
|
+
origin: 'tool',
|
|
518
|
+
payload: { taskId: pending.id, status: 'done' },
|
|
519
|
+
}));
|
|
520
|
+
}
|
|
521
|
+
return { response: 'Retry done.' };
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
const result = await runRuntimeAgenticWorkflow(agent, session, 'Build workspace', {
|
|
527
|
+
runId: 'run-failed-and-cancelled',
|
|
528
|
+
timeoutMs: 1000,
|
|
529
|
+
maxTurns: 3,
|
|
530
|
+
maxReplans: 1,
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
assert.equal(result.ok, true);
|
|
534
|
+
assert.equal(turns, 2);
|
|
535
|
+
assert.equal(events.some((event) => event.type === 'run_cancelled'), false);
|
|
536
|
+
assert.ok(events.some((event) => event.type === 'run_replanned'));
|
|
537
|
+
assert.equal(session.agentProjection.replans[0].reason, 'Production build ended with failed: build failed');
|
|
538
|
+
assert.deepEqual(session.agentProjection.replans[0].plan, ['Retry build']);
|
|
539
|
+
assert.equal(session.agentProjection.status, 'done');
|
|
540
|
+
} finally {
|
|
541
|
+
globalThis.fetch = originalFetch;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
373
545
|
test('runRuntimeAgenticWorkflow stops after replan budget is exhausted', async () => {
|
|
374
546
|
const session = {
|
|
375
547
|
activities: {},
|
|
@@ -707,6 +879,30 @@ function productionAgent() {
|
|
|
707
879
|
};
|
|
708
880
|
}
|
|
709
881
|
|
|
882
|
+
function runningPollableActivity(id, label) {
|
|
883
|
+
return {
|
|
884
|
+
id,
|
|
885
|
+
source: 'production',
|
|
886
|
+
label,
|
|
887
|
+
status: 'running',
|
|
888
|
+
terminal: false,
|
|
889
|
+
poll: { server: 'production', tool: 'production_job_status', args: { jobId: id }, intervalMs: 0 },
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function mcpActivityResponse(activity) {
|
|
894
|
+
return {
|
|
895
|
+
ok: true,
|
|
896
|
+
status: 200,
|
|
897
|
+
headers: { get: () => null },
|
|
898
|
+
text: async () => JSON.stringify({
|
|
899
|
+
result: {
|
|
900
|
+
content: [{ type: 'text', text: JSON.stringify({ _activity: activity }) }],
|
|
901
|
+
},
|
|
902
|
+
}),
|
|
903
|
+
};
|
|
904
|
+
}
|
|
905
|
+
|
|
710
906
|
function toolResult(payload) {
|
|
711
907
|
return { content: [{ type: 'text', text: JSON.stringify(payload) }] };
|
|
712
908
|
}
|
package/src/runtime/server.js
CHANGED
|
@@ -242,15 +242,19 @@ export function startRuntimeServer({
|
|
|
242
242
|
}
|
|
243
243
|
validateContractInDev('runRequest', { ...body, input });
|
|
244
244
|
if (context.running) {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
245
|
+
// Blind enqueueing made every message typed during a run
|
|
246
|
+
// invisible until the run ended (serve UI symptom: "no result
|
|
247
|
+
// until the job finished or was stopped"). Classify instead:
|
|
248
|
+
// observe → immediate status explanation, cancel → abort,
|
|
249
|
+
// ambiguous action → explicit choices, explicit "later" intent
|
|
250
|
+
// (body.intent = 'enqueue') → enqueue as before.
|
|
251
|
+
const result = await handleControlMessage(context, store, input, {
|
|
252
|
+
intent: body.intent,
|
|
253
|
+
startNextControlRequest,
|
|
254
|
+
cancel,
|
|
255
|
+
approve,
|
|
253
256
|
});
|
|
257
|
+
sendJson(response, result.statusCode, result.body);
|
|
254
258
|
return;
|
|
255
259
|
}
|
|
256
260
|
const accepted = startRuntimeRun(context, body);
|
|
@@ -274,6 +278,16 @@ export function startRuntimeServer({
|
|
|
274
278
|
sendJson(response, 202, { cancelled: true, workspace: context.workspace ?? workspace ?? null });
|
|
275
279
|
return;
|
|
276
280
|
}
|
|
281
|
+
if (request.method === 'POST' && url.pathname === '/kill') {
|
|
282
|
+
const body = await readJson(request);
|
|
283
|
+
const workspace = workspaceFromBody(body) ?? workspaceFromUrl(url);
|
|
284
|
+
const runId = url.searchParams.get('runId') ?? body.runId ?? null;
|
|
285
|
+
const context = await resolveContext({ workspace });
|
|
286
|
+
const result = await killRuntimeRuns(context, { workspace, runId });
|
|
287
|
+
publishState(context.workspace ?? workspace ?? null, context);
|
|
288
|
+
sendJson(response, 202, result);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
277
291
|
if (request.method === 'POST' && url.pathname === '/shutdown') {
|
|
278
292
|
const workspace = workspaceFromUrl(url);
|
|
279
293
|
const context = await resolveContext({ workspace });
|
|
@@ -355,6 +369,23 @@ export function startRuntimeServer({
|
|
|
355
369
|
return { body, workspace, context };
|
|
356
370
|
}
|
|
357
371
|
|
|
372
|
+
async function killRuntimeRuns(context, { workspace = null, runId = null } = {}) {
|
|
373
|
+
const targetWorkspace = context?.workspace ?? workspace ?? null;
|
|
374
|
+
const targetRunId = runId ? String(runId) : null;
|
|
375
|
+
if (!targetRunId || targetRunId === context?.currentRunId) {
|
|
376
|
+
context?.currentAbortController?.abort();
|
|
377
|
+
await cancel?.(context);
|
|
378
|
+
}
|
|
379
|
+
const runs = typeof store.interruptRuns === 'function'
|
|
380
|
+
? store.interruptRuns({ workspace: targetWorkspace, runId: targetRunId, reason: 'Runtime run killed by user.' })
|
|
381
|
+
: 0;
|
|
382
|
+
const tasks = typeof store.cancelActiveTasksForInterruptedRuns === 'function'
|
|
383
|
+
? store.cancelActiveTasksForInterruptedRuns({ workspace: targetWorkspace, runId: targetRunId })
|
|
384
|
+
: 0;
|
|
385
|
+
const queued = cancelQueuedControlItems(context?.session, targetWorkspace);
|
|
386
|
+
return { killed: true, workspace: targetWorkspace, runId: targetRunId, runs, tasks, queued };
|
|
387
|
+
}
|
|
388
|
+
|
|
358
389
|
function startRuntimeRun(context, body, { controlItemId = null } = {}) {
|
|
359
390
|
const runId = randomUUID();
|
|
360
391
|
const runWorkspace = context.workspace ?? body.workspace ?? null;
|
|
@@ -472,6 +503,34 @@ function controlQueueFor(session) {
|
|
|
472
503
|
return Array.isArray(session?.controlQueue) ? session.controlQueue : [];
|
|
473
504
|
}
|
|
474
505
|
|
|
506
|
+
function cancelQueuedControlItems(session, workspace = null) {
|
|
507
|
+
const now = new Date().toISOString();
|
|
508
|
+
const counted = new Set();
|
|
509
|
+
let count = 0;
|
|
510
|
+
for (const queue of [controlQueueFor(session), session?.agentProjection?.controlQueue].filter(Array.isArray)) {
|
|
511
|
+
for (const item of queue) {
|
|
512
|
+
if (item.status !== 'queued') continue;
|
|
513
|
+
if (workspace && item.workspace && item.workspace !== workspace) continue;
|
|
514
|
+
item.status = 'cancelled';
|
|
515
|
+
item.finishedAt = now;
|
|
516
|
+
item.updatedAt = now;
|
|
517
|
+
const id = item.id ?? item.runId ?? `${item.input}:${item.createdAt}`;
|
|
518
|
+
if (!counted.has(id)) {
|
|
519
|
+
counted.add(id);
|
|
520
|
+
if (session) {
|
|
521
|
+
dispatchAgentEvent(session, createAgentEvent('control_cancelled', {
|
|
522
|
+
origin: 'runtime',
|
|
523
|
+
workspace: item.workspace ?? workspace ?? null,
|
|
524
|
+
payload: { id, finishedAt: now },
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
count += 1;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return count;
|
|
532
|
+
}
|
|
533
|
+
|
|
475
534
|
function readOnlyControlResponse(kind, classification, status, explanation, { accepted = true, extra = {} } = {}) {
|
|
476
535
|
return {
|
|
477
536
|
statusCode: 200,
|