kiokuko-dsh 0.1.21 → 0.1.23
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/client.cjs +23 -1
- package/dist/client.d.ts +9 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/dsh/boundary-worker.d.ts +8 -1
- package/dist/dsh/boundary-worker.d.ts.map +1 -1
- package/dist/dsh/boundary-worker.js +100 -22
- package/dist/dsh/boundary-worker.js.map +1 -1
- package/dist/dsh/completion-report.d.ts +25 -0
- package/dist/dsh/completion-report.d.ts.map +1 -0
- package/dist/dsh/completion-report.js +88 -0
- package/dist/dsh/completion-report.js.map +1 -0
- 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 +365 -19
- 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 +41 -20
- package/dist/dsh/tools.js.map +1 -1
- package/dist/dsh/turn-process.d.ts +15 -1
- package/dist/dsh/turn-process.d.ts.map +1 -1
- package/dist/dsh/turn-process.js +69 -11
- 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 +14 -0
- package/docs/dsh-plugin.md +24 -6
- package/migrations/004_dsh_loop_guard.sql +46 -0
- package/migrations/005_dsh_completion_recovery.sql +118 -0
- package/package.json +1 -1
- package/skills/kiokuko-enno-oduno/SKILL.md +1 -0
package/dist/dsh/host-adapter.js
CHANGED
|
@@ -31,11 +31,13 @@ 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, enqueueUnsubmittedTurn, 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
|
-
import { DshBoundaryWorker } from './boundary-worker.js';
|
|
37
|
+
import { abortable, DshBoundaryWorker } from './boundary-worker.js';
|
|
38
38
|
import { DshSessionLogExportService } from './session-log-export.js';
|
|
39
|
+
import { DshCompletionReporter } from './completion-report.js';
|
|
40
|
+
import { claimAutomaticContinuationInTransaction, claimBoundaryEffectInTransaction, claimLoopRecoveryQuestionInTransaction, ennoInstructionDigest, resetBoundaryEffectGuardInTransaction, resetLoopGuardForUserInTransaction, } from './loop-guard.js';
|
|
39
41
|
function sessionEventSource(value) {
|
|
40
42
|
const source = value;
|
|
41
43
|
if (typeof source?.snapshotEvents !== 'function') {
|
|
@@ -94,12 +96,49 @@ function isHumanMessage(value) {
|
|
|
94
96
|
function pluginContinuationId(value) {
|
|
95
97
|
const message = objectRecord(value);
|
|
96
98
|
const source = objectRecord(message?.source);
|
|
97
|
-
if (source?.kind !== 'plugin' || source.plugin !== 'kiokuko-dsh'
|
|
99
|
+
if (source?.kind !== 'plugin' || source.plugin !== 'kiokuko-dsh'
|
|
100
|
+
|| (source.form !== 'continuation' && source.form !== 'loop-recovery'))
|
|
98
101
|
return undefined;
|
|
99
102
|
return typeof source.deliveryId === 'string' && /^[0-9a-f]{64}$/u.test(source.deliveryId)
|
|
100
103
|
? source.deliveryId
|
|
101
104
|
: undefined;
|
|
102
105
|
}
|
|
106
|
+
function isLoopRecoveryMessage(value) {
|
|
107
|
+
const source = objectRecord(objectRecord(value)?.source);
|
|
108
|
+
return source?.kind === 'plugin' && source.plugin === 'kiokuko-dsh' && source.form === 'loop-recovery';
|
|
109
|
+
}
|
|
110
|
+
function recoveryMessage(continuationId, answer) {
|
|
111
|
+
return Object.freeze({
|
|
112
|
+
id: continuationId,
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: [{
|
|
115
|
+
type: 'text',
|
|
116
|
+
text: `The user reviewed the stopped Kiokuko loop and supplied this recovery instruction:\n\n${answer}`,
|
|
117
|
+
}],
|
|
118
|
+
source: { kind: 'plugin', plugin: 'kiokuko-dsh', form: 'loop-recovery', deliveryId: continuationId },
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
function boundedMessageText(value) {
|
|
122
|
+
const content = objectRecord(value)?.content;
|
|
123
|
+
if (!Array.isArray(content))
|
|
124
|
+
return undefined;
|
|
125
|
+
const block = content.map(objectRecord).find((candidate) => candidate?.type === 'text' && typeof candidate.text === 'string');
|
|
126
|
+
return typeof block?.text === 'string' ? block.text.slice(0, 2_000) : undefined;
|
|
127
|
+
}
|
|
128
|
+
function boundedUtf8Text(value, maximumBytes) {
|
|
129
|
+
if (Buffer.byteLength(value, 'utf8') <= maximumBytes)
|
|
130
|
+
return value;
|
|
131
|
+
let result = '';
|
|
132
|
+
let bytes = 0;
|
|
133
|
+
for (const point of value) {
|
|
134
|
+
const size = Buffer.byteLength(point, 'utf8');
|
|
135
|
+
if (bytes + size > maximumBytes)
|
|
136
|
+
break;
|
|
137
|
+
result += point;
|
|
138
|
+
bytes += size;
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
103
142
|
function eventContinuationId(data) {
|
|
104
143
|
const direct = pluginContinuationId(data);
|
|
105
144
|
if (direct !== undefined)
|
|
@@ -638,12 +677,23 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
638
677
|
}),
|
|
639
678
|
};
|
|
640
679
|
if (humanPresent) {
|
|
680
|
+
boundaryWorker.cancelSession(event.sessionId);
|
|
641
681
|
const previous = currentSession(event.sessionId);
|
|
642
682
|
const state = previous === undefined ? undefined : states.get(previous.runId);
|
|
643
|
-
if (
|
|
683
|
+
if (previous !== undefined) {
|
|
644
684
|
try {
|
|
645
685
|
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
646
|
-
|
|
686
|
+
if (state !== undefined) {
|
|
687
|
+
const now = options.now?.() ?? new Date().toISOString();
|
|
688
|
+
supersedeOutboxAtOrBeforeRevisionInTransaction(database, event.sessionId, state.revision, now);
|
|
689
|
+
supersedeBoundaryJobsAtOrBeforeRevisionInTransaction(database, event.sessionId, state.revision, now);
|
|
690
|
+
}
|
|
691
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
692
|
+
runId: previous.runId,
|
|
693
|
+
dshSessionId: event.sessionId,
|
|
694
|
+
resolution: 'manual_user',
|
|
695
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
696
|
+
});
|
|
647
697
|
}));
|
|
648
698
|
}
|
|
649
699
|
catch {
|
|
@@ -752,9 +802,12 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
752
802
|
terminalizeLedgerRunInTransaction(database, runId, 'cancelled');
|
|
753
803
|
return undefined;
|
|
754
804
|
}
|
|
805
|
+
const automaticMessage = event.nativeMessages?.find((message) => pluginContinuationId(message) !== undefined && !isLoopRecoveryMessage(message));
|
|
806
|
+
const automaticClaimId = automaticMessage === undefined ? undefined : pluginContinuationId(automaticMessage);
|
|
755
807
|
const decision = decideDshContinuation(database, {
|
|
756
808
|
dshSessionId: event.sessionId,
|
|
757
809
|
cwd: event.cwd,
|
|
810
|
+
...(automaticClaimId === undefined ? {} : { claimId: automaticClaimId }),
|
|
758
811
|
}, runId);
|
|
759
812
|
if (!decision.continue || decision.runId !== runId) {
|
|
760
813
|
throw new KiokukoError('CONFLICT', decision.warning ?? 'The active Enno-Oduno run cannot be resumed by this DSH session');
|
|
@@ -969,12 +1022,18 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
969
1022
|
workUnitId: binding.workUnitId ?? null,
|
|
970
1023
|
});
|
|
971
1024
|
if (phase !== undefined && receiptOperation !== undefined) {
|
|
1025
|
+
const executionAttempt = operation === 'enno_work_report'
|
|
1026
|
+
? await runtime.withDatabase(database => readEnnoSnapshot(database, {
|
|
1027
|
+
runId: run.runId, workspace: run.workspace, orchestrationId: run.orchestrationId,
|
|
1028
|
+
}).workUnits.find(unit => unit.workUnit.id === binding.workUnitId)?.attemptCount ?? 0)
|
|
1029
|
+
: 0;
|
|
972
1030
|
await runtime.withDatabase((database) => prepareTurnIntent(database, {
|
|
973
1031
|
runId: run.runId,
|
|
974
1032
|
dshSessionId: run.sessionId,
|
|
975
1033
|
nativeTurn: run.turn,
|
|
976
1034
|
phase,
|
|
977
1035
|
contractRevision: binding.revision,
|
|
1036
|
+
executionAttempt,
|
|
978
1037
|
...(binding.workUnitId === undefined ? {} : { workUnitId: binding.workUnitId }),
|
|
979
1038
|
inputDigest,
|
|
980
1039
|
operation: receiptOperation,
|
|
@@ -1101,7 +1160,8 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1101
1160
|
return response.ennoOduno;
|
|
1102
1161
|
};
|
|
1103
1162
|
const boundaryAgents = new Map();
|
|
1104
|
-
const
|
|
1163
|
+
const boundarySignals = new Map();
|
|
1164
|
+
const boundaryEvent = (item, signal = boundarySignals.get(item.sessionId) ?? new AbortController().signal) => ({
|
|
1105
1165
|
agent: {
|
|
1106
1166
|
id: item.agentId,
|
|
1107
1167
|
sessionId: item.sessionId,
|
|
@@ -1113,6 +1173,87 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1113
1173
|
signal,
|
|
1114
1174
|
});
|
|
1115
1175
|
const readBoundaryState = async (item) => (runtime.withDatabase((database) => stateForRun(database, item)));
|
|
1176
|
+
const askForRecoveryInstruction = async (input) => {
|
|
1177
|
+
const agent = input.item.nativeAgent ?? agents?.get(input.item.agentId);
|
|
1178
|
+
const signal = boundarySignals.get(input.item.sessionId) ?? new AbortController().signal;
|
|
1179
|
+
if (userQuestions === undefined || agent === undefined)
|
|
1180
|
+
return undefined;
|
|
1181
|
+
try {
|
|
1182
|
+
const result = await abortable(userQuestions.ask({
|
|
1183
|
+
questions: [{
|
|
1184
|
+
id: input.questionId,
|
|
1185
|
+
header: 'Kiokuko stopped',
|
|
1186
|
+
question: input.title,
|
|
1187
|
+
detail: input.detail,
|
|
1188
|
+
}],
|
|
1189
|
+
agent,
|
|
1190
|
+
signal,
|
|
1191
|
+
}), signal);
|
|
1192
|
+
signal.throwIfAborted();
|
|
1193
|
+
const answer = result.answers[0];
|
|
1194
|
+
if (answer === undefined || answer.id !== input.questionId)
|
|
1195
|
+
return undefined;
|
|
1196
|
+
const value = answer.custom?.trim() || answer.selected[0]?.trim();
|
|
1197
|
+
return value === undefined || value.length === 0 ? undefined : boundedUtf8Text(value, 8 * 1024);
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
signal.throwIfAborted();
|
|
1201
|
+
// A broken/dismissed question surface must never become another retry
|
|
1202
|
+
// loop. The durable waiting_user state remains the recovery boundary.
|
|
1203
|
+
return undefined;
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
const loopRecoveryDetail = (snapshot, state, reason) => {
|
|
1207
|
+
const workUnitId = state.directive?.workUnit?.id ?? null;
|
|
1208
|
+
const workUnit = workUnitId === null
|
|
1209
|
+
? null
|
|
1210
|
+
: snapshot.workUnits.find((candidate) => candidate.workUnit.id === workUnitId) ?? null;
|
|
1211
|
+
const latestVerifier = snapshot.finalEvidence.at(-1);
|
|
1212
|
+
return [
|
|
1213
|
+
`Host status: phase=${snapshot.status}; nextAction=${state.nextAction}; role=${state.currentRole ?? 'none'}.`,
|
|
1214
|
+
`Revision=${snapshot.revision}; mutationRevision=${snapshot.mutationRevision}; attempts=${snapshot.attempts}/${snapshot.contract.maxAttempts}.`,
|
|
1215
|
+
workUnit === null
|
|
1216
|
+
? 'WorkUnit: none.'
|
|
1217
|
+
: `WorkUnit ${workUnit.workUnit.id}: ${workUnit.workUnit.objective} (status=${workUnit.status}, attempts=${workUnit.attemptCount}).`,
|
|
1218
|
+
latestVerifier === undefined
|
|
1219
|
+
? 'Latest verifier: none.'
|
|
1220
|
+
: `Latest verifier ${latestVerifier.verifier.id}: ${latestVerifier.status}.`,
|
|
1221
|
+
reason,
|
|
1222
|
+
'Enter the actual current handling status and the concrete instruction to execute next. If work should stop, say so explicitly.',
|
|
1223
|
+
].join('\n');
|
|
1224
|
+
};
|
|
1225
|
+
const guardBoundaryEffect = async (item, job) => {
|
|
1226
|
+
const guarded = await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1227
|
+
const snapshot = readEnnoSnapshot(database, {
|
|
1228
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1229
|
+
});
|
|
1230
|
+
const state = stateForSnapshot(snapshot);
|
|
1231
|
+
const claim = claimBoundaryEffectInTransaction(database, job, ennoInstructionDigest(snapshot, state.directive), options.now?.() ?? new Date().toISOString());
|
|
1232
|
+
return { snapshot, state, claim };
|
|
1233
|
+
}));
|
|
1234
|
+
if (guarded.claim.decision === 'deliver')
|
|
1235
|
+
return true;
|
|
1236
|
+
const answer = await askForRecoveryInstruction({
|
|
1237
|
+
item,
|
|
1238
|
+
questionId: `effect-${job.jobId.slice(0, 16)}`,
|
|
1239
|
+
title: 'Kiokuko stopped before a fourth stateful boundary operation without progress.',
|
|
1240
|
+
detail: loopRecoveryDetail(guarded.snapshot, guarded.state, `${job.kind} completed or was re-entered three times without authoritative Enno progress.`),
|
|
1241
|
+
});
|
|
1242
|
+
if (answer === undefined) {
|
|
1243
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1247
|
+
resetBoundaryEffectGuardInTransaction(database, job, options.now?.() ?? new Date().toISOString());
|
|
1248
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1249
|
+
runId: item.runId,
|
|
1250
|
+
dshSessionId: item.sessionId,
|
|
1251
|
+
resolution: 'manual_user',
|
|
1252
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1253
|
+
});
|
|
1254
|
+
}));
|
|
1255
|
+
return true;
|
|
1256
|
+
};
|
|
1116
1257
|
const confirmBoundary = async (item, state) => {
|
|
1117
1258
|
const confirmation = state.directive?.userFacingConfirmation;
|
|
1118
1259
|
if (confirmation === undefined || state.contractRevision === null)
|
|
@@ -1128,6 +1269,7 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1128
1269
|
}).revision);
|
|
1129
1270
|
},
|
|
1130
1271
|
submit: async (answer) => {
|
|
1272
|
+
event.signal.throwIfAborted();
|
|
1131
1273
|
response = await runtime.withDatabase((database) => answerEnno(database, {
|
|
1132
1274
|
runId: item.runId,
|
|
1133
1275
|
workspace: item.workspace,
|
|
@@ -1180,6 +1322,7 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1180
1322
|
...(advisoryEvidence === undefined ? {} : { advisoryEvidence }),
|
|
1181
1323
|
runtime,
|
|
1182
1324
|
});
|
|
1325
|
+
event.signal.throwIfAborted();
|
|
1183
1326
|
for (const message of messages) {
|
|
1184
1327
|
agent.inject({ id: randomUUID(), role: 'user', content: [{ type: 'text', text: message.content }], source: { kind: 'plugin', plugin: 'kiokuko-dsh', form: 'instructions' } });
|
|
1185
1328
|
}
|
|
@@ -1212,6 +1355,23 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1212
1355
|
return readBoundaryState(item);
|
|
1213
1356
|
},
|
|
1214
1357
|
validateBoundary: async ({ event }) => { await assertTurnBoundary(event); },
|
|
1358
|
+
requestLoopRecovery: async ({ event, state, automaticCount }) => {
|
|
1359
|
+
const item = currentForAgentEvent(event.agent.id, event.agent.sessionId, event.turn, event.agent.nativeSession, event.agent.nativeAgent);
|
|
1360
|
+
if (item === undefined)
|
|
1361
|
+
return undefined;
|
|
1362
|
+
const snapshot = await runtime.withDatabase((database) => readEnnoSnapshot(database, {
|
|
1363
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1364
|
+
}));
|
|
1365
|
+
const answer = await askForRecoveryInstruction({
|
|
1366
|
+
item,
|
|
1367
|
+
questionId: `legacy-loop-${item.runId.slice(0, 12)}`,
|
|
1368
|
+
title: 'Kiokuko stopped before a fourth identical automatic continuation.',
|
|
1369
|
+
detail: loopRecoveryDetail(snapshot, state, `The legacy turn controller continued the same instruction ${automaticCount} times without authoritative Enno progress.`),
|
|
1370
|
+
});
|
|
1371
|
+
if (answer === undefined)
|
|
1372
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1373
|
+
return answer;
|
|
1374
|
+
},
|
|
1215
1375
|
confirmUser: async ({ event, state }) => {
|
|
1216
1376
|
const item = currentForAgentEvent(event.agent.id, event.agent.sessionId, event.turn, event.agent.nativeSession, event.agent.nativeAgent);
|
|
1217
1377
|
if (item === undefined)
|
|
@@ -1244,12 +1404,49 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1244
1404
|
}
|
|
1245
1405
|
boundaryAgents.set(sessionId, nativeAgent);
|
|
1246
1406
|
},
|
|
1247
|
-
process: async (job) => {
|
|
1407
|
+
process: async (job, signal) => {
|
|
1408
|
+
boundarySignals.set(job.dshSessionId, signal);
|
|
1248
1409
|
const item = currentSession(job.dshSessionId);
|
|
1249
1410
|
if (item === undefined || item.closed || item.runId !== job.runId || item.turn < job.nativeTurn) {
|
|
1250
1411
|
throw new Error('kiokuko-dsh boundary job has no exact live run binding');
|
|
1251
1412
|
}
|
|
1252
|
-
if (job.kind.
|
|
1413
|
+
if ((job.kind === 'confirmation' || job.kind === 'final_verification' || job.kind === 'advisory')
|
|
1414
|
+
&& !await guardBoundaryEffect(item, job)) {
|
|
1415
|
+
return { kind: 'waiting_user' };
|
|
1416
|
+
}
|
|
1417
|
+
if (job.kind.startsWith('retry_')) {
|
|
1418
|
+
return { kind: 'completed', nextKind: 'delivery' };
|
|
1419
|
+
}
|
|
1420
|
+
if (job.kind === 'ask_akinator') {
|
|
1421
|
+
const snapshot = await runtime.withDatabase((database) => readEnnoSnapshot(database, {
|
|
1422
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1423
|
+
}));
|
|
1424
|
+
const state = stateForSnapshot(snapshot);
|
|
1425
|
+
const pending = await runtime.withDatabase((database) => readPendingOutbox(database, item.sessionId)
|
|
1426
|
+
.find((candidate) => candidate.receiptId === job.receiptId));
|
|
1427
|
+
const validationFact = boundedMessageText(pending?.message);
|
|
1428
|
+
const answer = await askForRecoveryInstruction({
|
|
1429
|
+
item,
|
|
1430
|
+
questionId: `validation-${job.receiptId.slice(0, 16)}`,
|
|
1431
|
+
title: 'Kiokuko validation repeatedly failed and needs your instruction.',
|
|
1432
|
+
detail: loopRecoveryDetail(snapshot, state, validationFact ?? 'The same validation constraint was rejected repeatedly.'),
|
|
1433
|
+
});
|
|
1434
|
+
if (answer === undefined) {
|
|
1435
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1436
|
+
return { kind: 'waiting_user' };
|
|
1437
|
+
}
|
|
1438
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1439
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1440
|
+
runId: item.runId,
|
|
1441
|
+
dshSessionId: item.sessionId,
|
|
1442
|
+
resolution: 'manual_user',
|
|
1443
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1444
|
+
});
|
|
1445
|
+
const outbox = readPendingOutbox(database, item.sessionId).find((candidate) => candidate.receiptId === job.receiptId);
|
|
1446
|
+
if (outbox !== undefined) {
|
|
1447
|
+
replacePendingOutboxMessageInTransaction(database, job.receiptId, recoveryMessage(outbox.continuationId, answer), options.now?.() ?? new Date().toISOString());
|
|
1448
|
+
}
|
|
1449
|
+
}));
|
|
1253
1450
|
return { kind: 'completed', nextKind: 'delivery' };
|
|
1254
1451
|
}
|
|
1255
1452
|
if (job.kind === 'classify_boundary') {
|
|
@@ -1288,7 +1485,8 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1288
1485
|
const directive = state.directive?.advisoryRound;
|
|
1289
1486
|
if (directive === undefined)
|
|
1290
1487
|
return { kind: 'completed', nextKind: 'classify_boundary' };
|
|
1291
|
-
const result = await advisoryRunner.run({ directive, signal
|
|
1488
|
+
const result = await advisoryRunner.run({ directive, signal });
|
|
1489
|
+
signal.throwIfAborted();
|
|
1292
1490
|
await submitAdvisory(result, { event: boundaryEvent(item), state });
|
|
1293
1491
|
return { kind: 'completed', nextKind: 'classify_boundary' };
|
|
1294
1492
|
}
|
|
@@ -1316,6 +1514,60 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1316
1514
|
}
|
|
1317
1515
|
catch { /* non-vetoing cache */ }
|
|
1318
1516
|
},
|
|
1517
|
+
beforeDelivery: async (job, outbox, signal) => {
|
|
1518
|
+
boundarySignals.set(job.dshSessionId, signal);
|
|
1519
|
+
if (isLoopRecoveryMessage(outbox.message))
|
|
1520
|
+
return 'deliver';
|
|
1521
|
+
const item = currentSession(job.dshSessionId);
|
|
1522
|
+
if (item === undefined || item.closed || item.runId !== job.runId)
|
|
1523
|
+
return 'superseded';
|
|
1524
|
+
const guarded = await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1525
|
+
const snapshot = readEnnoSnapshot(database, {
|
|
1526
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1527
|
+
});
|
|
1528
|
+
const state = stateForSnapshot(snapshot);
|
|
1529
|
+
const claim = claimAutomaticContinuationInTransaction(database, {
|
|
1530
|
+
claimId: outbox.continuationId,
|
|
1531
|
+
runId: item.runId,
|
|
1532
|
+
dshSessionId: item.sessionId,
|
|
1533
|
+
instructionDigest: ennoInstructionDigest(snapshot, state.directive),
|
|
1534
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1535
|
+
});
|
|
1536
|
+
const shouldAsk = claim.decision === 'wait_user'
|
|
1537
|
+
&& claimLoopRecoveryQuestionInTransaction(database, claim.claimId, options.now?.() ?? new Date().toISOString());
|
|
1538
|
+
return { snapshot, state, claim, shouldAsk };
|
|
1539
|
+
}));
|
|
1540
|
+
if (guarded.claim.decision === 'deliver')
|
|
1541
|
+
return 'deliver';
|
|
1542
|
+
if (!guarded.shouldAsk) {
|
|
1543
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1544
|
+
return 'waiting_user';
|
|
1545
|
+
}
|
|
1546
|
+
const answer = await askForRecoveryInstruction({
|
|
1547
|
+
item,
|
|
1548
|
+
questionId: `loop-${guarded.claim.claimId.slice(0, 16)}`,
|
|
1549
|
+
title: 'Kiokuko stopped before a fourth identical automatic continuation.',
|
|
1550
|
+
detail: loopRecoveryDetail(guarded.snapshot, guarded.state, [
|
|
1551
|
+
`The same instruction was automatically continued ${guarded.claim.ordinal - 1} times without authoritative Enno progress.`,
|
|
1552
|
+
boundedMessageText(outbox.message),
|
|
1553
|
+
].filter((value) => value !== undefined).join('\n')),
|
|
1554
|
+
});
|
|
1555
|
+
if (answer === undefined) {
|
|
1556
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1557
|
+
return 'waiting_user';
|
|
1558
|
+
}
|
|
1559
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1560
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1561
|
+
runId: item.runId,
|
|
1562
|
+
dshSessionId: item.sessionId,
|
|
1563
|
+
resolution: 'user_answer',
|
|
1564
|
+
claimId: guarded.claim.claimId,
|
|
1565
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1566
|
+
});
|
|
1567
|
+
replacePendingOutboxMessageInTransaction(database, job.receiptId, recoveryMessage(outbox.continuationId, answer), options.now?.() ?? new Date().toISOString());
|
|
1568
|
+
}));
|
|
1569
|
+
return 'deliver';
|
|
1570
|
+
},
|
|
1319
1571
|
dispatch: async (job, outbox) => {
|
|
1320
1572
|
const item = currentSession(job.dshSessionId);
|
|
1321
1573
|
const nativeAgent = (boundaryAgents.get(job.dshSessionId) ?? agents?.get(job.dshSessionId));
|
|
@@ -1324,13 +1576,81 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1324
1576
|
}
|
|
1325
1577
|
nativeAgent.steer(outbox.message);
|
|
1326
1578
|
},
|
|
1579
|
+
onWaitingUser: async (job, error, signal) => {
|
|
1580
|
+
boundarySignals.set(job.dshSessionId, signal);
|
|
1581
|
+
const item = currentSession(job.dshSessionId);
|
|
1582
|
+
if (item === undefined || item.closed || item.runId !== job.runId)
|
|
1583
|
+
return false;
|
|
1584
|
+
await sessionMirror.markWaitingUser(item.sessionId);
|
|
1585
|
+
const snapshot = await runtime.withDatabase((database) => readEnnoSnapshot(database, {
|
|
1586
|
+
runId: item.runId, workspace: item.workspace, orchestrationId: item.orchestrationId,
|
|
1587
|
+
}));
|
|
1588
|
+
const answer = await askForRecoveryInstruction({
|
|
1589
|
+
item,
|
|
1590
|
+
questionId: `boundary-${job.jobId.slice(0, 16)}`,
|
|
1591
|
+
title: 'Kiokuko stopped after three boundary-processing failures.',
|
|
1592
|
+
detail: loopRecoveryDetail(snapshot, stateForSnapshot(snapshot), `Last boundary error: ${(error instanceof Error ? error.message : String(error)).slice(0, 2_000)}`),
|
|
1593
|
+
});
|
|
1594
|
+
if (answer === undefined)
|
|
1595
|
+
return false;
|
|
1596
|
+
await runtime.withDatabase((database) => withImmediateTransaction(database, () => {
|
|
1597
|
+
resetLoopGuardForUserInTransaction(database, {
|
|
1598
|
+
runId: item.runId,
|
|
1599
|
+
dshSessionId: item.sessionId,
|
|
1600
|
+
resolution: 'manual_user',
|
|
1601
|
+
...(options.now === undefined ? {} : { now: options.now() }),
|
|
1602
|
+
});
|
|
1603
|
+
const outbox = readPendingOutbox(database, item.sessionId).find((candidate) => candidate.receiptId === job.receiptId);
|
|
1604
|
+
if (outbox !== undefined) {
|
|
1605
|
+
replacePendingOutboxMessageInTransaction(database, job.receiptId, recoveryMessage(outbox.continuationId, answer), options.now?.() ?? new Date().toISOString());
|
|
1606
|
+
}
|
|
1607
|
+
database.prepare(`
|
|
1608
|
+
UPDATE dsh_boundary_jobs
|
|
1609
|
+
SET status = 'pending', attempt_count = 0, available_at = ?,
|
|
1610
|
+
last_error_code = NULL, last_error_message = NULL, updated_at = ?
|
|
1611
|
+
WHERE job_id = ? AND status = 'waiting_user'
|
|
1612
|
+
`).run(options.now?.() ?? new Date().toISOString(), options.now?.() ?? new Date().toISOString(), job.jobId);
|
|
1613
|
+
}));
|
|
1614
|
+
return true;
|
|
1615
|
+
},
|
|
1616
|
+
});
|
|
1617
|
+
const completionReporter = new DshCompletionReporter(runtime, async (session) => {
|
|
1618
|
+
if (sessions?.flush === undefined)
|
|
1619
|
+
throw new Error('Native report flush is unavailable');
|
|
1620
|
+
await sessions.flush(session);
|
|
1327
1621
|
});
|
|
1622
|
+
const deliverCompletionReport = async (session) => {
|
|
1623
|
+
if (session !== undefined && typeof session.append === 'function') {
|
|
1624
|
+
await completionReporter.deliver(session);
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1328
1627
|
const rehydrateBoundarySession = async (nativeAgent) => {
|
|
1329
1628
|
const nativeSession = nativeAgent.session ?? sessions?.get(nativeAgent.id);
|
|
1330
1629
|
const sessionId = nativeSession?.id;
|
|
1331
1630
|
const cwd = nativeSession?.header?.cwd;
|
|
1332
1631
|
if (sessionId === undefined || cwd === undefined || typeof nativeSession?.snapshotEvents !== 'function')
|
|
1333
1632
|
return;
|
|
1633
|
+
if (nativeAgent.status === 'running')
|
|
1634
|
+
return;
|
|
1635
|
+
await deliverCompletionReport(nativeSession);
|
|
1636
|
+
const terminal = await runtime.withDatabase(database => database.prepare(`
|
|
1637
|
+
SELECT report.run_id AS runId, report.native_turn AS nativeTurn, contract.status
|
|
1638
|
+
FROM dsh_completion_reports AS report JOIN ledger_runs AS run ON run.run_id = report.run_id
|
|
1639
|
+
JOIN enno_contracts AS contract ON contract.run_id = report.run_id
|
|
1640
|
+
WHERE report.dsh_session_id = ? AND report.status = 'delivered' AND run.status = 'active'
|
|
1641
|
+
AND contract.status IN ('completed', 'blocked', 'cancelled')
|
|
1642
|
+
`).all(sessionId));
|
|
1643
|
+
for (const restored of terminal) {
|
|
1644
|
+
if (currentSession(sessionId)?.runId === restored.runId)
|
|
1645
|
+
continue;
|
|
1646
|
+
const end = dshTurnBoundarySeq(nativeSession, restored.nativeTurn, 'end');
|
|
1647
|
+
if (end === undefined)
|
|
1648
|
+
continue;
|
|
1649
|
+
await runLifecycle.closeTurn({ runId: restored.runId,
|
|
1650
|
+
status: restored.status === 'completed' ? 'completed' : restored.status === 'cancelled' ? 'cancelled' : 'failed',
|
|
1651
|
+
sourceEndSeq: end,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1334
1654
|
const pending = await runtime.withDatabase((database) => database.prepare(`
|
|
1335
1655
|
SELECT receipt.run_id AS runId, receipt.native_turn AS nativeTurn,
|
|
1336
1656
|
intake.task_text AS task
|
|
@@ -1371,6 +1691,7 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1371
1691
|
void rehydrateBoundarySession(nativeAgent).catch(() => undefined);
|
|
1372
1692
|
}
|
|
1373
1693
|
const resolveIdleClose = async (agentId, sessionId, nativeSession, nativeAgent) => {
|
|
1694
|
+
await deliverCompletionReport(nativeSession);
|
|
1374
1695
|
const item = currentForAgentEvent(agentId, sessionId, undefined, nativeSession, nativeAgent);
|
|
1375
1696
|
if (item === undefined || item.closed)
|
|
1376
1697
|
return undefined;
|
|
@@ -1385,9 +1706,9 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1385
1706
|
if (item.prepared.intake.profile.taskType === 'chat')
|
|
1386
1707
|
return undefined;
|
|
1387
1708
|
if (state.status === 'completed' || state.nextAction === 'complete') {
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1709
|
+
// A provider failure while wording the final response cannot undo
|
|
1710
|
+
// already verified completion. The durable host report covers it.
|
|
1711
|
+
return { runId: item.runId, status: 'completed', terminalTurn: item.turn };
|
|
1391
1712
|
}
|
|
1392
1713
|
// Idle means the driver has no active work, not that Enno reached a
|
|
1393
1714
|
// terminal state. Keep only genuinely resumable active states open.
|
|
@@ -1403,14 +1724,18 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1403
1724
|
const item = currentSession(sessionId);
|
|
1404
1725
|
if (item === undefined || item.closed || item.nativeSession !== nativeSession)
|
|
1405
1726
|
return undefined;
|
|
1727
|
+
const state = await runtime.withDatabase((database) => stateForRun(database, item));
|
|
1728
|
+
if (state.status === 'completed') {
|
|
1729
|
+
await deliverCompletionReport(nativeSession);
|
|
1730
|
+
return { runId: item.runId, status: 'completed', terminalTurn: item.turn };
|
|
1731
|
+
}
|
|
1406
1732
|
if (item.failed)
|
|
1407
1733
|
return { runId: item.runId, status: 'failed' };
|
|
1408
|
-
const state = await runtime.withDatabase((database) => stateForRun(database, item));
|
|
1409
1734
|
if (state.status === 'cancelled')
|
|
1410
1735
|
return { runId: item.runId, status: 'cancelled' };
|
|
1411
1736
|
if (state.status === 'blocked' || state.nextAction === 'report_blocker')
|
|
1412
1737
|
return { runId: item.runId, status: 'failed' };
|
|
1413
|
-
if (item.prepared.intake.profile.taskType === 'chat' || state.
|
|
1738
|
+
if (item.prepared.intake.profile.taskType === 'chat' || state.nextAction === 'complete') {
|
|
1414
1739
|
return { runId: item.runId, status: 'completed', terminalTurn: item.turn };
|
|
1415
1740
|
}
|
|
1416
1741
|
return { runId: item.runId, status: 'cancelled' };
|
|
@@ -1578,6 +1903,27 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1578
1903
|
}
|
|
1579
1904
|
if (!turnEndedWithError || (!durableRecoverable && !locallyRecoverable))
|
|
1580
1905
|
inMemoryClaims.delete(claimKey);
|
|
1906
|
+
if (reason?.kind === 'completed' && item.prepared.ennoOduno.applicable) {
|
|
1907
|
+
await runtime.withDatabase(database => {
|
|
1908
|
+
const state = stateForRun(database, item);
|
|
1909
|
+
if (state.nextAction === 'complete' || state.nextAction === 'report_blocker'
|
|
1910
|
+
|| state.status === 'cancelled' || state.contractRevision === null)
|
|
1911
|
+
return;
|
|
1912
|
+
const phase = state.nextAction === 'submit_ideal' ? 'ideal'
|
|
1913
|
+
: state.nextAction === 'submit_plan' || state.nextAction === 'ask_user_confirmation' ? 'planning'
|
|
1914
|
+
: state.nextAction === 'execute_work_unit' ? 'work_unit'
|
|
1915
|
+
: state.nextAction === 'submit_meditation' ? 'meditation' : 'final_review';
|
|
1916
|
+
const operation = phase === 'ideal' ? 'ideal_submit' : phase === 'planning' ? 'plan_submit'
|
|
1917
|
+
: phase === 'work_unit' ? 'work_report' : phase === 'meditation' ? 'meditation_submit' : 'finish';
|
|
1918
|
+
enqueueUnsubmittedTurn(database, {
|
|
1919
|
+
runId: item.runId, dshSessionId: item.sessionId, nativeTurn: item.turn,
|
|
1920
|
+
phase, operation, contractRevision: state.contractRevision,
|
|
1921
|
+
...(state.directive?.workUnit?.id === undefined ? {} : { workUnitId: state.directive.workUnit.id }),
|
|
1922
|
+
inputDigest: canonicalContentHash({ nextAction: state.nextAction, source: 'unsubmitted_turn' }),
|
|
1923
|
+
idempotencyKey: `dsh-unsubmitted:${item.turn}`, nextAction: state.nextAction,
|
|
1924
|
+
});
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1581
1927
|
boundaryWorker.kick(item.sessionId, item.nativeAgent);
|
|
1582
1928
|
})().catch(() => {
|
|
1583
1929
|
// Observe-only DSH listeners must never veto the native event.
|
|
@@ -1680,19 +2026,18 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1680
2026
|
for (const item of remainingRuns) {
|
|
1681
2027
|
try {
|
|
1682
2028
|
let status;
|
|
1683
|
-
if (item.
|
|
1684
|
-
status = 'failed';
|
|
1685
|
-
else if (item.prepared.intake.profile.taskType === 'chat')
|
|
1686
|
-
status = 'cancelled';
|
|
2029
|
+
if (item.prepared.intake.profile.taskType === 'chat')
|
|
2030
|
+
status = item.failed ? 'failed' : 'cancelled';
|
|
1687
2031
|
else {
|
|
1688
2032
|
const state = await runtime.withDatabase((database) => stateForRun(database, item));
|
|
1689
2033
|
status = state.status === 'completed' || state.nextAction === 'complete'
|
|
1690
2034
|
? 'completed'
|
|
1691
|
-
: state.status === 'blocked' || state.nextAction === 'report_blocker'
|
|
2035
|
+
: item.failed || state.status === 'blocked' || state.nextAction === 'report_blocker'
|
|
1692
2036
|
? 'failed'
|
|
1693
2037
|
: 'cancelled';
|
|
1694
2038
|
}
|
|
1695
2039
|
if (status === 'completed') {
|
|
2040
|
+
await deliverCompletionReport(item.nativeSession);
|
|
1696
2041
|
if (item.nativeSession === undefined || sessions?.flush === undefined) {
|
|
1697
2042
|
throw new KiokukoError('CONFLICT', 'Completed DSH run requires its exact native session checkpoint');
|
|
1698
2043
|
}
|
|
@@ -1741,6 +2086,7 @@ export function createDshHostAdapter(ctx, options = {}) {
|
|
|
1741
2086
|
activeModeRequests.clear();
|
|
1742
2087
|
advisoryRounds.clear();
|
|
1743
2088
|
boundaryAgents.clear();
|
|
2089
|
+
boundarySignals.clear();
|
|
1744
2090
|
resumedTurns.clear();
|
|
1745
2091
|
resumedLeases.clear();
|
|
1746
2092
|
inMemoryClaims.clear();
|