kiokuko-dsh 0.1.21 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dsh/boundary-worker.d.ts +4 -0
- package/dist/dsh/boundary-worker.d.ts.map +1 -1
- package/dist/dsh/boundary-worker.js +36 -10
- package/dist/dsh/boundary-worker.js.map +1 -1
- package/dist/dsh/continuation.d.ts +1 -0
- package/dist/dsh/continuation.d.ts.map +1 -1
- package/dist/dsh/continuation.js +19 -5
- package/dist/dsh/continuation.js.map +1 -1
- package/dist/dsh/enno-controller.d.ts +6 -0
- package/dist/dsh/enno-controller.d.ts.map +1 -1
- package/dist/dsh/enno-controller.js +42 -14
- package/dist/dsh/enno-controller.js.map +1 -1
- package/dist/dsh/host-adapter.d.ts.map +1 -1
- package/dist/dsh/host-adapter.js +275 -5
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/loop-guard.d.ts +43 -0
- package/dist/dsh/loop-guard.d.ts.map +1 -0
- package/dist/dsh/loop-guard.js +206 -0
- package/dist/dsh/loop-guard.js.map +1 -0
- package/dist/dsh/tools.d.ts.map +1 -1
- package/dist/dsh/tools.js +6 -6
- package/dist/dsh/tools.js.map +1 -1
- package/dist/dsh/turn-process.d.ts +8 -1
- package/dist/dsh/turn-process.d.ts.map +1 -1
- package/dist/dsh/turn-process.js +22 -1
- package/dist/dsh/turn-process.js.map +1 -1
- package/dist/model-tools/registry.d.ts +6 -0
- package/dist/model-tools/registry.d.ts.map +1 -1
- package/dist/model-tools/registry.js +35 -0
- package/dist/model-tools/registry.js.map +1 -1
- package/docs/database.md +8 -0
- package/migrations/004_dsh_loop_guard.sql +46 -0
- package/package.json +1 -1
package/dist/dsh/host-adapter.js
CHANGED
|
@@ -31,11 +31,12 @@ import { curateMemoryCandidates } from '../memory/curator.js';
|
|
|
31
31
|
import { checkpointDshMemory } from '../memory/scoped-memory.js';
|
|
32
32
|
import { LedgerStore } from '../ledger/store.js';
|
|
33
33
|
import { ENNO_APPLICABLE_TASK_TYPES } from '../enno-oduno/types.js';
|
|
34
|
-
import { commitExpectedFailure, ennoReceiptOperation, isExpectedTurnFailure, phaseForOperation, prepareTurnIntent, readPendingOutbox, readTurnSeal, markOutboxObservedInTransaction, appliedTurnOutcome, replacePendingOutboxMessageInTransaction, supersedeOutboxAtOrBeforeRevisionInTransaction, } from './turn-process.js';
|
|
34
|
+
import { commitExpectedFailure, ennoReceiptOperation, isExpectedTurnFailure, phaseForOperation, prepareTurnIntent, readPendingOutbox, readTurnSeal, markOutboxObservedInTransaction, appliedTurnOutcome, replacePendingOutboxMessageInTransaction, supersedeBoundaryJobsAtOrBeforeRevisionInTransaction, supersedeOutboxAtOrBeforeRevisionInTransaction, } from './turn-process.js';
|
|
35
35
|
import { backupInputClaimInTransaction, markClaimProgressInTransaction, settleInputClaimInTransaction, takeRecoverableInputClaimInTransaction, } from './input-claim.js';
|
|
36
36
|
import { DshSessionLogMirror } from './session-log-mirror.js';
|
|
37
37
|
import { DshBoundaryWorker } from './boundary-worker.js';
|
|
38
38
|
import { DshSessionLogExportService } from './session-log-export.js';
|
|
39
|
+
import { claimAutomaticContinuationInTransaction, claimBoundaryEffectInTransaction, claimLoopRecoveryQuestionInTransaction, ennoInstructionDigest, resetBoundaryEffectGuardInTransaction, resetLoopGuardForUserInTransaction, } from './loop-guard.js';
|
|
39
40
|
function sessionEventSource(value) {
|
|
40
41
|
const source = value;
|
|
41
42
|
if (typeof source?.snapshotEvents !== 'function') {
|
|
@@ -94,12 +95,49 @@ function isHumanMessage(value) {
|
|
|
94
95
|
function pluginContinuationId(value) {
|
|
95
96
|
const message = objectRecord(value);
|
|
96
97
|
const source = objectRecord(message?.source);
|
|
97
|
-
if (source?.kind !== 'plugin' || source.plugin !== 'kiokuko-dsh'
|
|
98
|
+
if (source?.kind !== 'plugin' || source.plugin !== 'kiokuko-dsh'
|
|
99
|
+
|| (source.form !== 'continuation' && source.form !== 'loop-recovery'))
|
|
98
100
|
return undefined;
|
|
99
101
|
return typeof source.deliveryId === 'string' && /^[0-9a-f]{64}$/u.test(source.deliveryId)
|
|
100
102
|
? source.deliveryId
|
|
101
103
|
: undefined;
|
|
102
104
|
}
|
|
105
|
+
function isLoopRecoveryMessage(value) {
|
|
106
|
+
const source = objectRecord(objectRecord(value)?.source);
|
|
107
|
+
return source?.kind === 'plugin' && source.plugin === 'kiokuko-dsh' && source.form === 'loop-recovery';
|
|
108
|
+
}
|
|
109
|
+
function recoveryMessage(continuationId, answer) {
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
id: continuationId,
|
|
112
|
+
role: 'user',
|
|
113
|
+
content: [{
|
|
114
|
+
type: 'text',
|
|
115
|
+
text: `The user reviewed the stopped Kiokuko loop and supplied this recovery instruction:\n\n${answer}`,
|
|
116
|
+
}],
|
|
117
|
+
source: { kind: 'plugin', plugin: 'kiokuko-dsh', form: 'loop-recovery', deliveryId: continuationId },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function boundedMessageText(value) {
|
|
121
|
+
const content = objectRecord(value)?.content;
|
|
122
|
+
if (!Array.isArray(content))
|
|
123
|
+
return undefined;
|
|
124
|
+
const block = content.map(objectRecord).find((candidate) => candidate?.type === 'text' && typeof candidate.text === 'string');
|
|
125
|
+
return typeof block?.text === 'string' ? block.text.slice(0, 2_000) : undefined;
|
|
126
|
+
}
|
|
127
|
+
function boundedUtf8Text(value, maximumBytes) {
|
|
128
|
+
if (Buffer.byteLength(value, 'utf8') <= maximumBytes)
|
|
129
|
+
return value;
|
|
130
|
+
let result = '';
|
|
131
|
+
let bytes = 0;
|
|
132
|
+
for (const point of value) {
|
|
133
|
+
const size = Buffer.byteLength(point, 'utf8');
|
|
134
|
+
if (bytes + size > maximumBytes)
|
|
135
|
+
break;
|
|
136
|
+
result += point;
|
|
137
|
+
bytes += size;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
103
141
|
function eventContinuationId(data) {
|
|
104
142
|
const direct = pluginContinuationId(data);
|
|
105
143
|
if (direct !== undefined)
|
|
@@ -640,10 +678,20 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
640
678
|
if (humanPresent) {
|
|
641
679
|
const previous = currentSession(event.sessionId);
|
|
642
680
|
const state = previous === undefined ? undefined : states.get(previous.runId);
|
|
643
|
-
if (
|
|
681
|
+
if (previous !== undefined) {
|
|
644
682
|
try {
|
|
645
683
|
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
646
|
-
|
|
684
|
+
if (state !== undefined) {
|
|
685
|
+
const now = options.now?.() ?? new Date().toISOString();
|
|
686
|
+
supersedeOutboxAtOrBeforeRevisionInTransaction(database, event.sessionId, state.revision, now);
|
|
687
|
+
supersedeBoundaryJobsAtOrBeforeRevisionInTransaction(database, event.sessionId, state.revision, now);
|
|
688
|
+
}
|
|
689
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
690
|
+
runId: previous.runId,
|
|
691
|
+
dshSessionId: event.sessionId,
|
|
692
|
+
resolution: 'manual_user',
|
|
693
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
694
|
+
});
|
|
647
695
|
}));
|
|
648
696
|
}
|
|
649
697
|
catch {
|
|
@@ -752,9 +800,12 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
752
800
|
terminalizeLedgerRunInTransaction(database, runId, 'cancelled');
|
|
753
801
|
return undefined;
|
|
754
802
|
}
|
|
803
|
+
const automaticMessage = event.nativeMessages?.find((message) => pluginContinuationId(message) !== undefined && !isLoopRecoveryMessage(message));
|
|
804
|
+
const automaticClaimId = automaticMessage === undefined ? undefined : pluginContinuationId(automaticMessage);
|
|
755
805
|
const decision = decideDshContinuation(database, {
|
|
756
806
|
dshSessionId: event.sessionId,
|
|
757
807
|
cwd: event.cwd,
|
|
808
|
+
...(automaticClaimId === undefined ? {} : { claimId: automaticClaimId }),
|
|
758
809
|
}, runId);
|
|
759
810
|
if (!decision.continue || decision.runId !== runId) {
|
|
760
811
|
throw new KiokukoError('CONFLICT', decision.warning ?? 'The active Enno-Oduno run cannot be resumed by this DSH session');
|
|
@@ -1113,6 +1164,83 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1113
1164
|
signal,
|
|
1114
1165
|
});
|
|
1115
1166
|
const readBoundaryState = async (item) => (runtime.withDatabase((database) => stateForRun(database, item)));
|
|
1167
|
+
const askForRecoveryInstruction = async (input) => {
|
|
1168
|
+
const agent = input.item.nativeAgent ?? agents?.get(input.item.agentId);
|
|
1169
|
+
if (userQuestions === undefined || agent === undefined)
|
|
1170
|
+
return undefined;
|
|
1171
|
+
try {
|
|
1172
|
+
const result = await userQuestions.ask({
|
|
1173
|
+
questions: [{
|
|
1174
|
+
id: input.questionId,
|
|
1175
|
+
header: 'Kiokuko stopped',
|
|
1176
|
+
question: input.title,
|
|
1177
|
+
detail: input.detail,
|
|
1178
|
+
}],
|
|
1179
|
+
agent,
|
|
1180
|
+
});
|
|
1181
|
+
const answer = result.answers[0];
|
|
1182
|
+
if (answer === undefined || answer.id !== input.questionId)
|
|
1183
|
+
return undefined;
|
|
1184
|
+
const value = answer.custom?.trim() || answer.selected[0]?.trim();
|
|
1185
|
+
return value === undefined || value.length === 0 ? undefined : boundedUtf8Text(value, 8 * 1024);
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
// A broken/dismissed question surface must never become another retry
|
|
1189
|
+
// loop. The durable waiting_user state remains the recovery boundary.
|
|
1190
|
+
return undefined;
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
const loopRecoveryDetail = (snapshot, state, reason) => {
|
|
1194
|
+
const workUnitId = state.directive?.workUnit?.id ?? null;
|
|
1195
|
+
const workUnit = workUnitId === null
|
|
1196
|
+
? null
|
|
1197
|
+
: snapshot.workUnits.find((candidate) => candidate.workUnit.id === workUnitId) ?? null;
|
|
1198
|
+
const latestVerifier = snapshot.finalEvidence.at(-1);
|
|
1199
|
+
return [
|
|
1200
|
+
`Host status: phase=${snapshot.status}; nextAction=${state.nextAction}; role=${state.currentRole ?? 'none'}.`,
|
|
1201
|
+
`Revision=${snapshot.revision}; mutationRevision=${snapshot.mutationRevision}; attempts=${snapshot.attempts}/${snapshot.contract.maxAttempts}.`,
|
|
1202
|
+
workUnit === null
|
|
1203
|
+
? 'WorkUnit: none.'
|
|
1204
|
+
: `WorkUnit ${workUnit.workUnit.id}: ${workUnit.workUnit.objective} (status=${workUnit.status}, attempts=${workUnit.attemptCount}).`,
|
|
1205
|
+
latestVerifier === undefined
|
|
1206
|
+
? 'Latest verifier: none.'
|
|
1207
|
+
: `Latest verifier ${latestVerifier.verifier.id}: ${latestVerifier.status}.`,
|
|
1208
|
+
reason,
|
|
1209
|
+
'Enter the actual current handling status and the concrete instruction to execute next. If work should stop, say so explicitly.',
|
|
1210
|
+
].join('\n');
|
|
1211
|
+
};
|
|
1212
|
+
const guardBoundaryEffect = async (item, job) => {
|
|
1213
|
+
const guarded = await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1214
|
+
const snapshot = readEnnoSnapshot(database, {
|
|
1215
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1216
|
+
});
|
|
1217
|
+
const state = stateForSnapshot(snapshot);
|
|
1218
|
+
const claim = claimBoundaryEffectInTransaction(database, job, ennoInstructionDigest(snapshot, state.directive), options.now?.() ?? new Date().toISOString());
|
|
1219
|
+
return { snapshot, state, claim };
|
|
1220
|
+
}));
|
|
1221
|
+
if (guarded.claim.decision === 'deliver')
|
|
1222
|
+
return true;
|
|
1223
|
+
const answer = await askForRecoveryInstruction({
|
|
1224
|
+
item,
|
|
1225
|
+
questionId: `effect-${job.jobId.slice(0, 16)}`,
|
|
1226
|
+
title: 'Kiokuko stopped before a fourth stateful boundary operation without progress.',
|
|
1227
|
+
detail: loopRecoveryDetail(guarded.snapshot, guarded.state, `${job.kind} completed or was re-entered three times without authoritative Enno progress.`),
|
|
1228
|
+
});
|
|
1229
|
+
if (answer === undefined) {
|
|
1230
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1234
|
+
resetBoundaryEffectGuardInTransaction(database, job, options.now?.() ?? new Date().toISOString());
|
|
1235
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1236
|
+
runId: item.runId,
|
|
1237
|
+
dshSessionId: item.sessionId,
|
|
1238
|
+
resolution: 'manual_user',
|
|
1239
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1240
|
+
});
|
|
1241
|
+
}));
|
|
1242
|
+
return true;
|
|
1243
|
+
};
|
|
1116
1244
|
const confirmBoundary = async (item, state) => {
|
|
1117
1245
|
const confirmation = state.directive?.userFacingConfirmation;
|
|
1118
1246
|
if (confirmation === undefined || state.contractRevision === null)
|
|
@@ -1212,6 +1340,23 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1212
1340
|
return readBoundaryState(item);
|
|
1213
1341
|
},
|
|
1214
1342
|
validateBoundary: async ({ event }) => { await assertTurnBoundary(event); },
|
|
1343
|
+
requestLoopRecovery: async ({ event, state, automaticCount }) => {
|
|
1344
|
+
const item = currentForAgentEvent(event.agent.id, event.agent.sessionId, event.turn, event.agent.nativeSession, event.agent.nativeAgent);
|
|
1345
|
+
if (item === undefined)
|
|
1346
|
+
return undefined;
|
|
1347
|
+
const snapshot = await runtime.withDatabase((database) => readEnnoSnapshot(database, {
|
|
1348
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1349
|
+
}));
|
|
1350
|
+
const answer = await askForRecoveryInstruction({
|
|
1351
|
+
item,
|
|
1352
|
+
questionId: `legacy-loop-${item.runId.slice(0, 12)}`,
|
|
1353
|
+
title: 'Kiokuko stopped before a fourth identical automatic continuation.',
|
|
1354
|
+
detail: loopRecoveryDetail(snapshot, state, `The legacy turn controller continued the same instruction ${automaticCount} times without authoritative Enno progress.`),
|
|
1355
|
+
});
|
|
1356
|
+
if (answer === undefined)
|
|
1357
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1358
|
+
return answer;
|
|
1359
|
+
},
|
|
1215
1360
|
confirmUser: async ({ event, state }) => {
|
|
1216
1361
|
const item = currentForAgentEvent(event.agent.id, event.agent.sessionId, event.turn, event.agent.nativeSession, event.agent.nativeAgent);
|
|
1217
1362
|
if (item === undefined)
|
|
@@ -1249,7 +1394,43 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1249
1394
|
if (item === undefined || item.closed || item.runId !== job.runId || item.turn < job.nativeTurn) {
|
|
1250
1395
|
throw new Error('kiokuko-dsh boundary job has no exact live run binding');
|
|
1251
1396
|
}
|
|
1252
|
-
if (job.kind.
|
|
1397
|
+
if ((job.kind === 'confirmation' || job.kind === 'final_verification' || job.kind === 'advisory')
|
|
1398
|
+
&& !await guardBoundaryEffect(item, job)) {
|
|
1399
|
+
return { kind: 'waiting_user' };
|
|
1400
|
+
}
|
|
1401
|
+
if (job.kind.startsWith('retry_')) {
|
|
1402
|
+
return { kind: 'completed', nextKind: 'delivery' };
|
|
1403
|
+
}
|
|
1404
|
+
if (job.kind === 'ask_akinator') {
|
|
1405
|
+
const snapshot = await runtime.withDatabase((database) => readEnnoSnapshot(database, {
|
|
1406
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1407
|
+
}));
|
|
1408
|
+
const state = stateForSnapshot(snapshot);
|
|
1409
|
+
const pending = await runtime.withDatabase((database) => readPendingOutbox(database, item.sessionId)
|
|
1410
|
+
.find((candidate) => candidate.receiptId === job.receiptId));
|
|
1411
|
+
const validationFact = boundedMessageText(pending?.message);
|
|
1412
|
+
const answer = await askForRecoveryInstruction({
|
|
1413
|
+
item,
|
|
1414
|
+
questionId: `validation-${job.receiptId.slice(0, 16)}`,
|
|
1415
|
+
title: 'Kiokuko validation repeatedly failed and needs your instruction.',
|
|
1416
|
+
detail: loopRecoveryDetail(snapshot, state, validationFact ?? 'The same validation constraint was rejected repeatedly.'),
|
|
1417
|
+
});
|
|
1418
|
+
if (answer === undefined) {
|
|
1419
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1420
|
+
return { kind: 'waiting_user' };
|
|
1421
|
+
}
|
|
1422
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1423
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1424
|
+
runId: item.runId,
|
|
1425
|
+
dshSessionId: item.sessionId,
|
|
1426
|
+
resolution: 'manual_user',
|
|
1427
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1428
|
+
});
|
|
1429
|
+
const outbox = readPendingOutbox(database, item.sessionId).find((candidate) => candidate.receiptId === job.receiptId);
|
|
1430
|
+
if (outbox !== undefined) {
|
|
1431
|
+
replacePendingOutboxMessageInTransaction(database, job.receiptId, recoveryMessage(outbox.continuationId, answer), options.now?.() ?? new Date().toISOString());
|
|
1432
|
+
}
|
|
1433
|
+
}));
|
|
1253
1434
|
return { kind: 'completed', nextKind: 'delivery' };
|
|
1254
1435
|
}
|
|
1255
1436
|
if (job.kind === 'classify_boundary') {
|
|
@@ -1316,6 +1497,59 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1316
1497
|
}
|
|
1317
1498
|
catch { /* non-vetoing cache */ }
|
|
1318
1499
|
},
|
|
1500
|
+
beforeDelivery: async (job, outbox) => {
|
|
1501
|
+
if (isLoopRecoveryMessage(outbox.message))
|
|
1502
|
+
return 'deliver';
|
|
1503
|
+
const item = currentSession(job.dshSessionId);
|
|
1504
|
+
if (item === undefined || item.closed || item.runId !== job.runId)
|
|
1505
|
+
return 'superseded';
|
|
1506
|
+
const guarded = await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1507
|
+
const snapshot = readEnnoSnapshot(database, {
|
|
1508
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1509
|
+
});
|
|
1510
|
+
const state = stateForSnapshot(snapshot);
|
|
1511
|
+
const claim = claimAutomaticContinuationInTransaction(database, {
|
|
1512
|
+
claimId: outbox.continuationId,
|
|
1513
|
+
runId: item.runId,
|
|
1514
|
+
dshSessionId: item.sessionId,
|
|
1515
|
+
instructionDigest: ennoInstructionDigest(snapshot, state.directive),
|
|
1516
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1517
|
+
});
|
|
1518
|
+
const shouldAsk = claim.decision === 'wait_user'
|
|
1519
|
+
&& claimLoopRecoveryQuestionInTransaction(database, claim.claimId, options.now?.() ?? new Date().toISOString());
|
|
1520
|
+
return { snapshot, state, claim, shouldAsk };
|
|
1521
|
+
}));
|
|
1522
|
+
if (guarded.claim.decision === 'deliver')
|
|
1523
|
+
return 'deliver';
|
|
1524
|
+
if (!guarded.shouldAsk) {
|
|
1525
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1526
|
+
return 'waiting_user';
|
|
1527
|
+
}
|
|
1528
|
+
const answer = await askForRecoveryInstruction({
|
|
1529
|
+
item,
|
|
1530
|
+
questionId: `loop-${guarded.claim.claimId.slice(0, 16)}`,
|
|
1531
|
+
title: 'Kiokuko stopped before a fourth identical automatic continuation.',
|
|
1532
|
+
detail: loopRecoveryDetail(guarded.snapshot, guarded.state, [
|
|
1533
|
+
`The same instruction was automatically continued ${guarded.claim.ordinal - 1} times without authoritative Enno progress.`,
|
|
1534
|
+
boundedMessageText(outbox.message),
|
|
1535
|
+
].filter((value) => value !== undefined).join('\n')),
|
|
1536
|
+
});
|
|
1537
|
+
if (answer === undefined) {
|
|
1538
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1539
|
+
return 'waiting_user';
|
|
1540
|
+
}
|
|
1541
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1542
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1543
|
+
runId: item.runId,
|
|
1544
|
+
dshSessionId: item.sessionId,
|
|
1545
|
+
resolution: 'user_answer',
|
|
1546
|
+
claimId: guarded.claim.claimId,
|
|
1547
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1548
|
+
});
|
|
1549
|
+
replacePendingOutboxMessageInTransaction(database, job.receiptId, recoveryMessage(outbox.continuationId, answer), options.now?.() ?? new Date().toISOString());
|
|
1550
|
+
}));
|
|
1551
|
+
return 'deliver';
|
|
1552
|
+
},
|
|
1319
1553
|
dispatch: async (job, outbox) => {
|
|
1320
1554
|
const item = currentSession(job.dshSessionId);
|
|
1321
1555
|
const nativeAgent = (boundaryAgents.get(job.dshSessionId) ?? agents?.get(job.dshSessionId));
|
|
@@ -1324,6 +1558,42 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1324
1558
|
}
|
|
1325
1559
|
nativeAgent.steer(outbox.message);
|
|
1326
1560
|
},
|
|
1561
|
+
onWaitingUser: async (job, error) => {
|
|
1562
|
+
const item = currentSession(job.dshSessionId);
|
|
1563
|
+
if (item === undefined || item.closed || item.runId !== job.runId)
|
|
1564
|
+
return false;
|
|
1565
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1566
|
+
const snapshot = await runtime.withDatabase((database) => readEnnoSnapshot(database, {
|
|
1567
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1568
|
+
}));
|
|
1569
|
+
const answer = await askForRecoveryInstruction({
|
|
1570
|
+
item,
|
|
1571
|
+
questionId: `boundary-${job.jobId.slice(0, 16)}`,
|
|
1572
|
+
title: 'Kiokuko stopped after three boundary-processing failures.',
|
|
1573
|
+
detail: loopRecoveryDetail(snapshot, stateForSnapshot(snapshot), `Last boundary error: ${(error instanceof Error ? error.message : String(error)).slice(0, 2_000)}`),
|
|
1574
|
+
});
|
|
1575
|
+
if (answer === undefined)
|
|
1576
|
+
return false;
|
|
1577
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1578
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1579
|
+
runId: item.runId,
|
|
1580
|
+
dshSessionId: item.sessionId,
|
|
1581
|
+
resolution: 'manual_user',
|
|
1582
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1583
|
+
});
|
|
1584
|
+
const outbox = readPendingOutbox(database, item.sessionId).find((candidate) => candidate.receiptId === job.receiptId);
|
|
1585
|
+
if (outbox !== undefined) {
|
|
1586
|
+
replacePendingOutboxMessageInTransaction(database, job.receiptId, recoveryMessage(outbox.continuationId, answer), options.now?.() ?? new Date().toISOString());
|
|
1587
|
+
}
|
|
1588
|
+
database.prepare(`
|
|
1589
|
+
UPDATE dsh_boundary_jobs
|
|
1590
|
+
SET status = 'pending', attempt_count = 0, available_at = ?,
|
|
1591
|
+
last_error_code = NULL, last_error_message = NULL, updated_at = ?
|
|
1592
|
+
WHERE job_id = ? AND status = 'waiting_user'
|
|
1593
|
+
`).run(options.now?.() ?? new Date().toISOString(), options.now?.() ?? new Date().toISOString(), job.jobId);
|
|
1594
|
+
}));
|
|
1595
|
+
return true;
|
|
1596
|
+
},
|
|
1327
1597
|
});
|
|
1328
1598
|
const rehydrateBoundarySession = async (nativeAgent) => {
|
|
1329
1599
|
const nativeSession = nativeAgent.session ?? sessions?.get(nativeAgent.id);
|