@bojackduy/opencode-loopd 1.13.0 → 1.13.2
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/CHANGELOG.md +37 -0
- package/dist/server.js +797 -461
- package/dist/tui.js +117 -58
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -47,6 +47,9 @@ function acquireLease(rt, timeoutMs) {
|
|
|
47
47
|
idleConfirmFailedGeneration: undefined,
|
|
48
48
|
idleStuckNotifiedGeneration: undefined,
|
|
49
49
|
workerAbortedAt: undefined,
|
|
50
|
+
activePromptDispatchAt: undefined,
|
|
51
|
+
activePromptAcceptedAt: undefined,
|
|
52
|
+
activeInboxLines: undefined,
|
|
50
53
|
activePromptObservedAt: undefined,
|
|
51
54
|
activeAssistantMessageID: undefined,
|
|
52
55
|
activeAssistantCompletedAt: undefined,
|
|
@@ -180,7 +183,7 @@ var init_event_tail = () => {};
|
|
|
180
183
|
import { promises as fs2 } from "fs";
|
|
181
184
|
import path from "path";
|
|
182
185
|
import os from "os";
|
|
183
|
-
import { createHash } from "crypto";
|
|
186
|
+
import { createHash, randomUUID } from "crypto";
|
|
184
187
|
function emptyState() {
|
|
185
188
|
return { version: CURRENT_VERSION, revision: 0, goals: [], runtimes: [], commandLedger: [], commands: [] };
|
|
186
189
|
}
|
|
@@ -212,25 +215,32 @@ async function acquireLock(directory, key, operation) {
|
|
|
212
215
|
const dir = lockDir(directory);
|
|
213
216
|
await fs2.mkdir(dir, { recursive: true });
|
|
214
217
|
const lockPath = lockFile(directory, key);
|
|
218
|
+
let lastOwner;
|
|
215
219
|
const deadline = performance.now() + LOCK_TIMEOUT_MS;
|
|
216
220
|
for (let attempt = 0;performance.now() < deadline; attempt++) {
|
|
217
221
|
try {
|
|
218
222
|
try {
|
|
219
223
|
const raw = await fs2.readFile(lockPath, "utf8");
|
|
220
224
|
const meta = JSON.parse(raw);
|
|
225
|
+
lastOwner = meta;
|
|
221
226
|
const age = Date.now() - Date.parse(meta.acquiredAt);
|
|
222
227
|
if (age > LOCK_STALE_MS && !processIsAlive(meta.pid)) {
|
|
223
228
|
await fs2.rm(lockPath, { force: true });
|
|
224
229
|
}
|
|
225
230
|
} catch {}
|
|
226
|
-
const meta = { pid: process.pid, operation, acquiredAt: new Date().toISOString() };
|
|
227
|
-
const fd = await fs2.open(lockPath, "wx");
|
|
231
|
+
const meta = { pid: process.pid, operation, acquiredAt: new Date().toISOString(), token: randomUUID() };
|
|
232
|
+
const fd = await fs2.open(lockPath, "wx+");
|
|
228
233
|
try {
|
|
229
234
|
await fd.writeFile(JSON.stringify(meta), "utf8");
|
|
230
|
-
|
|
231
|
-
|
|
235
|
+
return { path: lockPath, handle: fd, token: meta.token };
|
|
236
|
+
} catch (error) {
|
|
237
|
+
try {
|
|
238
|
+
await fs2.rm(lockPath, { force: true });
|
|
239
|
+
} finally {
|
|
240
|
+
await fd.close();
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
232
243
|
}
|
|
233
|
-
return;
|
|
234
244
|
} catch (error) {
|
|
235
245
|
if (error?.code === "EEXIST") {} else if (error?.code === "ENOENT") {
|
|
236
246
|
await fs2.mkdir(dir, { recursive: true });
|
|
@@ -241,26 +251,28 @@ async function acquireLock(directory, key, operation) {
|
|
|
241
251
|
}
|
|
242
252
|
await delay(Math.min(25 * (attempt + 1), 250, Math.max(0, deadline - performance.now())));
|
|
243
253
|
}
|
|
244
|
-
|
|
254
|
+
const owner = lastOwner ? ` Last observed owner: PID ${lastOwner.pid}, operation "${lastOwner.operation}", acquired ${lastOwner.acquiredAt}.` : " Owner metadata unavailable.";
|
|
255
|
+
throw new Error(`failed to acquire lock "${key}" for "${operation}" after ${LOCK_TIMEOUT_MS}ms. This is the loopd plugin's lock, not OpenCode core.${owner}`);
|
|
245
256
|
}
|
|
246
|
-
async function releaseLock(
|
|
247
|
-
const lockPath = lockFile(directory, key);
|
|
257
|
+
async function releaseLock(lease) {
|
|
248
258
|
try {
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
259
|
+
const held = await lease.handle.stat();
|
|
260
|
+
const buffer = Buffer.alloc(held.size);
|
|
261
|
+
const { bytesRead } = await lease.handle.read(buffer, 0, buffer.length, 0);
|
|
262
|
+
const meta = JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
|
|
263
|
+
if (meta.pid !== process.pid || meta.token !== lease.token)
|
|
253
264
|
return;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const meta2 = JSON.parse(raw2);
|
|
257
|
-
if (meta2.acquiredAt !== meta.acquiredAt || meta2.pid !== meta.pid)
|
|
265
|
+
const current = await fs2.stat(lease.path).catch((error) => {
|
|
266
|
+
if (error?.code === "ENOENT")
|
|
258
267
|
return;
|
|
259
|
-
|
|
268
|
+
throw error;
|
|
269
|
+
});
|
|
270
|
+
if (!current || current.dev !== held.dev || current.ino !== held.ino)
|
|
260
271
|
return;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
await fs2.rm(lease.path, { force: true });
|
|
273
|
+
} finally {
|
|
274
|
+
await lease.handle.close();
|
|
275
|
+
}
|
|
264
276
|
}
|
|
265
277
|
async function readState(directory) {
|
|
266
278
|
const target = stateFile(directory);
|
|
@@ -278,11 +290,11 @@ async function readState(directory) {
|
|
|
278
290
|
return emptyState();
|
|
279
291
|
const transient = error instanceof SyntaxError || error?.code === "EPERM" || error?.code === "EACCES" || error?.code === "EBUSY";
|
|
280
292
|
if (!transient || attempt === attempts - 1)
|
|
281
|
-
|
|
293
|
+
throw error;
|
|
282
294
|
await delay(25 * (attempt + 1));
|
|
283
295
|
}
|
|
284
296
|
}
|
|
285
|
-
|
|
297
|
+
throw new Error("State could not be read.");
|
|
286
298
|
}
|
|
287
299
|
function migrate(state) {
|
|
288
300
|
if (state.version === CURRENT_VERSION)
|
|
@@ -428,14 +440,14 @@ async function writeState(directory, state) {
|
|
|
428
440
|
await writeAtomic(stateFile(directory), payload);
|
|
429
441
|
}
|
|
430
442
|
async function mutateState(directory, description, fn) {
|
|
431
|
-
await acquireLock(directory, "state", description);
|
|
443
|
+
const lease = await acquireLock(directory, "state", description);
|
|
432
444
|
try {
|
|
433
445
|
const state = await readState(directory);
|
|
434
446
|
const next = await fn(state);
|
|
435
447
|
await writeState(directory, next);
|
|
436
448
|
return next;
|
|
437
449
|
} finally {
|
|
438
|
-
await releaseLock(
|
|
450
|
+
await releaseLock(lease);
|
|
439
451
|
}
|
|
440
452
|
}
|
|
441
453
|
async function appendEvent(directory, event) {
|
|
@@ -516,37 +528,80 @@ function inboxFile(directory, goalID) {
|
|
|
516
528
|
return path.join(loopDir(directory), "inboxes", `${goalID}.jsonl`);
|
|
517
529
|
}
|
|
518
530
|
async function appendGoalInbox(directory, goalID, from, text) {
|
|
519
|
-
|
|
520
|
-
await
|
|
521
|
-
|
|
522
|
-
|
|
531
|
+
let accepted = false;
|
|
532
|
+
await mutateState(directory, `inbox.enqueue:${goalID}`, async (state) => {
|
|
533
|
+
if (!state.goals.some((goal) => goal.id === goalID))
|
|
534
|
+
return state;
|
|
535
|
+
const lease = await acquireLock(directory, `inbox-${goalID}`, "inbox.append");
|
|
536
|
+
try {
|
|
537
|
+
const dir = path.join(loopDir(directory), "inboxes");
|
|
538
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
539
|
+
const msg = { id: randomUUID(), from, text, at: new Date().toISOString() };
|
|
540
|
+
await fs2.appendFile(inboxFile(directory, goalID), JSON.stringify(msg) + `
|
|
523
541
|
`, "utf8");
|
|
542
|
+
accepted = true;
|
|
543
|
+
} finally {
|
|
544
|
+
await releaseLock(lease);
|
|
545
|
+
}
|
|
546
|
+
return state;
|
|
547
|
+
});
|
|
548
|
+
return accepted;
|
|
524
549
|
}
|
|
525
|
-
async function
|
|
526
|
-
const
|
|
550
|
+
async function deleteGoalInbox(directory, goalID) {
|
|
551
|
+
const lease = await acquireLock(directory, `inbox-${goalID}`, "inbox.delete");
|
|
527
552
|
try {
|
|
528
|
-
|
|
553
|
+
await fs2.rm(inboxFile(directory, goalID), { force: true });
|
|
554
|
+
} finally {
|
|
555
|
+
await releaseLock(lease);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
async function acknowledgeGoalInbox(directory, goalID, delivered) {
|
|
559
|
+
if (!delivered.length)
|
|
560
|
+
return;
|
|
561
|
+
const lease = await acquireLock(directory, `inbox-${goalID}`, "inbox.acknowledge");
|
|
562
|
+
try {
|
|
563
|
+
let raw;
|
|
564
|
+
try {
|
|
565
|
+
raw = await fs2.readFile(inboxFile(directory, goalID), "utf8");
|
|
566
|
+
} catch (error) {
|
|
567
|
+
if (error?.code === "ENOENT")
|
|
568
|
+
return;
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
529
571
|
const lines = raw.trim().split(`
|
|
530
572
|
`).filter(Boolean);
|
|
531
|
-
if (
|
|
532
|
-
return
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
573
|
+
if (!delivered.every((line, index) => lines[index] === line))
|
|
574
|
+
return;
|
|
575
|
+
const rest = lines.slice(delivered.length);
|
|
576
|
+
if (rest.length)
|
|
577
|
+
await writeAtomic(inboxFile(directory, goalID), rest.join(`
|
|
578
|
+
`) + `
|
|
579
|
+
`);
|
|
580
|
+
else
|
|
581
|
+
await fs2.rm(inboxFile(directory, goalID), { force: true });
|
|
582
|
+
} finally {
|
|
583
|
+
await releaseLock(lease);
|
|
538
584
|
}
|
|
539
585
|
}
|
|
540
|
-
async function
|
|
541
|
-
const
|
|
586
|
+
async function snapshotGoalInbox(directory, goalID) {
|
|
587
|
+
const lease = await acquireLock(directory, `inbox-${goalID}`, "inbox.snapshot");
|
|
542
588
|
try {
|
|
543
|
-
const raw = await fs2.readFile(
|
|
589
|
+
const raw = await fs2.readFile(inboxFile(directory, goalID), "utf8");
|
|
544
590
|
const lines = raw.trim().split(`
|
|
545
591
|
`).filter(Boolean);
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
592
|
+
const messages = lines.map((line) => JSON.parse(line));
|
|
593
|
+
return { lines, messages: messages.map((m) => `[${m.from}] ${m.text}`) };
|
|
594
|
+
} catch (error) {
|
|
595
|
+
if (error?.code === "ENOENT")
|
|
596
|
+
return { lines: [], messages: [] };
|
|
597
|
+
throw error;
|
|
598
|
+
} finally {
|
|
599
|
+
await releaseLock(lease);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
async function peekGoalInbox(directory, goalID) {
|
|
603
|
+
try {
|
|
604
|
+
return (await snapshotGoalInbox(directory, goalID)).messages;
|
|
550
605
|
} catch {
|
|
551
606
|
return [];
|
|
552
607
|
}
|
|
@@ -621,6 +676,42 @@ var init_state_repository = __esm(() => {
|
|
|
621
676
|
init_event_tail();
|
|
622
677
|
});
|
|
623
678
|
|
|
679
|
+
// src/infrastructure/server-log.ts
|
|
680
|
+
import { appendFile } from "fs/promises";
|
|
681
|
+
async function logServerEvent(directory, event, details = {}) {
|
|
682
|
+
try {
|
|
683
|
+
await appendFile(SERVER_LOG_FILE, `${JSON.stringify({
|
|
684
|
+
timestamp: new Date().toISOString(),
|
|
685
|
+
directory,
|
|
686
|
+
event,
|
|
687
|
+
...details
|
|
688
|
+
}, errorReplacer)}
|
|
689
|
+
`);
|
|
690
|
+
} catch {}
|
|
691
|
+
}
|
|
692
|
+
function describeError(value) {
|
|
693
|
+
if (value instanceof Error)
|
|
694
|
+
return value.message;
|
|
695
|
+
if (typeof value === "string")
|
|
696
|
+
return value;
|
|
697
|
+
if (typeof value === "object" && value !== null && "message" in value) {
|
|
698
|
+
return String(value.message);
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
return JSON.stringify(value, errorReplacer);
|
|
702
|
+
} catch {
|
|
703
|
+
return String(value);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
function errorReplacer(_key, value) {
|
|
707
|
+
if (value instanceof Error) {
|
|
708
|
+
return { name: value.name, message: value.message, stack: value.stack };
|
|
709
|
+
}
|
|
710
|
+
return value;
|
|
711
|
+
}
|
|
712
|
+
var SERVER_LOG_FILE = "/tmp/loopd-server.log";
|
|
713
|
+
var init_server_log = () => {};
|
|
714
|
+
|
|
624
715
|
// src/domain/command-await.ts
|
|
625
716
|
function isTerminalCommandStatus(status) {
|
|
626
717
|
return TERMINAL_COMMAND_STATUSES.includes(status);
|
|
@@ -886,11 +977,11 @@ var init_command_watch = __esm(() => {
|
|
|
886
977
|
});
|
|
887
978
|
|
|
888
979
|
// src/application/command-await.ts
|
|
889
|
-
import { randomUUID } from "crypto";
|
|
980
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
890
981
|
function ledgerEvent(base) {
|
|
891
982
|
return {
|
|
892
983
|
version: 1,
|
|
893
|
-
eventID:
|
|
984
|
+
eventID: randomUUID2(),
|
|
894
985
|
timestamp: new Date().toISOString(),
|
|
895
986
|
revision: 0,
|
|
896
987
|
...base
|
|
@@ -1266,20 +1357,8 @@ async function wakeGoalForAwait(directory, continuation, goalID) {
|
|
|
1266
1357
|
if (goal.status !== "active") {
|
|
1267
1358
|
return { woke: false, message: `Goal is ${goal.status}; evidence waits in pendingInbox.` };
|
|
1268
1359
|
}
|
|
1269
|
-
await
|
|
1270
|
-
|
|
1271
|
-
if (!rt)
|
|
1272
|
-
return s;
|
|
1273
|
-
rt.phase = "idle";
|
|
1274
|
-
rt.activeRunID = undefined;
|
|
1275
|
-
rt.idleCandidateAt = undefined;
|
|
1276
|
-
rt.activePromptMessageID = undefined;
|
|
1277
|
-
rt.activeToolCallIDs = [];
|
|
1278
|
-
rt.updatedAt = new Date().toISOString();
|
|
1279
|
-
return s;
|
|
1280
|
-
});
|
|
1281
|
-
await continuation.continueTurn(directory, goalID, { force: true });
|
|
1282
|
-
return { woke: true, message: `Woke goal "${goal.name}" for command exit.` };
|
|
1360
|
+
const result = await continuation.nudge(directory, goalID);
|
|
1361
|
+
return { woke: result.ok && result.outcome === "sent", message: result.message };
|
|
1283
1362
|
}
|
|
1284
1363
|
var init_command_await2 = __esm(() => {
|
|
1285
1364
|
init_command_await();
|
|
@@ -1351,8 +1430,11 @@ function scopeConflictMessage(conflict) {
|
|
|
1351
1430
|
var init_workspace_scope = () => {};
|
|
1352
1431
|
|
|
1353
1432
|
// src/application/workspace-execution.ts
|
|
1354
|
-
import { randomUUID as
|
|
1433
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
1355
1434
|
import path3 from "path";
|
|
1435
|
+
function isWorkspaceToolExempt(tool) {
|
|
1436
|
+
return readTools.has(tool);
|
|
1437
|
+
}
|
|
1356
1438
|
function structuredWritePaths(tool, args) {
|
|
1357
1439
|
if (tool === "write" || tool === "edit") {
|
|
1358
1440
|
if (typeof args.filePath !== "string")
|
|
@@ -1391,107 +1473,31 @@ function structuredWritePaths(tool, args) {
|
|
|
1391
1473
|
throw new Error("Scope denied: patch has no file operations.");
|
|
1392
1474
|
return paths;
|
|
1393
1475
|
}
|
|
1394
|
-
function
|
|
1395
|
-
|
|
1396
|
-
return Number.POSITIVE_INFINITY;
|
|
1397
|
-
const age = now - Date.parse(call.at);
|
|
1398
|
-
return Number.isFinite(age) ? age : Number.POSITIVE_INFINITY;
|
|
1399
|
-
}
|
|
1400
|
-
function runtimeActivityFresh(state, goalID, now) {
|
|
1401
|
-
if (!goalID)
|
|
1402
|
-
return false;
|
|
1403
|
-
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
1404
|
-
if (!rt?.lastActivityAt)
|
|
1405
|
-
return false;
|
|
1406
|
-
const age = now - Date.parse(rt.lastActivityAt);
|
|
1407
|
-
return Number.isFinite(age) && age < STALE_WORKSPACE_CALL_TTL_MS;
|
|
1408
|
-
}
|
|
1409
|
-
function isGenerationSuperseded(state, call) {
|
|
1410
|
-
if (call.generation === undefined || !call.goalID)
|
|
1411
|
-
return false;
|
|
1412
|
-
const rt = state.runtimes.find((r) => r.goalID === call.goalID);
|
|
1413
|
-
return !!rt && call.generation < rt.runGeneration;
|
|
1414
|
-
}
|
|
1415
|
-
function isSyncStaleWorkspaceCall(state, call, now = Date.now()) {
|
|
1416
|
-
if (isGenerationSuperseded(state, call))
|
|
1417
|
-
return true;
|
|
1418
|
-
if (call.goalID && !state.goals.some((g) => g.id === call.goalID))
|
|
1419
|
-
return true;
|
|
1420
|
-
return callAgeMs(call, now) > CLEAR_PENDING_FORCE_TTL_MS;
|
|
1476
|
+
function isSyncStaleWorkspaceCall(_state, _call, _now = Date.now()) {
|
|
1477
|
+
return false;
|
|
1421
1478
|
}
|
|
1422
1479
|
function isStaleWorkspaceCall(state, call, liveness = {}) {
|
|
1423
|
-
|
|
1424
|
-
return true;
|
|
1425
|
-
const now = liveness.now ?? Date.now();
|
|
1426
|
-
const age = callAgeMs(call, now);
|
|
1427
|
-
if (!call.goalID)
|
|
1428
|
-
return age > OWNER_CALL_TTL_MS;
|
|
1429
|
-
const goal = state.goals.find((g) => g.id === call.goalID);
|
|
1430
|
-
if (!goal)
|
|
1431
|
-
return true;
|
|
1432
|
-
if (age <= STALE_WORKSPACE_CALL_TTL_MS)
|
|
1433
|
-
return false;
|
|
1434
|
-
const live = liveness.isWorkerLive?.(goal.id, goal.workerSessionID) ?? false;
|
|
1435
|
-
if (live && runtimeActivityFresh(state, goal.id, now))
|
|
1436
|
-
return false;
|
|
1437
|
-
if (live) {
|
|
1438
|
-
return age > CLEAR_PENDING_FORCE_TTL_MS;
|
|
1439
|
-
}
|
|
1440
|
-
return true;
|
|
1480
|
+
return isSyncStaleWorkspaceCall(state, call, liveness.now ?? Date.now());
|
|
1441
1481
|
}
|
|
1442
1482
|
function sweepStaleWorkspaceState(state, liveness = {}) {
|
|
1443
1483
|
const now = liveness.now ?? Date.now();
|
|
1444
1484
|
const summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
const kept = before.filter((call) => {
|
|
1448
|
-
if (call.goalID && clearPendingGoals.has(call.goalID) && !isGenerationSuperseded(state, call) && state.goals.some((g) => g.id === call.goalID)) {
|
|
1449
|
-
return true;
|
|
1450
|
-
}
|
|
1451
|
-
if (!isStaleWorkspaceCall(state, call, { ...liveness, now }))
|
|
1452
|
-
return true;
|
|
1453
|
-
summary.reapedCalls.push({
|
|
1454
|
-
callID: call.callID,
|
|
1455
|
-
sessionID: call.sessionID,
|
|
1456
|
-
goalID: call.goalID,
|
|
1457
|
-
reason: isGenerationSuperseded(state, call) ? "generation-superseded" : call.goalID && !state.goals.some((g) => g.id === call.goalID) ? "goal-gone" : "age-ttl-no-live-backing"
|
|
1458
|
-
});
|
|
1459
|
-
return false;
|
|
1460
|
-
});
|
|
1461
|
-
state.workspaceCalls = kept;
|
|
1462
|
-
for (const goal of state.goals) {
|
|
1485
|
+
const kept = state.workspaceCalls ?? [];
|
|
1486
|
+
for (const goal of [...state.goals]) {
|
|
1463
1487
|
const referenced = kept.some((call) => call.goalID === goal.id) || state.workspaceOperation?.goalID === goal.id;
|
|
1464
1488
|
if (goal.scopeClosing && !referenced && !(liveness.isWorkerLive?.(goal.id, goal.workerSessionID) ?? false)) {
|
|
1465
1489
|
goal.scopeClosing = false;
|
|
1466
1490
|
goal.updatedAt = new Date(now).toISOString();
|
|
1467
1491
|
summary.resetClosing.push(goal.id);
|
|
1468
1492
|
}
|
|
1469
|
-
if (goal.scopeClearPending
|
|
1493
|
+
if (goal.scopeClearPending) {
|
|
1494
|
+
retireWorker(state, goal);
|
|
1470
1495
|
state.goals = state.goals.filter((g) => g.id !== goal.id);
|
|
1471
1496
|
state.runtimes = state.runtimes.filter((r) => r.goalID !== goal.id);
|
|
1472
1497
|
state.commandAwaits = (state.commandAwaits ?? []).filter((a) => a.goalID !== goal.id);
|
|
1473
1498
|
summary.finalizedClears.push(goal.id);
|
|
1474
1499
|
}
|
|
1475
1500
|
}
|
|
1476
|
-
for (const goal of [...state.goals]) {
|
|
1477
|
-
if (!goal.scopeClearPending)
|
|
1478
|
-
continue;
|
|
1479
|
-
const refs = (state.workspaceCalls ?? []).filter((call) => call.goalID === goal.id);
|
|
1480
|
-
if (!refs.length)
|
|
1481
|
-
continue;
|
|
1482
|
-
if (refs.every((call) => callAgeMs(call, now) > CLEAR_PENDING_FORCE_TTL_MS)) {
|
|
1483
|
-
for (const call of refs) {
|
|
1484
|
-
summary.reapedCalls.push({ callID: call.callID, sessionID: call.sessionID, goalID: call.goalID, reason: "clear-pending-force" });
|
|
1485
|
-
}
|
|
1486
|
-
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID !== goal.id);
|
|
1487
|
-
if (!(state.workspaceCalls ?? []).some((call) => call.goalID === goal.id) && state.workspaceOperation?.goalID !== goal.id) {
|
|
1488
|
-
state.goals = state.goals.filter((g) => g.id !== goal.id);
|
|
1489
|
-
state.runtimes = state.runtimes.filter((r) => r.goalID !== goal.id);
|
|
1490
|
-
state.commandAwaits = (state.commandAwaits ?? []).filter((a) => a.goalID !== goal.id);
|
|
1491
|
-
summary.forceCleared.push(goal.id);
|
|
1492
|
-
}
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
1501
|
return summary;
|
|
1496
1502
|
}
|
|
1497
1503
|
function stampCallGeneration(state, goalID) {
|
|
@@ -1557,7 +1563,7 @@ async function beforeWorkspaceTool(directory, input, args) {
|
|
|
1557
1563
|
await mutateState(directory, `workspace.before:${input.callID}`, async (state) => {
|
|
1558
1564
|
const goal = state.goals.find((item) => item.workerSessionID === input.sessionID);
|
|
1559
1565
|
if (state.retiredWorkerSessions?.includes(input.sessionID))
|
|
1560
|
-
throw new Error("Scope denied: retired worker session; late executions are fenced.");
|
|
1566
|
+
throw new Error("Scope denied: retired worker session; goal cleared or worker rotated. Stop work; late executions are fenced.");
|
|
1561
1567
|
const held = heldGoals(state);
|
|
1562
1568
|
const protectedGoals = held.filter((item) => item.config.write_scope !== undefined || item.config.workspaceWrite === false);
|
|
1563
1569
|
if (!goal && ownerTools.has(input.tool) && (!protectedGoals.length || state.goals.some((item) => item.ownerSessionID === input.sessionID)))
|
|
@@ -1613,7 +1619,12 @@ async function beforeWorkspaceTool(directory, input, args) {
|
|
|
1613
1619
|
return state;
|
|
1614
1620
|
});
|
|
1615
1621
|
}
|
|
1616
|
-
async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
1622
|
+
async function afterWorkspaceTool(directory, sessionID, callID, tool) {
|
|
1623
|
+
if (tool && isWorkspaceToolExempt(tool))
|
|
1624
|
+
return;
|
|
1625
|
+
const snapshot = await readState(directory);
|
|
1626
|
+
if (!snapshot.workspaceCalls?.some((call) => call.sessionID === sessionID && call.callID === callID))
|
|
1627
|
+
return;
|
|
1617
1628
|
await mutateState(directory, `workspace.after:${callID}`, async (state) => {
|
|
1618
1629
|
state.workspaceCalls = state.workspaceCalls?.filter((call) => call.sessionID !== sessionID || call.callID !== callID);
|
|
1619
1630
|
const now = Date.now();
|
|
@@ -1621,6 +1632,7 @@ async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
|
1621
1632
|
const live = (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1622
1633
|
if (!goal.scopeClearPending || live || state.workspaceOperation?.goalID === goal.id)
|
|
1623
1634
|
return true;
|
|
1635
|
+
retireWorker(state, goal);
|
|
1624
1636
|
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => !(call.goalID === goal.id && isSyncStaleWorkspaceCall(state, call, now)));
|
|
1625
1637
|
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
1626
1638
|
return false;
|
|
@@ -1628,25 +1640,95 @@ async function afterWorkspaceTool(directory, sessionID, callID) {
|
|
|
1628
1640
|
return state;
|
|
1629
1641
|
});
|
|
1630
1642
|
}
|
|
1631
|
-
function
|
|
1643
|
+
async function releaseTerminalWorkspaceCalls(directory, evidence) {
|
|
1644
|
+
if (!evidence.length)
|
|
1645
|
+
return 0;
|
|
1646
|
+
const snapshot = await readState(directory);
|
|
1647
|
+
const relevant = evidence.some((proof) => snapshot.workspaceCalls?.some((call) => call.sessionID === proof.sessionID && call.callID === proof.callID) || snapshot.goals.some((goal) => goal.workerSessionID === proof.sessionID && snapshot.runtimes.some((rt) => rt.goalID === goal.id && rt.activeToolCallIDs?.includes(proof.callID))));
|
|
1648
|
+
if (!relevant)
|
|
1649
|
+
return 0;
|
|
1650
|
+
let released = 0;
|
|
1651
|
+
await mutateState(directory, "workspace.terminal-evidence", async (state) => {
|
|
1652
|
+
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => {
|
|
1653
|
+
const proof = evidence.find((item) => item.sessionID === call.sessionID && item.callID === call.callID && (item.status === "completed" || item.status === "error") && Number.isFinite(item.endedAt) && typeof call.at === "string" && Number.isFinite(Date.parse(call.at)) && item.endedAt >= Date.parse(call.at));
|
|
1654
|
+
if (!proof)
|
|
1655
|
+
return true;
|
|
1656
|
+
released += 1;
|
|
1657
|
+
const goal = state.goals.find((item) => item.id === call.goalID && item.workerSessionID === call.sessionID);
|
|
1658
|
+
const rt = goal && state.runtimes.find((item) => item.goalID === goal.id);
|
|
1659
|
+
if (rt && (call.generation === undefined || rt.runGeneration === call.generation)) {
|
|
1660
|
+
rt.activeToolCallIDs = rt.activeToolCallIDs?.filter((id) => id !== call.callID);
|
|
1661
|
+
}
|
|
1662
|
+
return false;
|
|
1663
|
+
});
|
|
1664
|
+
for (const goal of state.goals) {
|
|
1665
|
+
const rt = state.runtimes.find((item) => item.goalID === goal.id);
|
|
1666
|
+
const started = rt?.turnStartedAt || rt?.lastRunAt ? Date.parse(rt.turnStartedAt ?? rt.lastRunAt) : NaN;
|
|
1667
|
+
if (!rt?.activeToolCallIDs?.length || !Number.isFinite(started))
|
|
1668
|
+
continue;
|
|
1669
|
+
rt.activeToolCallIDs = rt.activeToolCallIDs.filter((callID) => {
|
|
1670
|
+
if (state.workspaceCalls?.some((call) => call.sessionID === goal.workerSessionID && call.callID === callID))
|
|
1671
|
+
return true;
|
|
1672
|
+
return !evidence.some((item) => item.sessionID === goal.workerSessionID && item.callID === callID && (item.status === "completed" || item.status === "error") && Number.isFinite(item.endedAt) && item.endedAt >= started);
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return state;
|
|
1676
|
+
});
|
|
1677
|
+
if (released)
|
|
1678
|
+
await logServerEvent(directory, "workspace.terminal-evidence-released", { released }).catch(() => {});
|
|
1679
|
+
return released;
|
|
1680
|
+
}
|
|
1681
|
+
async function reconcileTerminalWorkspaceCalls(directory, host) {
|
|
1682
|
+
if (!host.readToolExecutions)
|
|
1683
|
+
return;
|
|
1684
|
+
const state = await readState(directory);
|
|
1685
|
+
const sessions = [...new Set([
|
|
1686
|
+
...(state.workspaceCalls ?? []).map((call) => call.sessionID),
|
|
1687
|
+
...state.goals.filter((goal) => goal.workerSessionID && state.runtimes.some((rt) => rt.goalID === goal.id && rt.activeToolCallIDs?.length)).map((goal) => goal.workerSessionID)
|
|
1688
|
+
])];
|
|
1689
|
+
if (!sessions.length)
|
|
1690
|
+
return;
|
|
1691
|
+
const proof = await Promise.all(sessions.map(async (sessionID) => {
|
|
1692
|
+
try {
|
|
1693
|
+
return (await host.readToolExecutions(sessionID)).filter((item) => item.sessionID === sessionID);
|
|
1694
|
+
} catch {
|
|
1695
|
+
return [];
|
|
1696
|
+
}
|
|
1697
|
+
}));
|
|
1698
|
+
await releaseTerminalWorkspaceCalls(directory, proof.flat());
|
|
1699
|
+
}
|
|
1700
|
+
function guardV2ToolEditor(directory, editor, lifecycle) {
|
|
1632
1701
|
if (typeof editor.list !== "function" || typeof editor.update !== "function")
|
|
1633
1702
|
throw new Error("Scoped execution requires the v2 tool editor list/update execution surface.");
|
|
1703
|
+
const guarded = new Set;
|
|
1634
1704
|
for (const entry of editor.list()) {
|
|
1705
|
+
guarded.add(entry.id);
|
|
1635
1706
|
editor.update(entry.id, (definition) => {
|
|
1636
1707
|
const execute = definition.execute;
|
|
1637
1708
|
definition.execute = async (args, context) => {
|
|
1638
|
-
|
|
1709
|
+
const input = { tool: entry.id, sessionID: context.sessionID, callID: `${context.id}:${randomUUID4()}` };
|
|
1710
|
+
const values = args;
|
|
1711
|
+
if (lifecycle)
|
|
1712
|
+
await lifecycle.before(input, values);
|
|
1713
|
+
else
|
|
1714
|
+
await beforeWorkspaceTool(directory, input, values);
|
|
1715
|
+
let result;
|
|
1639
1716
|
try {
|
|
1640
|
-
|
|
1717
|
+
result = await execute(args, context);
|
|
1718
|
+
return result;
|
|
1641
1719
|
} finally {
|
|
1642
|
-
|
|
1720
|
+
if (lifecycle)
|
|
1721
|
+
await lifecycle.after(input, values, result);
|
|
1722
|
+
else
|
|
1723
|
+
await afterWorkspaceTool(directory, input.sessionID, input.callID, input.tool);
|
|
1643
1724
|
}
|
|
1644
1725
|
};
|
|
1645
1726
|
});
|
|
1646
1727
|
}
|
|
1728
|
+
return guarded;
|
|
1647
1729
|
}
|
|
1648
1730
|
async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
1649
|
-
const id =
|
|
1731
|
+
const id = randomUUID4();
|
|
1650
1732
|
await mutateState(directory, `workspace.operation:${id}`, async (state) => {
|
|
1651
1733
|
const now = Date.now();
|
|
1652
1734
|
const live = (state.workspaceCalls ?? []).filter((call) => !isSyncStaleWorkspaceCall(state, call, now));
|
|
@@ -1664,8 +1746,9 @@ async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
|
1664
1746
|
const now = Date.now();
|
|
1665
1747
|
state.goals = state.goals.filter((goal) => {
|
|
1666
1748
|
const live = (state.workspaceCalls ?? []).some((call) => call.goalID === goal.id && !isSyncStaleWorkspaceCall(state, call, now));
|
|
1667
|
-
if (!goal.scopeClearPending || live)
|
|
1749
|
+
if (!goal.scopeClearPending || live || state.workspaceOperation?.goalID === goal.id)
|
|
1668
1750
|
return true;
|
|
1751
|
+
retireWorker(state, goal);
|
|
1669
1752
|
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => !(call.goalID === goal.id && isSyncStaleWorkspaceCall(state, call, now)));
|
|
1670
1753
|
state.runtimes = state.runtimes.filter((runtime) => runtime.goalID !== goal.id);
|
|
1671
1754
|
return false;
|
|
@@ -1677,6 +1760,7 @@ async function withWorkspaceOperation(directory, goalID, sessionID, run) {
|
|
|
1677
1760
|
var readTools, ownerTools, STALE_WORKSPACE_CALL_TTL_MS, CLEAR_PENDING_FORCE_TTL_MS, OWNER_CALL_TTL_MS;
|
|
1678
1761
|
var init_workspace_execution = __esm(() => {
|
|
1679
1762
|
init_state_repository();
|
|
1763
|
+
init_server_log();
|
|
1680
1764
|
init_workspace_scope();
|
|
1681
1765
|
readTools = new Set([
|
|
1682
1766
|
"read",
|
|
@@ -1726,7 +1810,7 @@ import { tool as v1Tool } from "@opencode-ai/plugin/tool";
|
|
|
1726
1810
|
|
|
1727
1811
|
// src/application/control-worker.ts
|
|
1728
1812
|
init_state_repository();
|
|
1729
|
-
import { randomUUID as
|
|
1813
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
1730
1814
|
|
|
1731
1815
|
// src/application/goal-policy.ts
|
|
1732
1816
|
function resolveGoalCreationConfig(input) {
|
|
@@ -1799,42 +1883,8 @@ function cleanList(value) {
|
|
|
1799
1883
|
return value.map(cleanText).filter((item) => Boolean(item));
|
|
1800
1884
|
}
|
|
1801
1885
|
|
|
1802
|
-
// src/infrastructure/server-log.ts
|
|
1803
|
-
import { appendFile } from "fs/promises";
|
|
1804
|
-
var SERVER_LOG_FILE = "/tmp/loopd-server.log";
|
|
1805
|
-
async function logServerEvent(directory, event, details = {}) {
|
|
1806
|
-
try {
|
|
1807
|
-
await appendFile(SERVER_LOG_FILE, `${JSON.stringify({
|
|
1808
|
-
timestamp: new Date().toISOString(),
|
|
1809
|
-
directory,
|
|
1810
|
-
event,
|
|
1811
|
-
...details
|
|
1812
|
-
}, errorReplacer)}
|
|
1813
|
-
`);
|
|
1814
|
-
} catch {}
|
|
1815
|
-
}
|
|
1816
|
-
function describeError(value) {
|
|
1817
|
-
if (value instanceof Error)
|
|
1818
|
-
return value.message;
|
|
1819
|
-
if (typeof value === "string")
|
|
1820
|
-
return value;
|
|
1821
|
-
if (typeof value === "object" && value !== null && "message" in value) {
|
|
1822
|
-
return String(value.message);
|
|
1823
|
-
}
|
|
1824
|
-
try {
|
|
1825
|
-
return JSON.stringify(value, errorReplacer);
|
|
1826
|
-
} catch {
|
|
1827
|
-
return String(value);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
function errorReplacer(_key, value) {
|
|
1831
|
-
if (value instanceof Error) {
|
|
1832
|
-
return { name: value.name, message: value.message, stack: value.stack };
|
|
1833
|
-
}
|
|
1834
|
-
return value;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
1886
|
// src/application/control-worker.ts
|
|
1887
|
+
init_server_log();
|
|
1838
1888
|
init_command_await2();
|
|
1839
1889
|
var MAX_LEDGER_SIZE = 100;
|
|
1840
1890
|
var RESPONSE_CLEANUP_AGE_MS = 60 * 60 * 1000;
|
|
@@ -1998,23 +2048,25 @@ function createControlWorker(options) {
|
|
|
1998
2048
|
break;
|
|
1999
2049
|
}
|
|
2000
2050
|
case "resume": {
|
|
2001
|
-
await goalSvc.resume(directory, request.goalID);
|
|
2051
|
+
const result = await goalSvc.resume(directory, request.goalID);
|
|
2002
2052
|
const state = await readState(directory);
|
|
2003
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
2004
2053
|
response = {
|
|
2005
2054
|
...base,
|
|
2006
|
-
|
|
2055
|
+
ok: result.ok,
|
|
2056
|
+
message: result.message,
|
|
2057
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
2007
2058
|
stateRevision: state.revision
|
|
2008
2059
|
};
|
|
2009
2060
|
break;
|
|
2010
2061
|
}
|
|
2011
2062
|
case "retry": {
|
|
2012
|
-
await goalSvc.retry(directory, request.goalID);
|
|
2063
|
+
const result = await goalSvc.retry(directory, request.goalID);
|
|
2013
2064
|
const state = await readState(directory);
|
|
2014
|
-
const goal = state.goals.find((g) => g.id === request.goalID);
|
|
2015
2065
|
response = {
|
|
2016
2066
|
...base,
|
|
2017
|
-
|
|
2067
|
+
ok: result.ok,
|
|
2068
|
+
message: result.message,
|
|
2069
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
2018
2070
|
stateRevision: state.revision
|
|
2019
2071
|
};
|
|
2020
2072
|
break;
|
|
@@ -2030,6 +2082,7 @@ function createControlWorker(options) {
|
|
|
2030
2082
|
...base,
|
|
2031
2083
|
ok: result.ok,
|
|
2032
2084
|
message: result.message,
|
|
2085
|
+
errorCode: result.ok ? undefined : result.outcome,
|
|
2033
2086
|
stateRevision: state.revision
|
|
2034
2087
|
};
|
|
2035
2088
|
break;
|
|
@@ -2124,6 +2177,7 @@ function createControlWorker(options) {
|
|
|
2124
2177
|
...base,
|
|
2125
2178
|
ok: sent.ok,
|
|
2126
2179
|
message: sent.message,
|
|
2180
|
+
errorCode: sent.ok ? undefined : sent.outcome,
|
|
2127
2181
|
stateRevision: state.revision
|
|
2128
2182
|
};
|
|
2129
2183
|
break;
|
|
@@ -2186,7 +2240,7 @@ function createControlWorker(options) {
|
|
|
2186
2240
|
});
|
|
2187
2241
|
await appendEvent(directory, {
|
|
2188
2242
|
version: 1,
|
|
2189
|
-
eventID:
|
|
2243
|
+
eventID: randomUUID3(),
|
|
2190
2244
|
goalID: goal.id,
|
|
2191
2245
|
type: "goal.completed",
|
|
2192
2246
|
summary: goal.completionEvidence.summary,
|
|
@@ -2241,7 +2295,7 @@ function createControlWorker(options) {
|
|
|
2241
2295
|
});
|
|
2242
2296
|
await appendEvent(directory, {
|
|
2243
2297
|
version: 1,
|
|
2244
|
-
eventID:
|
|
2298
|
+
eventID: randomUUID3(),
|
|
2245
2299
|
goalID: goal.id,
|
|
2246
2300
|
type: "goal.blocked",
|
|
2247
2301
|
reason: goal.blocker.reason,
|
|
@@ -2409,45 +2463,18 @@ function createControlWorker(options) {
|
|
|
2409
2463
|
|
|
2410
2464
|
// src/application/loop-engine.ts
|
|
2411
2465
|
init_state_repository();
|
|
2412
|
-
|
|
2466
|
+
init_workspace_execution();
|
|
2467
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
2413
2468
|
|
|
2414
|
-
// src/domain/
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
budget_limited: ["complete", "blocked"],
|
|
2420
|
-
usage_limited: [],
|
|
2421
|
-
complete: []
|
|
2422
|
-
};
|
|
2423
|
-
var USER_TRANSITIONS = {
|
|
2424
|
-
active: ["paused"],
|
|
2425
|
-
paused: ["active"],
|
|
2426
|
-
blocked: ["active"],
|
|
2427
|
-
budget_limited: ["active"],
|
|
2428
|
-
usage_limited: ["active"],
|
|
2429
|
-
complete: ["active"]
|
|
2430
|
-
};
|
|
2431
|
-
var SYSTEM_TRANSITIONS = {
|
|
2432
|
-
active: ["budget_limited", "usage_limited"],
|
|
2433
|
-
paused: [],
|
|
2434
|
-
blocked: [],
|
|
2435
|
-
budget_limited: [],
|
|
2436
|
-
usage_limited: [],
|
|
2437
|
-
complete: []
|
|
2438
|
-
};
|
|
2439
|
-
function canTransition(current, target, caller) {
|
|
2440
|
-
const table = caller === "model" ? MODEL_TRANSITIONS : caller === "user" ? USER_TRANSITIONS : SYSTEM_TRANSITIONS;
|
|
2441
|
-
return table[current]?.includes(target) ?? false;
|
|
2442
|
-
}
|
|
2443
|
-
function isTerminal(status) {
|
|
2444
|
-
return status === "complete";
|
|
2445
|
-
}
|
|
2446
|
-
function createGoal(input) {
|
|
2447
|
-
const now = new Date().toISOString();
|
|
2448
|
-
return { ...input, tokensUsed: 0, costUsed: 0, timeUsedSeconds: 0, createdAt: now, updatedAt: now };
|
|
2469
|
+
// src/domain/tool-execution.ts
|
|
2470
|
+
function terminalToolExecution(part) {
|
|
2471
|
+
if (part?.type !== "tool" || typeof part.sessionID !== "string" || !part.sessionID || typeof part.callID !== "string" || !part.callID || part.state?.status !== "completed" && part.state?.status !== "error" || typeof part.state?.time?.end !== "number" || !Number.isFinite(part.state.time.end) || part.state.time.end <= 0)
|
|
2472
|
+
return;
|
|
2473
|
+
return { sessionID: part.sessionID, callID: part.callID, status: part.state.status, endedAt: part.state.time.end };
|
|
2449
2474
|
}
|
|
2475
|
+
|
|
2450
2476
|
// src/application/loop-engine.ts
|
|
2477
|
+
init_server_log();
|
|
2451
2478
|
var CONFIRM_IDLE_DURATION_MS = 2000;
|
|
2452
2479
|
var HANDLED_EVENT_TYPES = new Set([
|
|
2453
2480
|
"session.idle",
|
|
@@ -2468,6 +2495,7 @@ function createLoopEngine(options) {
|
|
|
2468
2495
|
const idleRecoverMs = options.idleRecoverMs ?? 3 * 60000;
|
|
2469
2496
|
let running = false;
|
|
2470
2497
|
let maintenanceTimer;
|
|
2498
|
+
let maintaining = false;
|
|
2471
2499
|
let knownWorkerSessions = new Set;
|
|
2472
2500
|
let knownWorkerSessionsLoaded = false;
|
|
2473
2501
|
const inflightContinuations = new Set;
|
|
@@ -2499,8 +2527,12 @@ function createLoopEngine(options) {
|
|
|
2499
2527
|
running = true;
|
|
2500
2528
|
loadWorkerSessionsIfneeded().catch(() => {});
|
|
2501
2529
|
maintenanceTimer = setInterval(() => {
|
|
2502
|
-
if (running)
|
|
2503
|
-
|
|
2530
|
+
if (running && !maintaining) {
|
|
2531
|
+
maintaining = true;
|
|
2532
|
+
maintenance().catch(() => {}).finally(() => {
|
|
2533
|
+
maintaining = false;
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2504
2536
|
}, maintenanceMs);
|
|
2505
2537
|
}
|
|
2506
2538
|
function stop() {
|
|
@@ -2513,13 +2545,21 @@ function createLoopEngine(options) {
|
|
|
2513
2545
|
function isRunning() {
|
|
2514
2546
|
return running;
|
|
2515
2547
|
}
|
|
2516
|
-
async function continueGoal(goalID) {
|
|
2548
|
+
async function continueGoal(goalID, opts) {
|
|
2517
2549
|
if (inflightContinuations.has(goalID))
|
|
2518
2550
|
return false;
|
|
2519
2551
|
inflightContinuations.add(goalID);
|
|
2520
2552
|
try {
|
|
2521
|
-
await
|
|
2522
|
-
|
|
2553
|
+
const state = await readState(directory);
|
|
2554
|
+
const goal = state.goals.find((g) => g.id === goalID);
|
|
2555
|
+
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
2556
|
+
if (!running || !goal || goal.status !== "active" || goal.interactive || !rt)
|
|
2557
|
+
return false;
|
|
2558
|
+
const forceFinish = opts?.forceFinish || enforceLimits(goal, rt).stop === "force_finish";
|
|
2559
|
+
const result = await goalService.continueTurn(directory, goalID, { forceFinish });
|
|
2560
|
+
if (!result.ok)
|
|
2561
|
+
await logServerEvent(directory, "engine.continuation-denied", { goalID, outcome: result.outcome, reason: result.message });
|
|
2562
|
+
return result.ok && result.outcome === "sent";
|
|
2523
2563
|
} finally {
|
|
2524
2564
|
inflightContinuations.delete(goalID);
|
|
2525
2565
|
}
|
|
@@ -2530,6 +2570,11 @@ function createLoopEngine(options) {
|
|
|
2530
2570
|
const type = event.type;
|
|
2531
2571
|
if (!type || !HANDLED_EVENT_TYPES.has(type))
|
|
2532
2572
|
return false;
|
|
2573
|
+
if (type === "message.part.updated") {
|
|
2574
|
+
const proof = terminalToolExecution(event.properties?.part);
|
|
2575
|
+
if (proof)
|
|
2576
|
+
await releaseTerminalWorkspaceCalls(directory, [proof]);
|
|
2577
|
+
}
|
|
2533
2578
|
const sessionID = eventSessionID(event);
|
|
2534
2579
|
if (!sessionID)
|
|
2535
2580
|
return false;
|
|
@@ -2573,8 +2618,9 @@ function createLoopEngine(options) {
|
|
|
2573
2618
|
async function handleMessageActivity(goal, event) {
|
|
2574
2619
|
let matched = false;
|
|
2575
2620
|
await mutateState(directory, `message-activity:${goal.id}`, async (s) => {
|
|
2621
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2576
2622
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2577
|
-
if (!rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2623
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2578
2624
|
return s;
|
|
2579
2625
|
if (event.type === "message.updated") {
|
|
2580
2626
|
const info = event.properties?.info;
|
|
@@ -2584,10 +2630,9 @@ function createLoopEngine(options) {
|
|
|
2584
2630
|
matched = true;
|
|
2585
2631
|
} else if (info?.role === "assistant" && info.parentID === rt.activePromptMessageID) {
|
|
2586
2632
|
Object.assign(rt, recordActivity(rt));
|
|
2633
|
+
rt.activePromptObservedAt ??= new Date().toISOString();
|
|
2587
2634
|
rt.activeAssistantMessageID = info.id;
|
|
2588
|
-
|
|
2589
|
-
rt.activeAssistantCompletedAt = new Date(info.time.completed).toISOString();
|
|
2590
|
-
}
|
|
2635
|
+
rt.activeAssistantCompletedAt = info.time?.completed && info.finish !== "tool-calls" ? new Date(info.time.completed).toISOString() : undefined;
|
|
2591
2636
|
matched = true;
|
|
2592
2637
|
}
|
|
2593
2638
|
} else {
|
|
@@ -2606,8 +2651,9 @@ function createLoopEngine(options) {
|
|
|
2606
2651
|
async function handleExecutionSucceeded(goal) {
|
|
2607
2652
|
let matched = false;
|
|
2608
2653
|
await mutateState(directory, `execution-succeeded:${goal.id}`, async (s) => {
|
|
2654
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2609
2655
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2610
|
-
if (!rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2656
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.phase !== "running" || !rt.activePromptMessageID)
|
|
2611
2657
|
return s;
|
|
2612
2658
|
Object.assign(rt, recordActivity(rt));
|
|
2613
2659
|
rt.activeAssistantCompletedAt = new Date().toISOString();
|
|
@@ -2626,7 +2672,7 @@ function createLoopEngine(options) {
|
|
|
2626
2672
|
const g = s.goals.find((item) => item.id === goalID);
|
|
2627
2673
|
if (!g)
|
|
2628
2674
|
return s;
|
|
2629
|
-
if (
|
|
2675
|
+
if (g.status !== "active" || g.interactive || g.workerSessionID !== goal.workerSessionID)
|
|
2630
2676
|
return s;
|
|
2631
2677
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2632
2678
|
if (!rt)
|
|
@@ -2674,7 +2720,7 @@ function createLoopEngine(options) {
|
|
|
2674
2720
|
const stagedRt = afterIdle.runtimes.find((r) => r.goalID === goalID);
|
|
2675
2721
|
const eventAnchored = stagedRt?.runGeneration === candidate.generation && Boolean(stagedRt?.activeAssistantCompletedAt);
|
|
2676
2722
|
const transcript = await inspectPromptTurn(goal.workerSessionID, candidate.promptMessageID);
|
|
2677
|
-
const failureReason = !transcript.latestUserPrompt && !eventAnchored ? "prompt-outside-window" : !eventAnchored && !candidate.assistantCompleted && !transcript.assistantCompleted ? "assistant-incomplete" : undefined;
|
|
2723
|
+
const failureReason = transcript.assistantInProgress ? "assistant-incomplete" : !transcript.latestUserPrompt && !eventAnchored ? "prompt-outside-window" : !eventAnchored && !candidate.assistantCompleted && !transcript.assistantCompleted ? "assistant-incomplete" : undefined;
|
|
2678
2724
|
if (failureReason) {
|
|
2679
2725
|
let newlyStamped = false;
|
|
2680
2726
|
await mutateState(directory, `idle.confirm-stamp:${goalID}`, async (s) => {
|
|
@@ -2691,7 +2737,7 @@ function createLoopEngine(options) {
|
|
|
2691
2737
|
if (newlyStamped) {
|
|
2692
2738
|
await appendEvent(directory, {
|
|
2693
2739
|
version: 1,
|
|
2694
|
-
eventID:
|
|
2740
|
+
eventID: randomUUID5(),
|
|
2695
2741
|
goalID,
|
|
2696
2742
|
type: "idle.confirm-failed",
|
|
2697
2743
|
reason: failureReason,
|
|
@@ -2705,7 +2751,7 @@ function createLoopEngine(options) {
|
|
|
2705
2751
|
afterIdle = await mutateState(directory, `idle.confirm:${goalID}`, async (s) => {
|
|
2706
2752
|
const g = s.goals.find((item) => item.id === goalID);
|
|
2707
2753
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2708
|
-
if (!g || !rt ||
|
|
2754
|
+
if (!g || !rt || g.status !== "active" || g.interactive || g.workerSessionID !== goal.workerSessionID)
|
|
2709
2755
|
return s;
|
|
2710
2756
|
if (rt.phase !== "running")
|
|
2711
2757
|
return s;
|
|
@@ -2736,7 +2782,7 @@ function createLoopEngine(options) {
|
|
|
2736
2782
|
if (completedRunID) {
|
|
2737
2783
|
await appendEvent(directory, {
|
|
2738
2784
|
version: 1,
|
|
2739
|
-
eventID:
|
|
2785
|
+
eventID: randomUUID5(),
|
|
2740
2786
|
goalID,
|
|
2741
2787
|
type: "run.completed",
|
|
2742
2788
|
runID: completedRunID,
|
|
@@ -2756,13 +2802,7 @@ function createLoopEngine(options) {
|
|
|
2756
2802
|
const limitResult = enforceLimits(freshGoal, freshRuntime);
|
|
2757
2803
|
if (limitResult.stop === "force_finish") {
|
|
2758
2804
|
if (!freshRuntime.forceFinishRequested) {
|
|
2759
|
-
await
|
|
2760
|
-
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
2761
|
-
if (rt)
|
|
2762
|
-
rt.forceFinishRequested = true;
|
|
2763
|
-
return s;
|
|
2764
|
-
});
|
|
2765
|
-
await goalService.continueTurn(directory, goalID, { forceFinish: true });
|
|
2805
|
+
await continueGoal(goalID, { forceFinish: true });
|
|
2766
2806
|
return true;
|
|
2767
2807
|
}
|
|
2768
2808
|
const blockedKey = goalID;
|
|
@@ -2801,7 +2841,7 @@ function createLoopEngine(options) {
|
|
|
2801
2841
|
});
|
|
2802
2842
|
await appendEvent(directory, {
|
|
2803
2843
|
version: 1,
|
|
2804
|
-
eventID:
|
|
2844
|
+
eventID: randomUUID5(),
|
|
2805
2845
|
goalID,
|
|
2806
2846
|
type: "goal.blocked",
|
|
2807
2847
|
reason: limitResult.reason + " (force-finish ignored)",
|
|
@@ -2825,7 +2865,7 @@ function createLoopEngine(options) {
|
|
|
2825
2865
|
});
|
|
2826
2866
|
await appendEvent(directory, {
|
|
2827
2867
|
version: 1,
|
|
2828
|
-
eventID:
|
|
2868
|
+
eventID: randomUUID5(),
|
|
2829
2869
|
goalID,
|
|
2830
2870
|
type: "goal.status_changed",
|
|
2831
2871
|
from: "active",
|
|
@@ -2843,7 +2883,7 @@ function createLoopEngine(options) {
|
|
|
2843
2883
|
return true;
|
|
2844
2884
|
}
|
|
2845
2885
|
async function inspectPromptTurn(workerSessionID, promptMessageID) {
|
|
2846
|
-
const noMatch = { latestUserPrompt: false, assistantCompleted: false };
|
|
2886
|
+
const noMatch = { latestUserPrompt: false, assistantCompleted: false, assistantInProgress: false };
|
|
2847
2887
|
if (!workerSessionID)
|
|
2848
2888
|
return noMatch;
|
|
2849
2889
|
let messages;
|
|
@@ -2857,9 +2897,14 @@ function createLoopEngine(options) {
|
|
|
2857
2897
|
return noMatch;
|
|
2858
2898
|
const allTimestamped = users.every((message) => message.timestamp && Number.isFinite(Date.parse(message.timestamp)));
|
|
2859
2899
|
const ordered = allTimestamped ? [...users].sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp)) : users;
|
|
2900
|
+
const assistants = messages.filter((message) => message.role === "assistant" && message.parentMessageID === promptMessageID);
|
|
2901
|
+
const orderedAssistants = assistants.every((message) => Number.isFinite(Date.parse(message.createdAt ?? message.timestamp ?? ""))) ? [...assistants].sort((a, b) => Date.parse(a.createdAt ?? a.timestamp) - Date.parse(b.createdAt ?? b.timestamp)) : assistants;
|
|
2902
|
+
const latestAssistant = orderedAssistants.at(-1);
|
|
2903
|
+
const assistantCompleted = Boolean(latestAssistant?.completedAt) && latestAssistant?.finish !== "tool-calls";
|
|
2860
2904
|
return {
|
|
2861
2905
|
latestUserPrompt: ordered.at(-1)?.messageID === promptMessageID,
|
|
2862
|
-
assistantCompleted
|
|
2906
|
+
assistantCompleted,
|
|
2907
|
+
assistantInProgress: Boolean(latestAssistant) && !assistantCompleted
|
|
2863
2908
|
};
|
|
2864
2909
|
}
|
|
2865
2910
|
async function handleSessionStatus(state, goal, event) {
|
|
@@ -2873,8 +2918,9 @@ function createLoopEngine(options) {
|
|
|
2873
2918
|
if (statusType === "idle")
|
|
2874
2919
|
return handleSessionIdle(state, goal);
|
|
2875
2920
|
await mutateState(directory, `status:${goal.id}`, async (s) => {
|
|
2921
|
+
const g = s.goals.find((item) => item.id === goal.id);
|
|
2876
2922
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2877
|
-
if (!rt)
|
|
2923
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.runGeneration !== runtime.runGeneration)
|
|
2878
2924
|
return s;
|
|
2879
2925
|
if (statusType === "busy" || statusType === "retry") {
|
|
2880
2926
|
Object.assign(rt, recordActivity(rt));
|
|
@@ -2895,7 +2941,7 @@ function createLoopEngine(options) {
|
|
|
2895
2941
|
const newState = await mutateState(directory, `error:${goal.id}`, async (s) => {
|
|
2896
2942
|
const g = s.goals.find((item) => item.id === goal.id);
|
|
2897
2943
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
2898
|
-
if (!rt)
|
|
2944
|
+
if (!g || g.status !== "active" || g.workerSessionID !== goal.workerSessionID || !rt || rt.runGeneration !== runtime.runGeneration)
|
|
2899
2945
|
return s;
|
|
2900
2946
|
rt.consecutiveFailures += 1;
|
|
2901
2947
|
rt.lastError = message;
|
|
@@ -2927,7 +2973,7 @@ function createLoopEngine(options) {
|
|
|
2927
2973
|
});
|
|
2928
2974
|
await appendEvent(directory, {
|
|
2929
2975
|
version: 1,
|
|
2930
|
-
eventID:
|
|
2976
|
+
eventID: randomUUID5(),
|
|
2931
2977
|
goalID: goal.id,
|
|
2932
2978
|
type: "run.failed",
|
|
2933
2979
|
runID: runtime.activeRunID || "unknown",
|
|
@@ -2941,7 +2987,7 @@ function createLoopEngine(options) {
|
|
|
2941
2987
|
if (updatedGoal?.status === "blocked") {
|
|
2942
2988
|
await appendEvent(directory, {
|
|
2943
2989
|
version: 1,
|
|
2944
|
-
eventID:
|
|
2990
|
+
eventID: randomUUID5(),
|
|
2945
2991
|
goalID: goal.id,
|
|
2946
2992
|
type: "goal.blocked",
|
|
2947
2993
|
reason: `Failed ${runtime.consecutiveFailures + 1} times`,
|
|
@@ -2969,7 +3015,7 @@ function createLoopEngine(options) {
|
|
|
2969
3015
|
});
|
|
2970
3016
|
await appendEvent(directory, {
|
|
2971
3017
|
version: 1,
|
|
2972
|
-
eventID:
|
|
3018
|
+
eventID: randomUUID5(),
|
|
2973
3019
|
goalID: goal.id,
|
|
2974
3020
|
type: "compaction.completed",
|
|
2975
3021
|
timestamp: new Date().toISOString(),
|
|
@@ -3039,7 +3085,7 @@ function createLoopEngine(options) {
|
|
|
3039
3085
|
});
|
|
3040
3086
|
await appendEvent(directory, {
|
|
3041
3087
|
version: 1,
|
|
3042
|
-
eventID:
|
|
3088
|
+
eventID: randomUUID5(),
|
|
3043
3089
|
goalID: goal.id,
|
|
3044
3090
|
type: "compaction.started",
|
|
3045
3091
|
timestamp: new Date().toISOString(),
|
|
@@ -3097,7 +3143,7 @@ function createLoopEngine(options) {
|
|
|
3097
3143
|
return false;
|
|
3098
3144
|
await appendEvent(directory, {
|
|
3099
3145
|
version: 1,
|
|
3100
|
-
eventID:
|
|
3146
|
+
eventID: randomUUID5(),
|
|
3101
3147
|
goalID: goal.id,
|
|
3102
3148
|
type: "goal.status_changed",
|
|
3103
3149
|
from: "active",
|
|
@@ -3121,6 +3167,7 @@ function createLoopEngine(options) {
|
|
|
3121
3167
|
}
|
|
3122
3168
|
async function sweepStaleWorkspaceOwnership() {
|
|
3123
3169
|
try {
|
|
3170
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
3124
3171
|
const snapshot = await readState(directory);
|
|
3125
3172
|
if (!snapshot.workspaceCalls?.length && !snapshot.goals.some((g) => g.scopeClosing || g.scopeClearPending)) {
|
|
3126
3173
|
return;
|
|
@@ -3198,7 +3245,7 @@ function createLoopEngine(options) {
|
|
|
3198
3245
|
}
|
|
3199
3246
|
return s;
|
|
3200
3247
|
});
|
|
3201
|
-
|
|
3248
|
+
await continueGoal(goal.id);
|
|
3202
3249
|
}
|
|
3203
3250
|
}
|
|
3204
3251
|
if (!manual && (runtime.phase === "running" || runtime.phase === "idle") && goal.workerSessionID) {
|
|
@@ -3214,22 +3261,6 @@ function createLoopEngine(options) {
|
|
|
3214
3261
|
});
|
|
3215
3262
|
await logServerEvent(directory, "maintenance.stale-run-cleared", { goalID: goal.id });
|
|
3216
3263
|
}
|
|
3217
|
-
if ((runtime.activeToolCallIDs?.length ?? 0) > 0 && runtime.lastActivityAt) {
|
|
3218
|
-
const age = Date.now() - Date.parse(runtime.lastActivityAt);
|
|
3219
|
-
if (age > 30000) {
|
|
3220
|
-
await mutateState(directory, `maintenance.toolcall-ttl:${goal.id}`, async (s) => {
|
|
3221
|
-
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
3222
|
-
if (rt && (rt.activeToolCallIDs?.length ?? 0) > 0) {
|
|
3223
|
-
rt.activeToolCallIDs = [];
|
|
3224
|
-
rt.idleCandidateAt = undefined;
|
|
3225
|
-
rt.idleCandidateGeneration = undefined;
|
|
3226
|
-
rt.updatedAt = new Date().toISOString();
|
|
3227
|
-
}
|
|
3228
|
-
return s;
|
|
3229
|
-
});
|
|
3230
|
-
await logServerEvent(directory, "maintenance.toolcall-ttl-cleared", { goalID: goal.id, age });
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
3264
|
const status = await host.sessionStatus(goal.workerSessionID);
|
|
3234
3265
|
if (status === "unknown") {
|
|
3235
3266
|
let shouldNotify = false;
|
|
@@ -3283,10 +3314,14 @@ function createLoopEngine(options) {
|
|
|
3283
3314
|
const rt = s.runtimes.find((r) => r.goalID === goal.id);
|
|
3284
3315
|
if (!g || !rt || g.status !== "active")
|
|
3285
3316
|
return s;
|
|
3317
|
+
if (g.interactive || g.scopeClosing || g.workerSessionID !== goal.workerSessionID)
|
|
3318
|
+
return s;
|
|
3286
3319
|
if (rt.phase !== "running")
|
|
3287
3320
|
return s;
|
|
3288
3321
|
if (rt.runGeneration !== generation)
|
|
3289
3322
|
return s;
|
|
3323
|
+
if (s.workspaceOperation || (s.workspaceCalls ?? []).some((call) => call.goalID === goal.id) || rt.activeToolCallIDs?.length)
|
|
3324
|
+
return s;
|
|
3290
3325
|
clearedToolCalls = rt.activeToolCallIDs?.length ?? 0;
|
|
3291
3326
|
Object.assign(rt, releaseLease(rt));
|
|
3292
3327
|
rt.activeRunID = undefined;
|
|
@@ -3300,7 +3335,7 @@ function createLoopEngine(options) {
|
|
|
3300
3335
|
if (recovered) {
|
|
3301
3336
|
await appendEvent(directory, {
|
|
3302
3337
|
version: 1,
|
|
3303
|
-
eventID:
|
|
3338
|
+
eventID: randomUUID5(),
|
|
3304
3339
|
goalID: goal.id,
|
|
3305
3340
|
type: "run.recovered",
|
|
3306
3341
|
runID: stalledRunID ?? "unknown",
|
|
@@ -3334,7 +3369,7 @@ function createLoopEngine(options) {
|
|
|
3334
3369
|
const stuckSeconds = Math.floor((Date.now() - Date.parse(failedAt)) / 1000);
|
|
3335
3370
|
await appendEvent(directory, {
|
|
3336
3371
|
version: 1,
|
|
3337
|
-
eventID:
|
|
3372
|
+
eventID: randomUUID5(),
|
|
3338
3373
|
goalID: goal.id,
|
|
3339
3374
|
type: "run.stuck",
|
|
3340
3375
|
runID: runtime.activeRunID ?? "unknown",
|
|
@@ -3374,7 +3409,7 @@ function createLoopEngine(options) {
|
|
|
3374
3409
|
});
|
|
3375
3410
|
await appendEvent(directory, {
|
|
3376
3411
|
version: 1,
|
|
3377
|
-
eventID:
|
|
3412
|
+
eventID: randomUUID5(),
|
|
3378
3413
|
goalID: goal.id,
|
|
3379
3414
|
type: "run.stuck",
|
|
3380
3415
|
runID: runtime.activeRunID,
|
|
@@ -3397,14 +3432,54 @@ function createLoopEngine(options) {
|
|
|
3397
3432
|
}
|
|
3398
3433
|
|
|
3399
3434
|
// src/application/goal-service.ts
|
|
3400
|
-
import { randomUUID as
|
|
3435
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
3436
|
+
|
|
3437
|
+
// src/domain/goal.ts
|
|
3438
|
+
var MODEL_TRANSITIONS = {
|
|
3439
|
+
active: ["complete", "blocked"],
|
|
3440
|
+
paused: [],
|
|
3441
|
+
blocked: [],
|
|
3442
|
+
budget_limited: ["complete", "blocked"],
|
|
3443
|
+
usage_limited: [],
|
|
3444
|
+
complete: []
|
|
3445
|
+
};
|
|
3446
|
+
var USER_TRANSITIONS = {
|
|
3447
|
+
active: ["paused"],
|
|
3448
|
+
paused: ["active"],
|
|
3449
|
+
blocked: ["active"],
|
|
3450
|
+
budget_limited: ["active"],
|
|
3451
|
+
usage_limited: ["active"],
|
|
3452
|
+
complete: ["active"]
|
|
3453
|
+
};
|
|
3454
|
+
var SYSTEM_TRANSITIONS = {
|
|
3455
|
+
active: ["budget_limited", "usage_limited"],
|
|
3456
|
+
paused: [],
|
|
3457
|
+
blocked: [],
|
|
3458
|
+
budget_limited: [],
|
|
3459
|
+
usage_limited: [],
|
|
3460
|
+
complete: []
|
|
3461
|
+
};
|
|
3462
|
+
function canTransition(current, target, caller) {
|
|
3463
|
+
const table = caller === "model" ? MODEL_TRANSITIONS : caller === "user" ? USER_TRANSITIONS : SYSTEM_TRANSITIONS;
|
|
3464
|
+
return table[current]?.includes(target) ?? false;
|
|
3465
|
+
}
|
|
3466
|
+
function isTerminal(status) {
|
|
3467
|
+
return status === "complete";
|
|
3468
|
+
}
|
|
3469
|
+
function createGoal(input) {
|
|
3470
|
+
const now = new Date().toISOString();
|
|
3471
|
+
return { ...input, tokensUsed: 0, costUsed: 0, timeUsedSeconds: 0, createdAt: now, updatedAt: now };
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// src/application/goal-service.ts
|
|
3401
3475
|
init_state_repository();
|
|
3402
3476
|
init_command_await2();
|
|
3403
3477
|
import * as path4 from "path";
|
|
3404
3478
|
import { promises as fs4 } from "fs";
|
|
3405
3479
|
|
|
3406
3480
|
// src/server/host-adapter.ts
|
|
3407
|
-
|
|
3481
|
+
init_server_log();
|
|
3482
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
3408
3483
|
|
|
3409
3484
|
// src/v2/native-rpc.ts
|
|
3410
3485
|
var NATIVE_RPC_ID = "loopd.native";
|
|
@@ -3630,7 +3705,7 @@ function parseModelRef(value) {
|
|
|
3630
3705
|
return { providerID, modelID };
|
|
3631
3706
|
}
|
|
3632
3707
|
function newPromptMessageID() {
|
|
3633
|
-
return `msg_${
|
|
3708
|
+
return `msg_${randomUUID6()}`;
|
|
3634
3709
|
}
|
|
3635
3710
|
function isV2PromptMessageID(messageID) {
|
|
3636
3711
|
return messageID.startsWith("msg_");
|
|
@@ -3810,6 +3885,8 @@ function createRealHost(client, directory) {
|
|
|
3810
3885
|
messageID: m.info?.id || m.id,
|
|
3811
3886
|
parentMessageID: m.info?.parentID,
|
|
3812
3887
|
completedAt: completedMs ? new Date(completedMs).toISOString() : undefined,
|
|
3888
|
+
finish: typeof m.info?.finish === "string" ? m.info.finish : undefined,
|
|
3889
|
+
createdAt: typeof createdMs === "number" ? new Date(createdMs).toISOString() : undefined,
|
|
3813
3890
|
tokens: tokens && typeof tokens.input === "number" ? {
|
|
3814
3891
|
input: tokens.input || 0,
|
|
3815
3892
|
output: tokens.output || 0,
|
|
@@ -3825,6 +3902,23 @@ function createRealHost(client, directory) {
|
|
|
3825
3902
|
return [];
|
|
3826
3903
|
}
|
|
3827
3904
|
},
|
|
3905
|
+
async readToolExecutions(sessionID) {
|
|
3906
|
+
try {
|
|
3907
|
+
const result = await withTimeout(client.session.messages({ path: { id: sessionID } }), 1e4, "OpenCode session.messages (tool terminal evidence)");
|
|
3908
|
+
if (!Array.isArray(result?.data))
|
|
3909
|
+
return [];
|
|
3910
|
+
return result.data.flatMap((message) => {
|
|
3911
|
+
if (message.info?.sessionID !== undefined && message.info.sessionID !== sessionID)
|
|
3912
|
+
return [];
|
|
3913
|
+
return (message.parts ?? []).flatMap((part) => {
|
|
3914
|
+
const evidence = terminalToolExecution(part?.sessionID === undefined && message.info?.sessionID === sessionID ? { ...part, sessionID } : part);
|
|
3915
|
+
return evidence?.sessionID === sessionID ? [evidence] : [];
|
|
3916
|
+
});
|
|
3917
|
+
});
|
|
3918
|
+
} catch {
|
|
3919
|
+
return [];
|
|
3920
|
+
}
|
|
3921
|
+
},
|
|
3828
3922
|
async compactSession(sessionID, model) {
|
|
3829
3923
|
try {
|
|
3830
3924
|
const identity = model ?? (await this.readSession(sessionID))?.model;
|
|
@@ -4056,6 +4150,7 @@ function createV2Host(context, statuses, options = {}) {
|
|
|
4056
4150
|
messageID: message.id,
|
|
4057
4151
|
parentMessageID,
|
|
4058
4152
|
completedAt: completed ? new Date(completed).toISOString() : undefined,
|
|
4153
|
+
createdAt: new Date(created).toISOString(),
|
|
4059
4154
|
tokens: message.tokens ? {
|
|
4060
4155
|
input: message.tokens.input,
|
|
4061
4156
|
output: message.tokens.output,
|
|
@@ -4298,6 +4393,7 @@ function buildContinuationSteering(goal, runtime, context) {
|
|
|
4298
4393
|
}
|
|
4299
4394
|
|
|
4300
4395
|
// src/application/goal-service.ts
|
|
4396
|
+
init_server_log();
|
|
4301
4397
|
init_workspace_execution();
|
|
4302
4398
|
|
|
4303
4399
|
class GoalStartError extends Error {
|
|
@@ -4312,6 +4408,14 @@ class GoalStartError extends Error {
|
|
|
4312
4408
|
this.failedStage = info.failedStage;
|
|
4313
4409
|
}
|
|
4314
4410
|
}
|
|
4411
|
+
|
|
4412
|
+
class GoalDeliveryBlockedError extends Error {
|
|
4413
|
+
result;
|
|
4414
|
+
constructor(result) {
|
|
4415
|
+
super(result.message);
|
|
4416
|
+
this.result = result;
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4315
4419
|
function createGoalService(host) {
|
|
4316
4420
|
const workers = createWorkerManager(host);
|
|
4317
4421
|
const sessions = new Map;
|
|
@@ -4851,7 +4955,7 @@ function createGoalService(host) {
|
|
|
4851
4955
|
}
|
|
4852
4956
|
await appendEvent(directory, {
|
|
4853
4957
|
version: 1,
|
|
4854
|
-
eventID:
|
|
4958
|
+
eventID: randomUUID7(),
|
|
4855
4959
|
goalID,
|
|
4856
4960
|
type: "run.failed",
|
|
4857
4961
|
runID,
|
|
@@ -4863,7 +4967,7 @@ function createGoalService(host) {
|
|
|
4863
4967
|
if (blocked) {
|
|
4864
4968
|
await appendEvent(directory, {
|
|
4865
4969
|
version: 1,
|
|
4866
|
-
eventID:
|
|
4970
|
+
eventID: randomUUID7(),
|
|
4867
4971
|
goalID,
|
|
4868
4972
|
type: "goal.blocked",
|
|
4869
4973
|
reason: `Worker prompt delivery failed: ${detail}`,
|
|
@@ -4900,13 +5004,67 @@ function createGoalService(host) {
|
|
|
4900
5004
|
}
|
|
4901
5005
|
}
|
|
4902
5006
|
async function dispatchOrRecordFailure(directory, goalID, dispatch, run, workerSessionID, blockImmediately = false) {
|
|
5007
|
+
let blocked;
|
|
5008
|
+
await mutateState(directory, `turn.dispatch-intent:${goalID}`, async (s) => {
|
|
5009
|
+
blocked = continuationGuard(s, goalID);
|
|
5010
|
+
const g = s.goals.find((item) => item.id === goalID);
|
|
5011
|
+
const rt = s.runtimes.find((item) => item.goalID === goalID);
|
|
5012
|
+
const ownsRun = rt && g?.workerSessionID === workerSessionID && rt.activeRunID === run.activeRunID && rt.activePromptMessageID === run.activePromptMessageID && (run.runGeneration === undefined || rt.runGeneration === run.runGeneration);
|
|
5013
|
+
if (!ownsRun)
|
|
5014
|
+
blocked ??= rejected(goalID, "blocked", "Run or worker was superseded before dispatch; no prompt was sent.");
|
|
5015
|
+
if (blocked) {
|
|
5016
|
+
if (ownsRun) {
|
|
5017
|
+
Object.assign(rt, releaseLease(rt));
|
|
5018
|
+
rt.activeRunID = undefined;
|
|
5019
|
+
}
|
|
5020
|
+
} else if (rt) {
|
|
5021
|
+
rt.activePromptDispatchAt = new Date().toISOString();
|
|
5022
|
+
}
|
|
5023
|
+
return s;
|
|
5024
|
+
});
|
|
5025
|
+
if (blocked) {
|
|
5026
|
+
if (blocked.outcome === "not_found")
|
|
5027
|
+
sessions.delete(goalID);
|
|
5028
|
+
throw new GoalDeliveryBlockedError(blocked);
|
|
5029
|
+
}
|
|
5030
|
+
let observed = false;
|
|
4903
5031
|
try {
|
|
4904
5032
|
await dispatch();
|
|
5033
|
+
observed = await wasPromptAccepted(directory, workerSessionID, run.activePromptMessageID);
|
|
4905
5034
|
} catch (error) {
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
5035
|
+
const current = await readState(directory);
|
|
5036
|
+
if (!current.goals.some((goal) => goal.id === goalID)) {
|
|
5037
|
+
throw new GoalDeliveryBlockedError(rejected(goalID, "not_found", "Goal cleared while host delivery was pending. Its late result was ignored; list current goals instead of retrying this ID."));
|
|
5038
|
+
}
|
|
5039
|
+
observed = await wasPromptAccepted(directory, workerSessionID, run.activePromptMessageID);
|
|
5040
|
+
if (!observed) {
|
|
5041
|
+
await recordPromptFailure(directory, goalID, error, blockImmediately, run.activeRunID);
|
|
5042
|
+
throw error;
|
|
5043
|
+
}
|
|
5044
|
+
}
|
|
5045
|
+
let removed = false;
|
|
5046
|
+
let acceptedRun = false;
|
|
5047
|
+
const accepted = await mutateState(directory, `turn.dispatch-accepted:${goalID}`, async (s) => {
|
|
5048
|
+
const g = s.goals.find((item) => item.id === goalID);
|
|
5049
|
+
removed = !g;
|
|
5050
|
+
const rt = s.runtimes.find((item) => item.goalID === goalID);
|
|
5051
|
+
if (rt && g?.status === "active" && g.workerSessionID === workerSessionID && rt.activeRunID === run.activeRunID && rt.activePromptMessageID === run.activePromptMessageID && (run.runGeneration === undefined || rt.runGeneration === run.runGeneration)) {
|
|
5052
|
+
acceptedRun = true;
|
|
5053
|
+
rt.activePromptAcceptedAt = new Date().toISOString();
|
|
5054
|
+
if (run.forceFinish)
|
|
5055
|
+
rt.forceFinishRequested = true;
|
|
5056
|
+
if (observed)
|
|
5057
|
+
rt.activePromptObservedAt ??= rt.activePromptAcceptedAt;
|
|
5058
|
+
}
|
|
5059
|
+
return s;
|
|
5060
|
+
});
|
|
5061
|
+
if (removed)
|
|
5062
|
+
throw new GoalDeliveryBlockedError(rejected(goalID, "not_found", "Goal cleared while host delivery was pending. Its late result was ignored; list current goals instead of retrying this ID."));
|
|
5063
|
+
if (!acceptedRun)
|
|
5064
|
+
throw new GoalDeliveryBlockedError(rejected(goalID, "blocked", "The turn or worker changed while host delivery was pending. Its late result was ignored; inspect the current run before retrying."));
|
|
5065
|
+
const rt = accepted.runtimes.find((item) => item.goalID === goalID);
|
|
5066
|
+
if (rt && rt.activeRunID === run.activeRunID && rt.activePromptAcceptedAt) {
|
|
5067
|
+
await acknowledgeGoalInbox(directory, goalID, rt.activeInboxLines ?? []);
|
|
4910
5068
|
}
|
|
4911
5069
|
}
|
|
4912
5070
|
async function ensureWorkerSession(directory, goal) {
|
|
@@ -4923,22 +5081,36 @@ function createGoalService(host) {
|
|
|
4923
5081
|
return session;
|
|
4924
5082
|
}
|
|
4925
5083
|
session = await workers.createWorker(goal);
|
|
4926
|
-
|
|
5084
|
+
let linked = false;
|
|
5085
|
+
let failure;
|
|
4927
5086
|
await mutateState(directory, `goal.set-worker:${goal.id}`, async (s) => {
|
|
4928
5087
|
const persisted = s.goals.find((item) => item.id === goal.id);
|
|
4929
|
-
if (persisted)
|
|
5088
|
+
if (!persisted)
|
|
5089
|
+
failure = rejected(goal.id, "not_found", "Goal cleared during worker creation; the new worker was retired. List your goals instead of retrying this ID.");
|
|
5090
|
+
else if (persisted.ownerSessionID !== goal.ownerSessionID || persisted.workerSessionID !== goal.workerSessionID || persisted.status !== "active") {
|
|
5091
|
+
failure = rejected(goal.id, "blocked", "Goal changed during worker creation; the unused worker was retired. Inspect current state before retrying.");
|
|
5092
|
+
} else {
|
|
4930
5093
|
persisted.workerSessionID = session.workerSessionID;
|
|
5094
|
+
linked = true;
|
|
4931
5095
|
if (session.topology) {
|
|
4932
5096
|
persisted.workerTopology = session.topology;
|
|
4933
5097
|
persisted.nativeParentID = session.nativeParentID;
|
|
4934
5098
|
}
|
|
4935
5099
|
}
|
|
5100
|
+
if (!linked)
|
|
5101
|
+
retireWorker(s, { ...goal, workerSessionID: session.workerSessionID });
|
|
4936
5102
|
return s;
|
|
4937
5103
|
});
|
|
5104
|
+
if (!linked) {
|
|
5105
|
+
sessions.delete(goal.id);
|
|
5106
|
+
await workers.abortWorker(session.workerSessionID).catch(() => {});
|
|
5107
|
+
throw new GoalDeliveryBlockedError(failure);
|
|
5108
|
+
}
|
|
5109
|
+
sessions.set(goal.id, session);
|
|
4938
5110
|
return session;
|
|
4939
5111
|
}
|
|
4940
5112
|
async function start(directory, input) {
|
|
4941
|
-
const id =
|
|
5113
|
+
const id = randomUUID7();
|
|
4942
5114
|
return withGoalOperation(id, () => startUnlocked(directory, input, id));
|
|
4943
5115
|
}
|
|
4944
5116
|
async function startUnlocked(directory, input, id) {
|
|
@@ -5045,7 +5217,7 @@ function createGoalService(host) {
|
|
|
5045
5217
|
});
|
|
5046
5218
|
await appendEvent(directory, {
|
|
5047
5219
|
version: 1,
|
|
5048
|
-
eventID:
|
|
5220
|
+
eventID: randomUUID7(),
|
|
5049
5221
|
goalID: id,
|
|
5050
5222
|
type: "goal.blocked",
|
|
5051
5223
|
reason: detail,
|
|
@@ -5072,7 +5244,7 @@ function createGoalService(host) {
|
|
|
5072
5244
|
const rt = state.runtimes.find((item) => item.goalID === id);
|
|
5073
5245
|
if (rt) {
|
|
5074
5246
|
Object.assign(rt, acquireLease(rt, g.config.timeoutMs || 300000));
|
|
5075
|
-
rt.activeRunID =
|
|
5247
|
+
rt.activeRunID = randomUUID7();
|
|
5076
5248
|
rt.activePromptMessageID = newPromptMessageID();
|
|
5077
5249
|
rt.runCount = 1;
|
|
5078
5250
|
rt.budgetTurnCount = 1;
|
|
@@ -5083,7 +5255,7 @@ function createGoalService(host) {
|
|
|
5083
5255
|
runtime = state2.runtimes.find((r) => r.goalID === id);
|
|
5084
5256
|
await appendEvent(directory, {
|
|
5085
5257
|
version: 1,
|
|
5086
|
-
eventID:
|
|
5258
|
+
eventID: randomUUID7(),
|
|
5087
5259
|
goalID: id,
|
|
5088
5260
|
type: "goal.created",
|
|
5089
5261
|
name: input.name,
|
|
@@ -5095,7 +5267,7 @@ function createGoalService(host) {
|
|
|
5095
5267
|
if (runtime) {
|
|
5096
5268
|
await appendEvent(directory, {
|
|
5097
5269
|
version: 1,
|
|
5098
|
-
eventID:
|
|
5270
|
+
eventID: randomUUID7(),
|
|
5099
5271
|
goalID: id,
|
|
5100
5272
|
type: "run.started",
|
|
5101
5273
|
runID: runtime.activeRunID,
|
|
@@ -5172,23 +5344,48 @@ function createGoalService(host) {
|
|
|
5172
5344
|
}
|
|
5173
5345
|
return accountTailUsage(directory, goalID, runtime, tail);
|
|
5174
5346
|
}
|
|
5347
|
+
function rejected(goalID, outcome, message) {
|
|
5348
|
+
if (outcome === "not_found")
|
|
5349
|
+
sessions.delete(goalID);
|
|
5350
|
+
return { ok: false, outcome, goalID, message };
|
|
5351
|
+
}
|
|
5352
|
+
function continuationGuard(state, goalID) {
|
|
5353
|
+
const goal = state.goals.find((g) => g.id === goalID);
|
|
5354
|
+
if (!goal)
|
|
5355
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5356
|
+
if (goal.status !== "active")
|
|
5357
|
+
return rejected(goalID, "invalid_state", `Goal is ${goal.status}; resume or retry it before requesting a turn.`);
|
|
5358
|
+
if (goal.config.write_scope !== undefined && !host.scopedExecution)
|
|
5359
|
+
return rejected(goalID, "blocked", "Enforced scopes are unsupported on this host.");
|
|
5360
|
+
if (goal.scopeClosing)
|
|
5361
|
+
return rejected(goalID, "blocked", "Goal is closing; wait for in-flight writes to finish before resuming.");
|
|
5362
|
+
if ((state.workspaceCalls ?? []).some((call) => call.goalID === goalID && !isSyncStaleWorkspaceCall(state, call, Date.now()))) {
|
|
5363
|
+
return rejected(goalID, "blocked", "Goal has in-flight write reservations. Nothing was sent; retry after writes finish.");
|
|
5364
|
+
}
|
|
5365
|
+
if (state.workspaceOperation)
|
|
5366
|
+
return rejected(goalID, "blocked", `Workspace operation ${state.workspaceOperation.id} is in progress. Nothing was sent; retry after it finishes.`);
|
|
5367
|
+
const runtime = state.runtimes.find((r) => r.goalID === goalID);
|
|
5368
|
+
if (!runtime)
|
|
5369
|
+
return rejected(goalID, "blocked", "Goal runtime is missing; no turn was sent.");
|
|
5370
|
+
if (runtime.activeToolCallIDs?.length)
|
|
5371
|
+
return rejected(goalID, "blocked", "Worker has outstanding tool calls. Nothing was sent; wait for terminal tool evidence.");
|
|
5372
|
+
}
|
|
5175
5373
|
async function continueTurnUnlocked(directory, goalID, opts) {
|
|
5374
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5176
5375
|
const preState = await readState(directory);
|
|
5177
5376
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
const now = Date.now();
|
|
5184
|
-
if (goal.scopeClosing || (preState.workspaceCalls ?? []).some((call) => call.goalID === goalID && !isSyncStaleWorkspaceCall(preState, call, now)) || preState.workspaceOperation)
|
|
5185
|
-
return;
|
|
5186
|
-
}
|
|
5377
|
+
const guard = continuationGuard(preState, goalID);
|
|
5378
|
+
if (guard)
|
|
5379
|
+
return guard;
|
|
5380
|
+
if (!goal)
|
|
5381
|
+
return rejected(goalID, "not_found", "Goal not found.");
|
|
5187
5382
|
const runtime = preState.runtimes.find((r) => r.goalID === goalID);
|
|
5188
5383
|
if (!runtime)
|
|
5189
|
-
return;
|
|
5384
|
+
return rejected(goalID, "blocked", "Goal runtime is missing; no turn was sent.");
|
|
5385
|
+
if (runtime.activePromptAcceptedAt)
|
|
5386
|
+
await acknowledgeGoalInbox(directory, goalID, runtime.activeInboxLines ?? []);
|
|
5190
5387
|
if (runtime.phase === "running" && leaseIsValid(runtime))
|
|
5191
|
-
return;
|
|
5388
|
+
return rejected(goalID, "blocked", "An existing turn holds the run lease; no additional prompt was sent.");
|
|
5192
5389
|
let session = sessions.get(goalID);
|
|
5193
5390
|
if (session && session.workerSessionID !== goal.workerSessionID) {
|
|
5194
5391
|
sessions.delete(goalID);
|
|
@@ -5205,18 +5402,22 @@ function createGoalService(host) {
|
|
|
5205
5402
|
if (!session && !goal.workerSessionID)
|
|
5206
5403
|
session = await ensureWorkerSession(directory, goal);
|
|
5207
5404
|
if (!session)
|
|
5208
|
-
return;
|
|
5405
|
+
return rejected(goalID, "blocked", "Worker session is missing; resume or retry the goal.");
|
|
5209
5406
|
if (!opts?.force && !await workers.isIdle(session.workerSessionID))
|
|
5210
|
-
return;
|
|
5407
|
+
return rejected(goalID, "blocked", "Worker is busy or its status is unknown; no prompt was sent. Retry after confirmed idle.");
|
|
5211
5408
|
if (goal.pendingIdentity || goal.modelSwitch?.pending || goal.agentSwitch?.pending) {
|
|
5212
5409
|
if (await host.sessionStatus(session.workerSessionID) !== "idle")
|
|
5213
|
-
return;
|
|
5410
|
+
return rejected(goalID, "blocked", "Pending identity requires an idle worker; no prompt was sent. Wait for idle and retry.");
|
|
5214
5411
|
await applyPendingIdentity(directory, goal);
|
|
5215
5412
|
await applyPendingModel(directory, goal);
|
|
5216
5413
|
await applyPendingAgent(directory, goal);
|
|
5217
5414
|
}
|
|
5218
5415
|
let acquired = false;
|
|
5416
|
+
let acquisitionFailure;
|
|
5219
5417
|
const state = await mutateState(directory, `turn.acquire:${goalID}`, async (s) => {
|
|
5418
|
+
acquisitionFailure = continuationGuard(s, goalID);
|
|
5419
|
+
if (acquisitionFailure)
|
|
5420
|
+
return s;
|
|
5220
5421
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5221
5422
|
if (!g || g.status !== "active")
|
|
5222
5423
|
return s;
|
|
@@ -5227,7 +5428,7 @@ function createGoalService(host) {
|
|
|
5227
5428
|
return s;
|
|
5228
5429
|
const timeoutMs = g.config.timeoutMs || 300000;
|
|
5229
5430
|
Object.assign(rt, acquireLease(rt, timeoutMs));
|
|
5230
|
-
rt.activeRunID =
|
|
5431
|
+
rt.activeRunID = randomUUID7();
|
|
5231
5432
|
rt.activePromptMessageID = newPromptMessageID();
|
|
5232
5433
|
rt.runCount += 1;
|
|
5233
5434
|
if (rt.freeRetryPending) {
|
|
@@ -5242,70 +5443,97 @@ function createGoalService(host) {
|
|
|
5242
5443
|
const freshGoal = state.goals.find((g) => g.id === goalID);
|
|
5243
5444
|
const freshRuntime = state.runtimes.find((r) => r.goalID === goalID);
|
|
5244
5445
|
if (!acquired || !freshGoal || freshGoal.status !== "active" || !freshRuntime?.activeRunID)
|
|
5245
|
-
return;
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5446
|
+
return acquisitionFailure ?? rejected(goalID, "blocked", "Run lease acquisition lost to another turn; no prompt was sent.");
|
|
5447
|
+
const sent = {
|
|
5448
|
+
ok: true,
|
|
5449
|
+
outcome: "sent",
|
|
5249
5450
|
goalID,
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
if (opts?.bare) {
|
|
5257
|
-
const bareWords = await drainGoalInbox(directory, goalID);
|
|
5258
|
-
const bareText = bareWords.join(`
|
|
5259
|
-
`).trim();
|
|
5260
|
-
if (bareText) {
|
|
5261
|
-
await dispatchOrRecordFailure(directory, goalID, () => workers.sendBare(session, freshGoal, freshRuntime, bareText), freshRuntime, session.workerSessionID);
|
|
5262
|
-
return;
|
|
5263
|
-
}
|
|
5264
|
-
}
|
|
5265
|
-
const inboxMessages = await drainGoalInbox(directory, goalID);
|
|
5266
|
-
const allEvents = await readEvents(directory, 200);
|
|
5267
|
-
const progressHistory = allEvents.filter((e) => e.goalID === goalID && e.type === "goal.progress").map((e) => ({
|
|
5268
|
-
summary: String(e.summary || ""),
|
|
5269
|
-
next: e.next ? String(e.next) : undefined,
|
|
5270
|
-
at: String(e.timestamp || "")
|
|
5271
|
-
}));
|
|
5272
|
-
let transcriptTail;
|
|
5273
|
-
try {
|
|
5274
|
-
transcriptTail = await host.readMessages(freshGoal.workerSessionID, 5);
|
|
5275
|
-
} catch {
|
|
5276
|
-
transcriptTail = [];
|
|
5277
|
-
}
|
|
5278
|
-
await accountTailUsage(directory, goalID, freshRuntime, transcriptTail ?? []);
|
|
5279
|
-
let verification;
|
|
5451
|
+
workerSessionID: session.workerSessionID,
|
|
5452
|
+
runGeneration: freshRuntime.runGeneration,
|
|
5453
|
+
promptMessageID: freshRuntime.activePromptMessageID,
|
|
5454
|
+
message: `Prompt submitted for "${freshGoal.name}"; worker activity is not yet confirmed.`
|
|
5455
|
+
};
|
|
5456
|
+
let dispatchAttempted = false;
|
|
5280
5457
|
try {
|
|
5281
|
-
const
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5458
|
+
const inbox = await snapshotGoalInbox(directory, goalID);
|
|
5459
|
+
const inboxMessages = inbox.messages;
|
|
5460
|
+
await mutateState(directory, `turn.inbox-snapshot:${goalID}`, async (s) => {
|
|
5461
|
+
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5462
|
+
if (rt && rt.activeRunID === freshRuntime.activeRunID)
|
|
5463
|
+
rt.activeInboxLines = inbox.lines;
|
|
5464
|
+
return s;
|
|
5465
|
+
});
|
|
5466
|
+
await appendEvent(directory, {
|
|
5467
|
+
version: 1,
|
|
5468
|
+
eventID: randomUUID7(),
|
|
5469
|
+
goalID,
|
|
5470
|
+
type: "run.started",
|
|
5471
|
+
runID: freshRuntime.activeRunID,
|
|
5472
|
+
turnCount: freshRuntime.runCount,
|
|
5473
|
+
timestamp: new Date().toISOString(),
|
|
5474
|
+
revision: state.revision
|
|
5475
|
+
});
|
|
5476
|
+
if (opts?.bare) {
|
|
5477
|
+
const bareText = inboxMessages.join(`
|
|
5478
|
+
`).trim();
|
|
5479
|
+
if (bareText) {
|
|
5480
|
+
dispatchAttempted = true;
|
|
5481
|
+
await dispatchOrRecordFailure(directory, goalID, () => workers.sendBare(session, freshGoal, freshRuntime, bareText), freshRuntime, session.workerSessionID);
|
|
5482
|
+
return sent;
|
|
5288
5483
|
}
|
|
5289
5484
|
}
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5485
|
+
const allEvents = await readEvents(directory, 200);
|
|
5486
|
+
const progressHistory = allEvents.filter((e) => e.goalID === goalID && e.type === "goal.progress").map((e) => ({
|
|
5487
|
+
summary: String(e.summary || ""),
|
|
5488
|
+
next: e.next ? String(e.next) : undefined,
|
|
5489
|
+
at: String(e.timestamp || "")
|
|
5490
|
+
}));
|
|
5491
|
+
let transcriptTail;
|
|
5492
|
+
try {
|
|
5493
|
+
transcriptTail = await host.readMessages(freshGoal.workerSessionID, 5);
|
|
5494
|
+
} catch {
|
|
5495
|
+
transcriptTail = [];
|
|
5299
5496
|
}
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5497
|
+
await accountTailUsage(directory, goalID, freshRuntime, transcriptTail ?? []);
|
|
5498
|
+
let verification;
|
|
5499
|
+
try {
|
|
5500
|
+
const artifactDir = freshGoal.config.artifactDir;
|
|
5501
|
+
if (artifactDir) {
|
|
5502
|
+
try {
|
|
5503
|
+
const files = await fs4.readdir(artifactDir);
|
|
5504
|
+
verification = { artifactSummary: files.length ? `${files.length} file(s): ${files.slice(0, 8).join(", ")}` : "no artifacts yet" };
|
|
5505
|
+
} catch {
|
|
5506
|
+
verification = { artifactSummary: "no artifacts yet" };
|
|
5507
|
+
}
|
|
5508
|
+
}
|
|
5509
|
+
if (freshGoal.config.checks?.length) {
|
|
5510
|
+
const c = `checks configured: ${freshGoal.config.checks.length} \u2014 run them before claiming completion`;
|
|
5511
|
+
verification = { ...verification || {}, failedChecks: [c], checksPassed: undefined };
|
|
5512
|
+
}
|
|
5513
|
+
if (freshRuntime.evaluatorRejectionCount && freshRuntime.evaluatorRejectionCount > 0) {
|
|
5514
|
+
verification = { ...verification || {}, evaluatorRejectionCount: freshRuntime.evaluatorRejectionCount };
|
|
5515
|
+
}
|
|
5516
|
+
if (freshRuntime.lastRejectionDetails) {
|
|
5517
|
+
verification = { ...verification || {}, lastRejectionDetails: freshRuntime.lastRejectionDetails };
|
|
5518
|
+
}
|
|
5519
|
+
} catch {}
|
|
5520
|
+
const context = {
|
|
5521
|
+
inboxMessages: inboxMessages.length > 0 ? inboxMessages : undefined,
|
|
5522
|
+
progressHistory: progressHistory.length > 0 ? progressHistory : undefined,
|
|
5523
|
+
transcriptTail: transcriptTail && transcriptTail.length > 0 ? transcriptTail : undefined,
|
|
5524
|
+
forceFinish: opts?.forceFinish || undefined,
|
|
5525
|
+
verification
|
|
5526
|
+
};
|
|
5527
|
+
dispatchAttempted = true;
|
|
5528
|
+
await dispatchOrRecordFailure(directory, goalID, () => workers.continueWorker(session, freshGoal, freshRuntime, context), { ...freshRuntime, forceFinish: opts?.forceFinish }, session.workerSessionID);
|
|
5529
|
+
return sent;
|
|
5530
|
+
} catch (error) {
|
|
5531
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5532
|
+
return error.result;
|
|
5533
|
+
if (!dispatchAttempted)
|
|
5534
|
+
await recordPromptFailure(directory, goalID, error, false, freshRuntime.activeRunID);
|
|
5535
|
+
throw error;
|
|
5536
|
+
}
|
|
5309
5537
|
}
|
|
5310
5538
|
async function setInteractiveUnlocked(directory, goalID, interactive) {
|
|
5311
5539
|
const state = await mutateState(directory, `goal.interactive:${goalID}`, async (s) => {
|
|
@@ -5324,7 +5552,7 @@ function createGoalService(host) {
|
|
|
5324
5552
|
return { ok: false, message: "Goal not found." };
|
|
5325
5553
|
await appendEvent(directory, {
|
|
5326
5554
|
version: 1,
|
|
5327
|
-
eventID:
|
|
5555
|
+
eventID: randomUUID7(),
|
|
5328
5556
|
goalID,
|
|
5329
5557
|
type: "goal.interactive_changed",
|
|
5330
5558
|
interactive,
|
|
@@ -5385,7 +5613,7 @@ function createGoalService(host) {
|
|
|
5385
5613
|
await cancelAwaitsForGoal(directory, goalID, "goal paused").catch(() => {});
|
|
5386
5614
|
await appendEvent(directory, {
|
|
5387
5615
|
version: 1,
|
|
5388
|
-
eventID:
|
|
5616
|
+
eventID: randomUUID7(),
|
|
5389
5617
|
goalID,
|
|
5390
5618
|
type: "goal.status_changed",
|
|
5391
5619
|
from: "active",
|
|
@@ -5421,6 +5649,7 @@ function createGoalService(host) {
|
|
|
5421
5649
|
}
|
|
5422
5650
|
}
|
|
5423
5651
|
async function resumeUnlocked(directory, goalID) {
|
|
5652
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5424
5653
|
const preState = await readState(directory);
|
|
5425
5654
|
const preGoal = preState.goals.find((g) => g.id === goalID);
|
|
5426
5655
|
const workerLiveness = await probeWorkerLiveness(preGoal?.workerSessionID);
|
|
@@ -5435,10 +5664,10 @@ function createGoalService(host) {
|
|
|
5435
5664
|
sweepStaleWorkspaceState(state, liveness);
|
|
5436
5665
|
const rt = state.runtimes.find((r) => r.goalID === goalID);
|
|
5437
5666
|
const liveCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness));
|
|
5438
|
-
const blocking = liveCalls
|
|
5667
|
+
const blocking = liveCalls;
|
|
5439
5668
|
if (blocking.length) {
|
|
5440
5669
|
if (workerLiveness === "unknown") {
|
|
5441
|
-
throw new Error(`Worker liveness unknown for session "${goal.workerSessionID}" (probe timed out or unreachable); keeping ${blocking.length} write reservation(s) \u2014 nothing reaped, nothing rotated. ` + `The worker may still be running
|
|
5670
|
+
throw new Error(`Worker liveness unknown for session "${goal.workerSessionID}" (probe timed out or unreachable); keeping ${blocking.length} write reservation(s) \u2014 nothing reaped, nothing rotated. ` + `The worker may still be running; wait for actual tool completion/quiescence. Neither age nor a missing session releases execution fences.`);
|
|
5442
5671
|
}
|
|
5443
5672
|
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
5444
5673
|
}
|
|
@@ -5453,7 +5682,6 @@ function createGoalService(host) {
|
|
|
5453
5682
|
}
|
|
5454
5683
|
goal.scopeClosing = false;
|
|
5455
5684
|
goal.scopeClearPending = false;
|
|
5456
|
-
state.workspaceCalls = (state.workspaceCalls ?? []).filter((call) => call.goalID !== goalID);
|
|
5457
5685
|
assertWorkspaceWriteAvailable(state, goal, goal.ownerSessionID);
|
|
5458
5686
|
goal.status = "active";
|
|
5459
5687
|
goal.updatedAt = new Date().toISOString();
|
|
@@ -5461,11 +5689,15 @@ function createGoalService(host) {
|
|
|
5461
5689
|
return state;
|
|
5462
5690
|
});
|
|
5463
5691
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5464
|
-
if (!goal
|
|
5465
|
-
return;
|
|
5692
|
+
if (!goal)
|
|
5693
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5694
|
+
if (!resumed)
|
|
5695
|
+
return rejected(goalID, "invalid_state", `Cannot resume a ${goal.status} goal; no turn was sent.`);
|
|
5466
5696
|
try {
|
|
5467
5697
|
await ensureWorkerSession(directory, goal);
|
|
5468
5698
|
} catch (error) {
|
|
5699
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5700
|
+
return error.result;
|
|
5469
5701
|
await mutateState(directory, `goal.resume-rollback:${goalID}`, async (s) => {
|
|
5470
5702
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5471
5703
|
if (g?.status === "active")
|
|
@@ -5476,7 +5708,7 @@ function createGoalService(host) {
|
|
|
5476
5708
|
}
|
|
5477
5709
|
await appendEvent(directory, {
|
|
5478
5710
|
version: 1,
|
|
5479
|
-
eventID:
|
|
5711
|
+
eventID: randomUUID7(),
|
|
5480
5712
|
goalID,
|
|
5481
5713
|
type: "goal.status_changed",
|
|
5482
5714
|
from: "paused",
|
|
@@ -5484,9 +5716,11 @@ function createGoalService(host) {
|
|
|
5484
5716
|
timestamp: new Date().toISOString(),
|
|
5485
5717
|
revision: state.revision
|
|
5486
5718
|
});
|
|
5487
|
-
await continueTurnUnlocked(directory, goalID);
|
|
5719
|
+
const result = await continueTurnUnlocked(directory, goalID);
|
|
5720
|
+
return result.ok ? { ...result, message: `Goal "${goal.name}" resumed; ${result.message}` } : result;
|
|
5488
5721
|
}
|
|
5489
5722
|
async function retryUnlocked(directory, goalID) {
|
|
5723
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5490
5724
|
const preState = await readState(directory);
|
|
5491
5725
|
const workerLiveness = await probeWorkerLiveness(preState.goals.find((g) => g.id === goalID)?.workerSessionID);
|
|
5492
5726
|
let retried = false;
|
|
@@ -5496,11 +5730,10 @@ function createGoalService(host) {
|
|
|
5496
5730
|
return state;
|
|
5497
5731
|
const liveness = { isWorkerLive: (_gid, _wsid) => _gid === goalID ? workerLiveness !== "dead" : false, now: Date.now() };
|
|
5498
5732
|
sweepStaleWorkspaceState(state, liveness);
|
|
5499
|
-
const
|
|
5500
|
-
const blocking = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness) && (call.generation === undefined || !rt0 || call.generation >= rt0.runGeneration));
|
|
5733
|
+
const blocking = (state.workspaceCalls ?? []).filter((call) => call.goalID === goalID && !isStaleWorkspaceCall(state, call, liveness));
|
|
5501
5734
|
if (blocking.length) {
|
|
5502
5735
|
if (workerLiveness === "unknown") {
|
|
5503
|
-
throw new Error(`Worker liveness unknown for session "${goal.workerSessionID}" (probe timed out or unreachable); keeping ${blocking.length} write reservation(s) \u2014 nothing reaped, nothing rotated. ` + `The worker may still be running
|
|
5736
|
+
throw new Error(`Worker liveness unknown for session "${goal.workerSessionID}" (probe timed out or unreachable); keeping ${blocking.length} write reservation(s) \u2014 nothing reaped, nothing rotated. ` + `The worker may still be running; wait for actual tool completion/quiescence. Neither age nor a missing session releases execution fences.`);
|
|
5504
5737
|
}
|
|
5505
5738
|
throw new Error("Goal still has closing/in-flight writes; ownership cannot be reacquired before quiescence.");
|
|
5506
5739
|
}
|
|
@@ -5538,11 +5771,15 @@ function createGoalService(host) {
|
|
|
5538
5771
|
return state;
|
|
5539
5772
|
});
|
|
5540
5773
|
const goal = state.goals.find((g) => g.id === goalID);
|
|
5541
|
-
if (!goal
|
|
5542
|
-
return;
|
|
5774
|
+
if (!goal)
|
|
5775
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5776
|
+
if (!retried)
|
|
5777
|
+
return rejected(goalID, "invalid_state", `Cannot retry a ${goal.status} goal; no turn was sent.`);
|
|
5543
5778
|
try {
|
|
5544
5779
|
await ensureWorkerSession(directory, goal);
|
|
5545
5780
|
} catch (error) {
|
|
5781
|
+
if (error instanceof GoalDeliveryBlockedError)
|
|
5782
|
+
return error.result;
|
|
5546
5783
|
await mutateState(directory, `goal.retry-rollback:${goalID}`, async (s) => {
|
|
5547
5784
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5548
5785
|
if (g?.status === "active")
|
|
@@ -5553,7 +5790,7 @@ function createGoalService(host) {
|
|
|
5553
5790
|
}
|
|
5554
5791
|
await appendEvent(directory, {
|
|
5555
5792
|
version: 1,
|
|
5556
|
-
eventID:
|
|
5793
|
+
eventID: randomUUID7(),
|
|
5557
5794
|
goalID,
|
|
5558
5795
|
type: "goal.status_changed",
|
|
5559
5796
|
from: "blocked",
|
|
@@ -5561,7 +5798,7 @@ function createGoalService(host) {
|
|
|
5561
5798
|
timestamp: new Date().toISOString(),
|
|
5562
5799
|
revision: state.revision
|
|
5563
5800
|
});
|
|
5564
|
-
|
|
5801
|
+
return continueTurnUnlocked(directory, goalID);
|
|
5565
5802
|
}
|
|
5566
5803
|
async function clearUnlocked(directory, goalID) {
|
|
5567
5804
|
const pre = await readState(directory);
|
|
@@ -5570,12 +5807,10 @@ function createGoalService(host) {
|
|
|
5570
5807
|
const g = s.goals.find((item) => item.id === goalID);
|
|
5571
5808
|
if (g)
|
|
5572
5809
|
retireWorker(s, g);
|
|
5573
|
-
s.workspaceCalls = (s.workspaceCalls ?? []).filter((call) => call.goalID !== goalID);
|
|
5574
|
-
if (s.workspaceOperation?.goalID === goalID)
|
|
5575
|
-
s.workspaceOperation = undefined;
|
|
5576
5810
|
s.goals = s.goals.filter((g) => g.id !== goalID);
|
|
5577
5811
|
s.runtimes = s.runtimes.filter((r) => r.goalID !== goalID);
|
|
5578
5812
|
s.commandAwaits = (s.commandAwaits ?? []).filter((a) => a.goalID !== goalID);
|
|
5813
|
+
await deleteGoalInbox(directory, goalID);
|
|
5579
5814
|
return s;
|
|
5580
5815
|
});
|
|
5581
5816
|
sessions.delete(goalID);
|
|
@@ -5587,7 +5822,7 @@ function createGoalService(host) {
|
|
|
5587
5822
|
await cancelAwaitsForGoal(directory, goalID, "goal cleared").catch(() => {});
|
|
5588
5823
|
await appendEvent(directory, {
|
|
5589
5824
|
version: 1,
|
|
5590
|
-
eventID:
|
|
5825
|
+
eventID: randomUUID7(),
|
|
5591
5826
|
goalID,
|
|
5592
5827
|
type: "goal.cleared",
|
|
5593
5828
|
timestamp: new Date().toISOString(),
|
|
@@ -5602,6 +5837,7 @@ function createGoalService(host) {
|
|
|
5602
5837
|
return new Map(sessions);
|
|
5603
5838
|
}
|
|
5604
5839
|
async function reconcile(directory) {
|
|
5840
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5605
5841
|
try {
|
|
5606
5842
|
let summary = { reapedCalls: [], resetClosing: [], finalizedClears: [], forceCleared: [] };
|
|
5607
5843
|
const pre = await readState(directory);
|
|
@@ -5710,22 +5946,39 @@ function createGoalService(host) {
|
|
|
5710
5946
|
}
|
|
5711
5947
|
}
|
|
5712
5948
|
async function nudgeUnlocked(directory, goalID) {
|
|
5949
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5713
5950
|
const preState = await readState(directory);
|
|
5714
5951
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5715
5952
|
if (!goal)
|
|
5716
|
-
return
|
|
5953
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
5717
5954
|
if (goal.status !== "active") {
|
|
5718
|
-
return
|
|
5955
|
+
return rejected(goalID, "invalid_state", `Goal is ${goal.status}; resume or retry it before nudging.`);
|
|
5719
5956
|
}
|
|
5720
5957
|
const quotaHold = quotaHoldMessage(goal);
|
|
5721
5958
|
if (quotaHold)
|
|
5722
|
-
return
|
|
5959
|
+
return rejected(goalID, "blocked", quotaHold);
|
|
5960
|
+
const guard = continuationGuard(preState, goalID);
|
|
5961
|
+
if (guard)
|
|
5962
|
+
return guard;
|
|
5963
|
+
if (goal.workerSessionID) {
|
|
5964
|
+
const status = await host.sessionStatus(goal.workerSessionID);
|
|
5965
|
+
if (status === "busy")
|
|
5966
|
+
return rejected(goalID, "blocked", "Worker is busy; no prompt was sent and its active turn was left intact. Retry after idle.");
|
|
5967
|
+
if (status !== "idle" && (goal.pendingIdentity || goal.modelSwitch?.pending || goal.agentSwitch?.pending)) {
|
|
5968
|
+
return rejected(goalID, "blocked", "Pending identity requires confirmed idle; worker status is unknown. No prompt was sent.");
|
|
5969
|
+
}
|
|
5970
|
+
if (status !== "idle")
|
|
5971
|
+
return rejected(goalID, "blocked", "Worker status is unknown or retrying; an outstanding prompt may already be accepted. Nothing was sent or reset. Retry after confirmed idle.");
|
|
5972
|
+
}
|
|
5723
5973
|
if (goal.config.write_scope !== undefined) {
|
|
5724
5974
|
await pauseUnlocked(directory, goalID);
|
|
5725
|
-
|
|
5726
|
-
return { ok: true, message: `Re-prompted protected worker for "${goal.name}" in a freshly fenced session.` };
|
|
5975
|
+
return resumeUnlocked(directory, goalID);
|
|
5727
5976
|
}
|
|
5977
|
+
let resetFailure;
|
|
5728
5978
|
const cleared = await mutateState(directory, `goal.nudge:${goalID}`, async (s) => {
|
|
5979
|
+
resetFailure = continuationGuard(s, goalID);
|
|
5980
|
+
if (resetFailure)
|
|
5981
|
+
return s;
|
|
5729
5982
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5730
5983
|
if (!rt)
|
|
5731
5984
|
return s;
|
|
@@ -5737,30 +5990,46 @@ function createGoalService(host) {
|
|
|
5737
5990
|
rt.updatedAt = new Date().toISOString();
|
|
5738
5991
|
return s;
|
|
5739
5992
|
});
|
|
5993
|
+
if (resetFailure)
|
|
5994
|
+
return resetFailure;
|
|
5740
5995
|
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
5741
5996
|
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
5742
|
-
return
|
|
5997
|
+
return rejected(goalID, "blocked", "Goal has no worker session. Use resume_goal or retry_goal.");
|
|
5743
5998
|
}
|
|
5744
|
-
|
|
5745
|
-
return { ok: true, message: `Re-prompted worker for "${freshGoal.name}".` };
|
|
5999
|
+
return continueTurnUnlocked(directory, goalID, { force: true });
|
|
5746
6000
|
}
|
|
5747
6001
|
async function sendUnlocked(directory, goalID, text) {
|
|
5748
6002
|
const trimmed = text.trim();
|
|
5749
6003
|
if (!trimmed)
|
|
5750
|
-
return
|
|
6004
|
+
return rejected(goalID, "invalid_state", "Nothing to send.");
|
|
6005
|
+
await reconcileTerminalWorkspaceCalls(directory, host);
|
|
5751
6006
|
const preState = await readState(directory);
|
|
5752
6007
|
const goal = preState.goals.find((g) => g.id === goalID);
|
|
5753
6008
|
if (!goal)
|
|
5754
|
-
return
|
|
5755
|
-
await appendGoalInbox(directory, goalID, "user", trimmed)
|
|
6009
|
+
return rejected(goalID, "not_found", "Goal not found (it may have been cleared). List your goals instead of retrying this ID.");
|
|
6010
|
+
if (!await appendGoalInbox(directory, goalID, "user", trimmed))
|
|
6011
|
+
return rejected(goalID, "not_found", "Goal cleared before words could be queued. List current goals instead of retrying this ID.");
|
|
5756
6012
|
if (goal.status !== "active") {
|
|
5757
|
-
return { ok: true, message: `Queued for "${goal.name}" (goal is ${goal.status}; delivers on the next active turn).` };
|
|
6013
|
+
return { ok: true, outcome: "queued", goalID, message: `Queued for "${goal.name}" (goal is ${goal.status}; delivers on the next active turn).` };
|
|
6014
|
+
}
|
|
6015
|
+
const guard = continuationGuard(preState, goalID);
|
|
6016
|
+
if (guard)
|
|
6017
|
+
return { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}". ${guard.message}` };
|
|
6018
|
+
const status = goal.workerSessionID ? await host.sessionStatus(goal.workerSessionID) : undefined;
|
|
6019
|
+
if (!(await readState(directory)).goals.some((item) => item.id === goalID))
|
|
6020
|
+
return rejected(goalID, "not_found", "Goal cleared during worker status probe; no prompt was sent.");
|
|
6021
|
+
if (goal.workerSessionID && status !== "idle") {
|
|
6022
|
+
return { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}"; worker is busy or unconfirmed. Its active turn was left intact.` };
|
|
5758
6023
|
}
|
|
5759
6024
|
if (goal.config.write_scope !== undefined) {
|
|
5760
|
-
await continueTurnUnlocked(directory, goalID, { bare: true });
|
|
5761
|
-
return { ok: true, message: `
|
|
6025
|
+
const result = await continueTurnUnlocked(directory, goalID, { bare: true });
|
|
6026
|
+
return result.ok || result.outcome === "not_found" ? result : { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}". ${result.message}` };
|
|
5762
6027
|
}
|
|
6028
|
+
let resetFailure;
|
|
5763
6029
|
const cleared = await mutateState(directory, `goal.send:${goalID}`, async (s) => {
|
|
6030
|
+
resetFailure = continuationGuard(s, goalID);
|
|
6031
|
+
if (resetFailure)
|
|
6032
|
+
return s;
|
|
5764
6033
|
const rt = s.runtimes.find((r) => r.goalID === goalID);
|
|
5765
6034
|
if (!rt)
|
|
5766
6035
|
return s;
|
|
@@ -5772,12 +6041,14 @@ function createGoalService(host) {
|
|
|
5772
6041
|
rt.updatedAt = new Date().toISOString();
|
|
5773
6042
|
return s;
|
|
5774
6043
|
});
|
|
6044
|
+
if (resetFailure)
|
|
6045
|
+
return resetFailure.outcome === "not_found" ? resetFailure : { ok: true, outcome: "queued", goalID, message: `Words saved in inbox. ${resetFailure.message}` };
|
|
5775
6046
|
const freshGoal = cleared.goals.find((g) => g.id === goalID);
|
|
5776
6047
|
if (!freshGoal || !freshGoal.workerSessionID) {
|
|
5777
|
-
return { ok: true, message: `Queued for "${goal.name}" (no worker session yet; delivers on the next turn).` };
|
|
6048
|
+
return { ok: true, outcome: "queued", goalID, message: `Queued for "${goal.name}" (no worker session yet; delivers on the next turn).` };
|
|
5778
6049
|
}
|
|
5779
|
-
await continueTurnUnlocked(directory, goalID, { force: true, bare: true });
|
|
5780
|
-
return { ok: true, message: `
|
|
6050
|
+
const result = await continueTurnUnlocked(directory, goalID, { force: true, bare: true });
|
|
6051
|
+
return result.ok || result.outcome === "not_found" ? result : { ok: true, outcome: "queued", goalID, message: `Saved in inbox for "${goal.name}". ${result.message}` };
|
|
5781
6052
|
}
|
|
5782
6053
|
function sendUserMessage(directory, goalID, text) {
|
|
5783
6054
|
return withGoalOperation(goalID, () => sendUnlocked(directory, goalID, text));
|
|
@@ -5851,8 +6122,9 @@ function createGoalService(host) {
|
|
|
5851
6122
|
|
|
5852
6123
|
// src/application/schedule-worker.ts
|
|
5853
6124
|
init_state_repository();
|
|
5854
|
-
|
|
6125
|
+
init_server_log();
|
|
5855
6126
|
init_workspace_execution();
|
|
6127
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
5856
6128
|
function createScheduleWorker(options) {
|
|
5857
6129
|
const { directory, goalService } = options;
|
|
5858
6130
|
const intervalMs = options.intervalMs ?? 5000;
|
|
@@ -5943,7 +6215,7 @@ function createScheduleWorker(options) {
|
|
|
5943
6215
|
continue;
|
|
5944
6216
|
await appendEvent(directory, {
|
|
5945
6217
|
version: 1,
|
|
5946
|
-
eventID:
|
|
6218
|
+
eventID: randomUUID8(),
|
|
5947
6219
|
goalID: goal.id,
|
|
5948
6220
|
type: "schedule.tick",
|
|
5949
6221
|
scheduleRunCount: count,
|
|
@@ -5969,7 +6241,7 @@ function createScheduleWorker(options) {
|
|
|
5969
6241
|
}
|
|
5970
6242
|
|
|
5971
6243
|
// src/v2/native-server.ts
|
|
5972
|
-
import { randomUUID as
|
|
6244
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
5973
6245
|
|
|
5974
6246
|
// src/v2/native-bridge.ts
|
|
5975
6247
|
class NativeBridgeError extends Error {
|
|
@@ -6111,7 +6383,7 @@ async function setupNativeServer(context) {
|
|
|
6111
6383
|
bridge,
|
|
6112
6384
|
requestWorker: async (input) => {
|
|
6113
6385
|
const request = {
|
|
6114
|
-
requestID: `req_${
|
|
6386
|
+
requestID: `req_${randomUUID9()}`,
|
|
6115
6387
|
goalID: input.goalID ?? "",
|
|
6116
6388
|
parentSessionID: input.parentSessionID,
|
|
6117
6389
|
title: input.title,
|
|
@@ -6380,7 +6652,7 @@ function createCommandHost(opts) {
|
|
|
6380
6652
|
}
|
|
6381
6653
|
|
|
6382
6654
|
// src/application/command-service.ts
|
|
6383
|
-
import { randomUUID as
|
|
6655
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
6384
6656
|
import { promises as fs5 } from "fs";
|
|
6385
6657
|
import path5 from "path";
|
|
6386
6658
|
|
|
@@ -6636,6 +6908,7 @@ function validateCommandStreamMessage(value) {
|
|
|
6636
6908
|
}
|
|
6637
6909
|
|
|
6638
6910
|
// src/application/command-service.ts
|
|
6911
|
+
init_server_log();
|
|
6639
6912
|
init_workspace_execution();
|
|
6640
6913
|
init_command_await2();
|
|
6641
6914
|
init_state_repository();
|
|
@@ -6745,7 +7018,7 @@ function createCommandService(host, opts) {
|
|
|
6745
7018
|
function watchLedger(directory, commandID, type, extra) {
|
|
6746
7019
|
return appendEvent(directory, {
|
|
6747
7020
|
version: 1,
|
|
6748
|
-
eventID:
|
|
7021
|
+
eventID: randomUUID10(),
|
|
6749
7022
|
commandID,
|
|
6750
7023
|
type,
|
|
6751
7024
|
timestamp: new Date().toISOString(),
|
|
@@ -7222,7 +7495,7 @@ function createCommandService(host, opts) {
|
|
|
7222
7495
|
}).catch(() => {});
|
|
7223
7496
|
await appendEvent(directory, {
|
|
7224
7497
|
version: 1,
|
|
7225
|
-
eventID:
|
|
7498
|
+
eventID: randomUUID10(),
|
|
7226
7499
|
commandID: id,
|
|
7227
7500
|
type: "command.exited",
|
|
7228
7501
|
exitCode: info.exitCode,
|
|
@@ -7267,7 +7540,7 @@ function createCommandService(host, opts) {
|
|
|
7267
7540
|
watchState = initialWatchState();
|
|
7268
7541
|
}
|
|
7269
7542
|
const startedAt = new Date;
|
|
7270
|
-
const id =
|
|
7543
|
+
const id = randomUUID10();
|
|
7271
7544
|
const cwd = input.cwd || directory;
|
|
7272
7545
|
const useShell = input.shell === true;
|
|
7273
7546
|
const spawnOpts = useShell ? { command: "/bin/sh", args: ["-c", shellJoin(command, input.args ?? [])], cwd, cols: input.cols, rows: input.rows, env: input.env } : { command, args: input.args ?? [], cwd, cols: input.cols, rows: input.rows, env: input.env };
|
|
@@ -7333,7 +7606,7 @@ function createCommandService(host, opts) {
|
|
|
7333
7606
|
rememberDir(id, directory);
|
|
7334
7607
|
await appendEvent(directory, {
|
|
7335
7608
|
version: 1,
|
|
7336
|
-
eventID:
|
|
7609
|
+
eventID: randomUUID10(),
|
|
7337
7610
|
...input.goalID ? { goalID: input.goalID } : {},
|
|
7338
7611
|
commandID: id,
|
|
7339
7612
|
type: "command.started",
|
|
@@ -7433,7 +7706,7 @@ function createCommandService(host, opts) {
|
|
|
7433
7706
|
return { session, text: file.text, totalBytes: session.outputBytes, startByte: file.startByte, live: session.status === "running" };
|
|
7434
7707
|
},
|
|
7435
7708
|
async write(directory, id, ownerSessionID, data) {
|
|
7436
|
-
const scopeCallID = `command-write:${
|
|
7709
|
+
const scopeCallID = `command-write:${randomUUID10()}`;
|
|
7437
7710
|
await beforeWorkspaceTool(directory, { tool: "loopd_command_write", sessionID: ownerSessionID, callID: scopeCallID }, {});
|
|
7438
7711
|
try {
|
|
7439
7712
|
const session = await service.get(directory, id, ownerSessionID);
|
|
@@ -7889,7 +8162,7 @@ function createCommandService(host, opts) {
|
|
|
7889
8162
|
}
|
|
7890
8163
|
|
|
7891
8164
|
// src/application/command-event-broker.ts
|
|
7892
|
-
import { randomUUID as
|
|
8165
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
7893
8166
|
function safeDeliver(sink, msg) {
|
|
7894
8167
|
try {
|
|
7895
8168
|
sink(msg);
|
|
@@ -7932,7 +8205,7 @@ function createCommandEventBroker(resolver) {
|
|
|
7932
8205
|
throw new Error("ownerSessionID is required.");
|
|
7933
8206
|
if (typeof sink !== "function")
|
|
7934
8207
|
throw new Error("sink must be a function.");
|
|
7935
|
-
const sinkID =
|
|
8208
|
+
const sinkID = randomUUID11();
|
|
7936
8209
|
const entry = { sinkID, sink, state: "subscribing", buffer: [] };
|
|
7937
8210
|
entriesFor(commandID).set(sinkID, entry);
|
|
7938
8211
|
try {
|
|
@@ -8021,9 +8294,10 @@ function createCommandEventBroker(resolver) {
|
|
|
8021
8294
|
}
|
|
8022
8295
|
|
|
8023
8296
|
// src/server/command-stream-server.ts
|
|
8024
|
-
import { randomBytes, randomUUID as
|
|
8297
|
+
import { randomBytes, randomUUID as randomUUID12, timingSafeEqual } from "crypto";
|
|
8025
8298
|
import { promises as fs6 } from "fs";
|
|
8026
8299
|
import path6 from "path";
|
|
8300
|
+
init_server_log();
|
|
8027
8301
|
function streamEndpointPath(directory) {
|
|
8028
8302
|
return path6.join(directory, ".opencode", "loopd", "commands", ".stream-endpoint.json");
|
|
8029
8303
|
}
|
|
@@ -8055,7 +8329,7 @@ var MAX_BUFFERED_BYTES = 512 * 1024;
|
|
|
8055
8329
|
function createCommandStreamServer(directory, commandService, broker) {
|
|
8056
8330
|
const endpointPath = streamEndpointPath(directory);
|
|
8057
8331
|
const token = randomBytes(32).toString("hex");
|
|
8058
|
-
const generation =
|
|
8332
|
+
const generation = randomUUID12();
|
|
8059
8333
|
const startedAt = new Date().toISOString();
|
|
8060
8334
|
let server;
|
|
8061
8335
|
let serverURL;
|
|
@@ -8375,7 +8649,7 @@ function createCommandStreamServer(directory, commandService, broker) {
|
|
|
8375
8649
|
|
|
8376
8650
|
// src/server/goal-tools.ts
|
|
8377
8651
|
init_state_repository();
|
|
8378
|
-
import { randomUUID as
|
|
8652
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
8379
8653
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
8380
8654
|
// src/domain/verification.ts
|
|
8381
8655
|
var MAX_RECENT_ATTEMPTS = 10;
|
|
@@ -8390,6 +8664,7 @@ function appendVerificationAttempt(recent, attempt) {
|
|
|
8390
8664
|
// src/server/goal-tools.ts
|
|
8391
8665
|
import { exec as execChild } from "child_process";
|
|
8392
8666
|
import { promisify } from "util";
|
|
8667
|
+
init_server_log();
|
|
8393
8668
|
|
|
8394
8669
|
// src/application/goal-scope.ts
|
|
8395
8670
|
init_state_repository();
|
|
@@ -8703,7 +8978,7 @@ function goalTools(dir, goalService, hostSessionID, defaults = {}, host) {
|
|
|
8703
8978
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active"])).revision;
|
|
8704
8979
|
const event = {
|
|
8705
8980
|
version: 1,
|
|
8706
|
-
eventID:
|
|
8981
|
+
eventID: randomUUID13(),
|
|
8707
8982
|
goalID: goal.id,
|
|
8708
8983
|
type: "goal.progress",
|
|
8709
8984
|
summary: args.summary,
|
|
@@ -8768,7 +9043,7 @@ Exit code: ${f.exitCode}${stdoutSnippet}${stderrSnippet}`;
|
|
|
8768
9043
|
Working directory: ${cwd}
|
|
8769
9044
|
|
|
8770
9045
|
${failureDetails}`;
|
|
8771
|
-
attemptID =
|
|
9046
|
+
attemptID = randomUUID13();
|
|
8772
9047
|
const verificationAttempt = {
|
|
8773
9048
|
id: attemptID,
|
|
8774
9049
|
sequence: runtime.evaluatorRejectionCount,
|
|
@@ -8813,7 +9088,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8813
9088
|
if (attemptID) {
|
|
8814
9089
|
await appendEvent(dir, {
|
|
8815
9090
|
version: 1,
|
|
8816
|
-
eventID:
|
|
9091
|
+
eventID: randomUUID13(),
|
|
8817
9092
|
goalID: goal.id,
|
|
8818
9093
|
type: "goal.completion_rejected",
|
|
8819
9094
|
attemptID,
|
|
@@ -8831,7 +9106,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8831
9106
|
if (blocked && rejectedGoal?.blocker) {
|
|
8832
9107
|
await appendEvent(dir, {
|
|
8833
9108
|
version: 1,
|
|
8834
|
-
eventID:
|
|
9109
|
+
eventID: randomUUID13(),
|
|
8835
9110
|
goalID: goal.id,
|
|
8836
9111
|
type: "goal.blocked",
|
|
8837
9112
|
reason: rejectedGoal.blocker.reason,
|
|
@@ -8891,7 +9166,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8891
9166
|
}
|
|
8892
9167
|
runtime.updatedAt = new Date().toISOString();
|
|
8893
9168
|
}
|
|
8894
|
-
const attemptID =
|
|
9169
|
+
const attemptID = randomUUID13();
|
|
8895
9170
|
const cwd = goal.config.checkCwd || goal.config.artifactDir || dir;
|
|
8896
9171
|
const checks = (goal.config.checks || []).map((cmd) => ({
|
|
8897
9172
|
command: cmd,
|
|
@@ -8915,7 +9190,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8915
9190
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active", "budget_limited"])).revision;
|
|
8916
9191
|
const event = {
|
|
8917
9192
|
version: 1,
|
|
8918
|
-
eventID:
|
|
9193
|
+
eventID: randomUUID13(),
|
|
8919
9194
|
goalID: goal.id,
|
|
8920
9195
|
type: "goal.completed",
|
|
8921
9196
|
summary: args.summary,
|
|
@@ -8949,6 +9224,16 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8949
9224
|
if (!goal) {
|
|
8950
9225
|
return { title: "No goal", output: "No active goal to block." };
|
|
8951
9226
|
}
|
|
9227
|
+
if (goal.status === "blocked") {
|
|
9228
|
+
return { title: "Goal already blocked", output: JSON.stringify({
|
|
9229
|
+
goalID: goal.id,
|
|
9230
|
+
goalName: goal.name,
|
|
9231
|
+
status: "blocked",
|
|
9232
|
+
alreadyBlocked: true,
|
|
9233
|
+
reason: goal.blocker?.reason,
|
|
9234
|
+
needed: goal.blocker?.needed
|
|
9235
|
+
}) };
|
|
9236
|
+
}
|
|
8952
9237
|
if (!canTransition(goal.status, "blocked", "model")) {
|
|
8953
9238
|
return { title: "Invalid transition", output: `Cannot block goal in ${goal.status} state.` };
|
|
8954
9239
|
}
|
|
@@ -8975,7 +9260,7 @@ ${failureDetails.slice(0, 500)}`,
|
|
|
8975
9260
|
state.revision = (await persistWorkerMutation(dir, goal, runtime, ["active", "budget_limited"])).revision;
|
|
8976
9261
|
const event = {
|
|
8977
9262
|
version: 1,
|
|
8978
|
-
eventID:
|
|
9263
|
+
eventID: randomUUID13(),
|
|
8979
9264
|
goalID: goal.id,
|
|
8980
9265
|
type: "goal.blocked",
|
|
8981
9266
|
reason: args.reason,
|
|
@@ -9029,6 +9314,9 @@ async function withParentIdentity(defaults, host, ownerSessionID) {
|
|
|
9029
9314
|
function findGoalByWorkerSession(state, sessionID) {
|
|
9030
9315
|
if (!sessionID)
|
|
9031
9316
|
return;
|
|
9317
|
+
if (state.retiredWorkerSessions?.includes(sessionID)) {
|
|
9318
|
+
throw new Error("Goal cleared or worker retired. This session has no goal authority; stop work and ask the owner to list their current goals. Do not resume a remembered ID.");
|
|
9319
|
+
}
|
|
9032
9320
|
return state.goals.find((g) => g.workerSessionID === sessionID && (g.status === "active" || g.status === "blocked"));
|
|
9033
9321
|
}
|
|
9034
9322
|
function formatGoalStructured(goal, runtime, enforcedScopedExecution = false) {
|
|
@@ -9156,7 +9444,7 @@ var GOAL_STATUS_META = {
|
|
|
9156
9444
|
var PHASE_META = {
|
|
9157
9445
|
idle: { short: "Idle", hint: "between turns" },
|
|
9158
9446
|
queued: { short: "Queued", hint: "waiting to start" },
|
|
9159
|
-
running: { short: "
|
|
9447
|
+
running: { short: "Reserved", hint: "turn reserved; activity unconfirmed" },
|
|
9160
9448
|
compacting: { short: "Compacting", hint: "summarizing context" },
|
|
9161
9449
|
waiting_retry: { short: "Retrying", hint: "backing off" },
|
|
9162
9450
|
stopping: { short: "Stopping", hint: "abort in flight" }
|
|
@@ -9167,9 +9455,30 @@ function goalStatusLabel(status) {
|
|
|
9167
9455
|
function phaseLabel(phase) {
|
|
9168
9456
|
return PHASE_META[phase] ?? { short: phase, hint: "" };
|
|
9169
9457
|
}
|
|
9170
|
-
function
|
|
9458
|
+
function workerActivityLabel(runtime, now = Date.now()) {
|
|
9459
|
+
if (runtime.phase !== "running")
|
|
9460
|
+
return { ...phaseLabel(runtime.phase), acting: false };
|
|
9461
|
+
const expires = runtime.leaseExpiresAt ? Date.parse(runtime.leaseExpiresAt) : NaN;
|
|
9462
|
+
if (Number.isFinite(expires) && expires <= now)
|
|
9463
|
+
return { short: "Stale", hint: "lease expired; activity unconfirmed (not proof of worker exit)", acting: false };
|
|
9464
|
+
if (!Number.isFinite(expires) || (runtime.unknownStatusCount ?? 0) > 0)
|
|
9465
|
+
return { short: "Unconfirmed", hint: "worker status unavailable; do not assume the prompt failed", acting: false };
|
|
9466
|
+
if (runtime.activeAssistantCompletedAt)
|
|
9467
|
+
return { short: "Finishing", hint: "response completed; waiting for turn reconciliation", acting: false };
|
|
9468
|
+
if (runtime.activePromptObservedAt && runtime.lastWorkerStatus !== "idle" && runtime.lastWorkerStatus !== "retry" && (runtime.activeAssistantMessageID || runtime.activeToolCallIDs?.length)) {
|
|
9469
|
+
return { short: "Running", hint: "correlated assistant or tool activity observed", acting: true };
|
|
9470
|
+
}
|
|
9471
|
+
if (runtime.activePromptObservedAt)
|
|
9472
|
+
return { short: "Prompt seen", hint: "prompt observed; response activity not confirmed", acting: false };
|
|
9473
|
+
if (runtime.activePromptAcceptedAt)
|
|
9474
|
+
return { short: "Awaiting", hint: "host accepted prompt; transcript activity not confirmed", acting: false };
|
|
9475
|
+
if (runtime.activePromptDispatchAt)
|
|
9476
|
+
return { short: "Sending", hint: "dispatch requested; awaiting host acceptance", acting: false };
|
|
9477
|
+
return { short: "Requested", hint: "run reserved; no dispatch evidence yet", acting: false };
|
|
9478
|
+
}
|
|
9479
|
+
function describeGoalState(status, phase, now = Date.now()) {
|
|
9171
9480
|
const goal = goalStatusLabel(status);
|
|
9172
|
-
const activity = phase ? phaseLabel(phase) : undefined;
|
|
9481
|
+
const activity = phase ? typeof phase === "string" ? phaseLabel(phase) : workerActivityLabel(phase, now) : undefined;
|
|
9173
9482
|
const workerClause = activity ? `worker is ${activity.hint || activity.short.toLowerCase()}` : "worker state unknown";
|
|
9174
9483
|
if (status === "active")
|
|
9175
9484
|
return `Loopd owns this; ${workerClause}.`;
|
|
@@ -9514,18 +9823,21 @@ function ownerTools2(options) {
|
|
|
9514
9823
|
execute: async (args, context) => {
|
|
9515
9824
|
const state = await readState(directory);
|
|
9516
9825
|
const ownerID = context?.sessionID;
|
|
9517
|
-
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id
|
|
9826
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
9518
9827
|
if (!goal) {
|
|
9519
9828
|
return {
|
|
9520
9829
|
title: "No goal found",
|
|
9521
|
-
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
9830
|
+
output: JSON.stringify({ ok: false, outcome: "not_found", message: args.goal_id ? "Goal not found (it may have been cleared). List your goals instead of retrying this ID." : "No matching active goal for this session." })
|
|
9522
9831
|
};
|
|
9523
9832
|
}
|
|
9833
|
+
if (goal.ownerSessionID !== ownerID) {
|
|
9834
|
+
return { title: "Not authorized", output: JSON.stringify({ ok: false, outcome: "unauthorized", message: "This session cannot control that goal." }) };
|
|
9835
|
+
}
|
|
9524
9836
|
const result = await goalService.sendUserMessage(directory, goal.id, args.message);
|
|
9525
9837
|
return {
|
|
9526
|
-
title: result.ok ? "Message
|
|
9838
|
+
title: result.ok ? result.outcome === "queued" ? "Message queued" : "Message submitted" : "Send failed",
|
|
9527
9839
|
output: JSON.stringify({
|
|
9528
|
-
|
|
9840
|
+
...result,
|
|
9529
9841
|
goalID: goal.id,
|
|
9530
9842
|
goalName: goal.name,
|
|
9531
9843
|
message: result.message
|
|
@@ -9585,16 +9897,20 @@ function ownerTools2(options) {
|
|
|
9585
9897
|
execute: async (args, context) => {
|
|
9586
9898
|
const state = await readState(directory);
|
|
9587
9899
|
const ownerID = context?.sessionID;
|
|
9588
|
-
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id
|
|
9900
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id) : state.goals.find((g) => g.ownerSessionID === ownerID && (g.status === "paused" || g.status === "blocked"));
|
|
9589
9901
|
if (!goal) {
|
|
9590
9902
|
return {
|
|
9591
9903
|
title: "No goal found",
|
|
9592
9904
|
output: JSON.stringify({
|
|
9593
9905
|
ok: false,
|
|
9594
|
-
|
|
9906
|
+
outcome: "not_found",
|
|
9907
|
+
message: args.goal_id ? "Goal not found (it may have been cleared). List your goals instead of retrying this ID." : "No matching paused/blocked goal for this session."
|
|
9595
9908
|
})
|
|
9596
9909
|
};
|
|
9597
9910
|
}
|
|
9911
|
+
if (goal.ownerSessionID !== ownerID) {
|
|
9912
|
+
return { title: "Not authorized", output: JSON.stringify({ ok: false, outcome: "unauthorized", message: "This session cannot control that goal." }) };
|
|
9913
|
+
}
|
|
9598
9914
|
if (goal.status === "active") {
|
|
9599
9915
|
return {
|
|
9600
9916
|
title: "Already active",
|
|
@@ -9602,18 +9918,13 @@ function ownerTools2(options) {
|
|
|
9602
9918
|
};
|
|
9603
9919
|
}
|
|
9604
9920
|
try {
|
|
9605
|
-
|
|
9606
|
-
await goalService.resume(directory, goal.id);
|
|
9607
|
-
} else if (goal.status === "blocked") {
|
|
9608
|
-
await goalService.retry(directory, goal.id);
|
|
9609
|
-
}
|
|
9921
|
+
const result = goal.status === "blocked" ? await goalService.retry(directory, goal.id) : await goalService.resume(directory, goal.id);
|
|
9610
9922
|
return {
|
|
9611
|
-
title: "
|
|
9923
|
+
title: result.ok ? "Resume submitted" : "Resume not delivered",
|
|
9612
9924
|
output: JSON.stringify({
|
|
9613
|
-
|
|
9925
|
+
...result,
|
|
9614
9926
|
goalID: goal.id,
|
|
9615
|
-
goalName: goal.name
|
|
9616
|
-
message: `Goal "${goal.name}" resumed.`
|
|
9927
|
+
goalName: goal.name
|
|
9617
9928
|
})
|
|
9618
9929
|
};
|
|
9619
9930
|
} catch (error) {
|
|
@@ -9629,20 +9940,23 @@ function ownerTools2(options) {
|
|
|
9629
9940
|
}
|
|
9630
9941
|
}),
|
|
9631
9942
|
nudge_goal: tool2({
|
|
9632
|
-
description: "
|
|
9943
|
+
description: "Request a recovery turn for an active goal after confirmed worker idle. Returns the actual delivery outcome or actionable blocker (in-flight writes, workspace operation, busy/unknown worker, pending identity, quota). Never interrupts a live worker or duplicates an unconfirmed prompt. Per-goal mutex.",
|
|
9633
9944
|
args: {
|
|
9634
9945
|
goal_id: tool2.schema.string().optional().describe("Goal ID. Omit to nudge the first active goal.")
|
|
9635
9946
|
},
|
|
9636
9947
|
execute: async (args, context) => {
|
|
9637
9948
|
const state = await readState(directory);
|
|
9638
9949
|
const ownerID = context?.sessionID;
|
|
9639
|
-
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id
|
|
9950
|
+
const goal = args.goal_id ? state.goals.find((g) => g.id === args.goal_id) : state.goals.find((g) => g.ownerSessionID === ownerID && g.status !== "complete");
|
|
9640
9951
|
if (!goal) {
|
|
9641
9952
|
return {
|
|
9642
9953
|
title: "No goal found",
|
|
9643
|
-
output: JSON.stringify({ ok: false, message: "No matching active goal for this session." })
|
|
9954
|
+
output: JSON.stringify({ ok: false, outcome: "not_found", message: args.goal_id ? "Goal not found (it may have been cleared). List your goals instead of retrying this ID." : "No matching active goal for this session." })
|
|
9644
9955
|
};
|
|
9645
9956
|
}
|
|
9957
|
+
if (goal.ownerSessionID !== ownerID) {
|
|
9958
|
+
return { title: "Not authorized", output: JSON.stringify({ ok: false, outcome: "unauthorized", message: "This session cannot control that goal." }) };
|
|
9959
|
+
}
|
|
9646
9960
|
try {
|
|
9647
9961
|
const result = await goalService.nudge(directory, goal.id);
|
|
9648
9962
|
return {
|
|
@@ -10144,9 +10458,10 @@ function commandTools(options) {
|
|
|
10144
10458
|
}
|
|
10145
10459
|
|
|
10146
10460
|
// src/server/plugin.ts
|
|
10461
|
+
init_server_log();
|
|
10147
10462
|
init_state_repository();
|
|
10148
10463
|
// package.json
|
|
10149
|
-
var version = "1.13.
|
|
10464
|
+
var version = "1.13.2";
|
|
10150
10465
|
|
|
10151
10466
|
// src/server/plugin.ts
|
|
10152
10467
|
init_workspace_execution();
|
|
@@ -10255,6 +10570,8 @@ function createServerHooks(directory, host, defaults) {
|
|
|
10255
10570
|
tool: { ...goalTools(directory, goalService, undefined, defaults, host), ...ownerTools2({ directory, host, goalService }), ...commandTools({ directory, commandService, capabilities: commandHost.capabilities, goalService }) },
|
|
10256
10571
|
"tool.execute.before": async (input, _output) => {
|
|
10257
10572
|
await beforeWorkspaceTool(directory, input, _output.args);
|
|
10573
|
+
if (isWorkspaceToolExempt(input.tool))
|
|
10574
|
+
return;
|
|
10258
10575
|
const activeWorkers = goalService.getActiveWorkers();
|
|
10259
10576
|
let matchedGoalID;
|
|
10260
10577
|
for (const [goalID, worker] of activeWorkers) {
|
|
@@ -10275,7 +10592,7 @@ function createServerHooks(directory, host, defaults) {
|
|
|
10275
10592
|
} catch {}
|
|
10276
10593
|
},
|
|
10277
10594
|
"tool.execute.after": async (input, output) => {
|
|
10278
|
-
await afterWorkspaceTool(directory, input.sessionID, input.callID);
|
|
10595
|
+
await afterWorkspaceTool(directory, input.sessionID, input.callID, input.tool);
|
|
10279
10596
|
if (input.tool === "loopd_create_goal" || input.tool === "get_goal" || input.tool === "report_goal_progress") {
|
|
10280
10597
|
ensureStarted();
|
|
10281
10598
|
reconcileInBackground();
|
|
@@ -10283,7 +10600,7 @@ function createServerHooks(directory, host, defaults) {
|
|
|
10283
10600
|
const activeWorkers = goalService.getActiveWorkers();
|
|
10284
10601
|
let matchedGoalID;
|
|
10285
10602
|
for (const [goalID, worker] of activeWorkers) {
|
|
10286
|
-
if (worker.workerSessionID === input.sessionID) {
|
|
10603
|
+
if (!isWorkspaceToolExempt(input.tool) && worker.workerSessionID === input.sessionID) {
|
|
10287
10604
|
matchedGoalID = goalID;
|
|
10288
10605
|
break;
|
|
10289
10606
|
}
|
|
@@ -10304,6 +10621,8 @@ function createServerHooks(directory, host, defaults) {
|
|
|
10304
10621
|
if (!raw)
|
|
10305
10622
|
return;
|
|
10306
10623
|
const parsed = JSON.parse(raw);
|
|
10624
|
+
if (parsed.alreadyBlocked === true)
|
|
10625
|
+
return;
|
|
10307
10626
|
if (parsed.status !== "complete" && parsed.status !== "blocked")
|
|
10308
10627
|
return;
|
|
10309
10628
|
const goalID = parsed.goalID;
|
|
@@ -10370,6 +10689,7 @@ var v2 = {
|
|
|
10370
10689
|
const eventController = new AbortController;
|
|
10371
10690
|
let eventTask = Promise.resolve();
|
|
10372
10691
|
let disposed = false;
|
|
10692
|
+
let guardedToolIDs = new Set;
|
|
10373
10693
|
const cleanup = async () => {
|
|
10374
10694
|
if (disposed)
|
|
10375
10695
|
return;
|
|
@@ -10387,9 +10707,23 @@ var v2 = {
|
|
|
10387
10707
|
for (const [id, definition] of Object.entries(hooks.tool ?? {})) {
|
|
10388
10708
|
editor.add(toV2Tool(id, definition, directory));
|
|
10389
10709
|
}
|
|
10390
|
-
guardV2ToolEditor(directory, editor
|
|
10710
|
+
guardedToolIDs = guardV2ToolEditor(directory, editor, {
|
|
10711
|
+
before: async (input, args) => {
|
|
10712
|
+
await hooks["tool.execute.before"]?.(input, { args });
|
|
10713
|
+
},
|
|
10714
|
+
after: async (input, args, result) => {
|
|
10715
|
+
const output = result;
|
|
10716
|
+
await hooks["tool.execute.after"]?.({ ...input, args }, {
|
|
10717
|
+
title: "",
|
|
10718
|
+
output: typeof output?.content === "string" ? output.content : JSON.stringify(output?.content ?? ""),
|
|
10719
|
+
metadata: output?.metadata ?? {}
|
|
10720
|
+
});
|
|
10721
|
+
}
|
|
10722
|
+
});
|
|
10391
10723
|
}));
|
|
10392
10724
|
registrations.push(await context.tool.hook("execute.before", async (input) => {
|
|
10725
|
+
if (input.tool === "execute" || guardedToolIDs.has(input.tool))
|
|
10726
|
+
return;
|
|
10393
10727
|
await hooks["tool.execute.before"]?.({
|
|
10394
10728
|
tool: input.tool,
|
|
10395
10729
|
sessionID: input.sessionID,
|
|
@@ -10397,6 +10731,8 @@ var v2 = {
|
|
|
10397
10731
|
}, { args: input.input });
|
|
10398
10732
|
}));
|
|
10399
10733
|
registrations.push(await context.tool.hook("execute.after", async (input) => {
|
|
10734
|
+
if (input.tool === "execute" || guardedToolIDs.has(input.tool))
|
|
10735
|
+
return;
|
|
10400
10736
|
const output = input.status === "completed" ? input.result : { content: JSON.stringify(input.error) };
|
|
10401
10737
|
await hooks["tool.execute.after"]?.({
|
|
10402
10738
|
tool: input.tool,
|