kiokuko-dsh 0.2.26 → 0.2.28
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/context/replay-model-memory.d.ts +2 -2
- package/dist/context/replay-model-memory.d.ts.map +1 -1
- package/dist/context/replay-model-memory.js +70 -16
- package/dist/context/replay-model-memory.js.map +1 -1
- package/dist/dsh/agenticreplay-host.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-host.js +5 -1
- package/dist/dsh/agenticreplay-host.js.map +1 -1
- package/dist/dsh/agenticreplay-libraries.d.ts +1 -0
- package/dist/dsh/agenticreplay-libraries.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-libraries.js.map +1 -1
- package/dist/dsh/agenticreplay-recorder.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-recorder.js +22 -10
- package/dist/dsh/agenticreplay-recorder.js.map +1 -1
- package/dist/dsh/agenticreplay-security.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-security.js +40 -6
- package/dist/dsh/agenticreplay-security.js.map +1 -1
- package/dist/dsh/agenticreplay-types.d.ts +3 -0
- package/dist/dsh/agenticreplay-types.d.ts.map +1 -1
- package/dist/dsh/agenticreplay-types.js.map +1 -1
- package/dist/dsh/coding-workflow/host.d.ts +1 -0
- package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/host.js +10 -0
- package/dist/dsh/coding-workflow/host.js.map +1 -1
- package/dist/dsh/coding-workflow/service.d.ts +6 -2
- package/dist/dsh/coding-workflow/service.d.ts.map +1 -1
- package/dist/dsh/coding-workflow/service.js +6 -5
- package/dist/dsh/coding-workflow/service.js.map +1 -1
- package/dist/dsh/core/host.d.ts.map +1 -1
- package/dist/dsh/core/host.js +10 -1
- package/dist/dsh/core/host.js.map +1 -1
- package/dist/dsh/execution-support.d.ts.map +1 -1
- package/dist/dsh/execution-support.js +2 -0
- package/dist/dsh/execution-support.js.map +1 -1
- package/dist/dsh/host-adapter/lifecycle.d.ts +20 -18
- package/dist/dsh/host-adapter/lifecycle.d.ts.map +1 -1
- package/dist/dsh/host-adapter/lifecycle.js +298 -102
- package/dist/dsh/host-adapter/lifecycle.js.map +1 -1
- package/dist/dsh/host-adapter.d.ts +47 -47
- package/dist/dsh/host-adapter.d.ts.map +1 -1
- package/dist/dsh/host-adapter.js +1213 -592
- package/dist/dsh/host-adapter.js.map +1 -1
- package/dist/dsh/index.d.ts +42 -41
- package/dist/dsh/index.d.ts.map +1 -1
- package/dist/dsh/index.js +166 -92
- package/dist/dsh/index.js.map +1 -1
- package/dist/dsh/lifecycle-memory.d.ts +12 -7
- package/dist/dsh/lifecycle-memory.d.ts.map +1 -1
- package/dist/dsh/lifecycle-memory.js +606 -172
- package/dist/dsh/lifecycle-memory.js.map +1 -1
- package/dist/dsh/memory-application.d.ts +9 -9
- package/dist/dsh/memory-application.d.ts.map +1 -1
- package/dist/dsh/memory-application.js +448 -147
- package/dist/dsh/memory-application.js.map +1 -1
- package/dist/dsh/on-demand-intake.d.ts +13 -0
- package/dist/dsh/on-demand-intake.d.ts.map +1 -1
- package/dist/dsh/on-demand-intake.js +30 -7
- package/dist/dsh/on-demand-intake.js.map +1 -1
- package/dist/dsh/repository-learning-reflector.d.ts +58 -0
- package/dist/dsh/repository-learning-reflector.d.ts.map +1 -0
- package/dist/dsh/repository-learning-reflector.js +230 -0
- package/dist/dsh/repository-learning-reflector.js.map +1 -0
- package/dist/dsh/repository-learning.d.ts +34 -0
- package/dist/dsh/repository-learning.d.ts.map +1 -0
- package/dist/dsh/repository-learning.js +512 -0
- package/dist/dsh/repository-learning.js.map +1 -0
- package/dist/dsh/task-completion.d.ts +7 -7
- package/dist/dsh/task-completion.d.ts.map +1 -1
- package/dist/dsh/task-completion.js +554 -194
- package/dist/dsh/task-completion.js.map +1 -1
- package/dist/dsh/task-state-host.d.ts +10 -10
- package/dist/dsh/task-state-host.d.ts.map +1 -1
- package/dist/dsh/task-state-host.js +337 -145
- package/dist/dsh/task-state-host.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/locale/en.json +1 -1
- package/dist/locale/ja.json +1 -1
- package/dist/locale/ko.json +1 -1
- package/dist/locale/zh-CN.json +1 -1
- package/dist/memory/forget.d.ts +2 -2
- package/dist/memory/forget.d.ts.map +1 -1
- package/dist/memory/forget.js +175 -74
- package/dist/memory/forget.js.map +1 -1
- package/dist/memory/replay-reflection.d.ts +94 -0
- package/dist/memory/replay-reflection.d.ts.map +1 -0
- package/dist/memory/replay-reflection.js +149 -0
- package/dist/memory/replay-reflection.js.map +1 -0
- package/dist/memory/replay-review.d.ts +18 -12
- package/dist/memory/replay-review.d.ts.map +1 -1
- package/dist/memory/replay-review.js +107 -46
- package/dist/memory/replay-review.js.map +1 -1
- package/dist/memory/repository-learning.d.ts +106 -0
- package/dist/memory/repository-learning.d.ts.map +1 -0
- package/dist/memory/repository-learning.js +348 -0
- package/dist/memory/repository-learning.js.map +1 -0
- package/docs/agenticreplay-recording.md +20 -2
- package/docs/on-demand-intake.md +8 -2
- package/docs/repository-learning.md +109 -0
- package/migrations/048_repository_learning.sql +65 -0
- package/package.json +6 -1
|
@@ -1,31 +1,40 @@
|
|
|
1
|
-
import { randomUUID } from
|
|
2
|
-
import { realpathSync } from
|
|
3
|
-
import { z } from
|
|
4
|
-
import { TASK_TYPES } from
|
|
5
|
-
import { KiokukoError } from
|
|
6
|
-
import { withImmediateTransaction } from
|
|
7
|
-
import { executeIdempotentInTransaction } from
|
|
8
|
-
import { LedgerStore } from
|
|
9
|
-
import { resolveProjectWorkspaceReadOnly } from
|
|
10
|
-
import { canonicalContentHash } from
|
|
11
|
-
import { resolveGroundedIntakeProfile } from
|
|
12
|
-
import { DshRunIntakeService } from
|
|
13
|
-
import { stableLispCapabilities } from
|
|
14
|
-
import { observedTaskCompletionEvidence } from
|
|
15
|
-
import { assessTaskCompletion } from
|
|
16
|
-
import { stopTaskNativeChildren } from
|
|
17
|
-
import { stopTaskOwnedExecutions } from
|
|
18
|
-
import { isNativeSubagent } from
|
|
19
|
-
import { bindTaskRepository, findSessionTask, readTaskSnapshot, recordTaskEvidence, transitionTask, requestTaskStop, confirmTaskStop, transferTaskOwner, TASK_OPERATIONS, TaskSnapshotSchema } from
|
|
20
|
-
export const TASK_WORKFLOW_SERVICE =
|
|
21
|
-
export const TASK_STATE_EVENT =
|
|
22
|
-
export const TASK_OPERATION_TOOL =
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { TASK_TYPES } from "../akinator/types.js";
|
|
5
|
+
import { KiokukoError } from "../errors.js";
|
|
6
|
+
import { withImmediateTransaction } from "../db/transaction.js";
|
|
7
|
+
import { executeIdempotentInTransaction } from "./intake-idempotency.js";
|
|
8
|
+
import { LedgerStore } from "../ledger/store.js";
|
|
9
|
+
import { resolveProjectWorkspaceReadOnly } from "../memory/workspaces.js";
|
|
10
|
+
import { canonicalContentHash, } from "../serialization/validate.js";
|
|
11
|
+
import { resolveGroundedIntakeProfile } from "./intake-profile-resolver.js";
|
|
12
|
+
import { DshRunIntakeService } from "./run-intake-service.js";
|
|
13
|
+
import { stableLispCapabilities } from "./lisp-service-key.js";
|
|
14
|
+
import { observedTaskCompletionEvidence } from "./owned-completion-evidence.js";
|
|
15
|
+
import { assessTaskCompletion } from "./task-completion.js";
|
|
16
|
+
import { stopTaskNativeChildren } from "./native-task-authority.js";
|
|
17
|
+
import { stopTaskOwnedExecutions } from "./owned-execution.js";
|
|
18
|
+
import { isNativeSubagent } from "./native-subagent.js";
|
|
19
|
+
import { bindTaskRepository, findSessionTask, readTaskSnapshot, recordTaskEvidence, transitionTask, requestTaskStop, confirmTaskStop, transferTaskOwner, TASK_OPERATIONS, TaskSnapshotSchema, } from "./task-state.js";
|
|
20
|
+
export const TASK_WORKFLOW_SERVICE = "kiokukoTaskWorkflow";
|
|
21
|
+
export const TASK_STATE_EVENT = "kiokuko/task-state";
|
|
22
|
+
export const TASK_OPERATION_TOOL = "task_operation";
|
|
23
23
|
const text = z.string().trim().min(1).max(4096);
|
|
24
|
-
export const TaskModelOperationSchema = z.discriminatedUnion(
|
|
25
|
-
z
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
export const TaskModelOperationSchema = z.discriminatedUnion("operation", [
|
|
25
|
+
z
|
|
26
|
+
.object({ operation: z.literal("create"), taskType: z.enum(TASK_TYPES) })
|
|
27
|
+
.strict(),
|
|
28
|
+
...TASK_OPERATIONS.map((operation) => z
|
|
29
|
+
.object({
|
|
30
|
+
operation: z.literal(operation),
|
|
31
|
+
taskId: text,
|
|
32
|
+
expectedRevision: z.number().int().positive(),
|
|
33
|
+
expectedGeneration: z.number().int().positive(),
|
|
34
|
+
reason: text,
|
|
35
|
+
evidence: z.array(text.max(512)).max(32).default([]),
|
|
36
|
+
})
|
|
37
|
+
.strict()),
|
|
29
38
|
]);
|
|
30
39
|
/** Public adapter over SQLite. It keeps no authoritative task state in memory. */
|
|
31
40
|
export class DshTaskWorkflow {
|
|
@@ -38,21 +47,29 @@ export class DshTaskWorkflow {
|
|
|
38
47
|
this.options = options;
|
|
39
48
|
}
|
|
40
49
|
current(agent) {
|
|
41
|
-
if (this.#closed ||
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
if (this.#closed ||
|
|
51
|
+
!agent?.session ||
|
|
52
|
+
this.options.agents.get(agent.id) !== agent ||
|
|
53
|
+
this.options.sessions.get(agent.session.id) !== agent.session ||
|
|
54
|
+
isNativeSubagent(agent)) {
|
|
55
|
+
throw new KiokukoError("CONFLICT", "Task operation requires the exact current primary native agent and session");
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
async owner(agent) {
|
|
47
59
|
this.current(agent);
|
|
48
60
|
if (!agent.session.header?.cwd)
|
|
49
|
-
throw new KiokukoError(
|
|
61
|
+
throw new KiokukoError("CONFLICT", "Current native workspace is unavailable");
|
|
50
62
|
const cwd = realpathSync(agent.session.header.cwd);
|
|
51
63
|
const owner = await this.options.runtime.withDatabase(async (db) => {
|
|
52
|
-
const project = await resolveProjectWorkspaceReadOnly(db, cwd, {
|
|
64
|
+
const project = await resolveProjectWorkspaceReadOnly(db, cwd, {
|
|
65
|
+
allowDirectory: true,
|
|
66
|
+
});
|
|
53
67
|
if (!project)
|
|
54
|
-
throw new KiokukoError(
|
|
55
|
-
return {
|
|
68
|
+
throw new KiokukoError("CONFLICT", "Current native workspace is not registered");
|
|
69
|
+
return {
|
|
70
|
+
sessionId: agent.session.id,
|
|
71
|
+
repositoryRoot: project.repositoryRoot,
|
|
72
|
+
};
|
|
56
73
|
});
|
|
57
74
|
this.current(agent);
|
|
58
75
|
return owner;
|
|
@@ -60,17 +77,24 @@ export class DshTaskWorkflow {
|
|
|
60
77
|
async snapshot(agent) {
|
|
61
78
|
this.current(agent);
|
|
62
79
|
if (!agent.session.header?.cwd)
|
|
63
|
-
throw new KiokukoError(
|
|
80
|
+
throw new KiokukoError("CONFLICT", "Current native workspace is unavailable");
|
|
64
81
|
const cwd = realpathSync(agent.session.header.cwd);
|
|
65
82
|
const task = await this.options.runtime.withDatabase(async (db) => {
|
|
66
|
-
const project = await resolveProjectWorkspaceReadOnly(db, cwd, {
|
|
67
|
-
|
|
83
|
+
const project = await resolveProjectWorkspaceReadOnly(db, cwd, {
|
|
84
|
+
allowDirectory: true,
|
|
85
|
+
});
|
|
86
|
+
return project
|
|
87
|
+
? findSessionTask(db, {
|
|
88
|
+
sessionId: agent.session.id,
|
|
89
|
+
repositoryRoot: project.repositoryRoot,
|
|
90
|
+
}, { includeLatestTerminal: true })
|
|
91
|
+
: undefined;
|
|
68
92
|
});
|
|
69
93
|
this.current(agent);
|
|
70
94
|
return { sessionId: agent.session.id, task: task ?? null };
|
|
71
95
|
}
|
|
72
96
|
isControl(execution) {
|
|
73
|
-
if (!execution || typeof execution !==
|
|
97
|
+
if (!execution || typeof execution !== "object")
|
|
74
98
|
return false;
|
|
75
99
|
try {
|
|
76
100
|
this.current(execution.agent);
|
|
@@ -80,27 +104,35 @@ export class DshTaskWorkflow {
|
|
|
80
104
|
}
|
|
81
105
|
return this.options.demand?.isControl(execution) === true;
|
|
82
106
|
}
|
|
83
|
-
publish(task) {
|
|
84
|
-
this
|
|
85
|
-
|
|
86
|
-
|
|
107
|
+
publish(task) {
|
|
108
|
+
if (!this.#closed) {
|
|
109
|
+
this.options.demand?.taskStateChanged(task);
|
|
110
|
+
try {
|
|
111
|
+
this.options.emit(task);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
/* A UI observer cannot roll back a committed operation. */
|
|
115
|
+
}
|
|
87
116
|
}
|
|
88
|
-
|
|
89
|
-
}
|
|
117
|
+
return task;
|
|
118
|
+
}
|
|
90
119
|
/** Durable public-call receipts share the mutation's transaction. Reading a
|
|
91
120
|
* receipt still requires the exact current native owner and repository, but
|
|
92
121
|
* never re-publishes an old snapshot or restarts its lifecycle side effects. */
|
|
93
122
|
modelReceipt(db, owner, replay, operation) {
|
|
94
123
|
if (!replay)
|
|
95
124
|
return operation?.();
|
|
96
|
-
if (owner.sessionId !== replay.owner.sessionId ||
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
if (owner.sessionId !== replay.owner.sessionId ||
|
|
126
|
+
owner.repositoryRoot !== replay.owner.repositoryRoot)
|
|
127
|
+
throw new KiokukoError("CONFLICT", "Task owner or repository changed");
|
|
128
|
+
const absent = Symbol("no-model-operation-receipt");
|
|
99
129
|
let executed = false;
|
|
100
130
|
try {
|
|
101
131
|
return withImmediateTransaction(db, () => {
|
|
102
132
|
const result = TaskSnapshotSchema.parse(executeIdempotentInTransaction(db, {
|
|
103
|
-
scope: replay.scope,
|
|
133
|
+
scope: replay.scope,
|
|
134
|
+
key: replay.key,
|
|
135
|
+
request: replay.request,
|
|
104
136
|
}, () => {
|
|
105
137
|
if (!operation)
|
|
106
138
|
throw absent;
|
|
@@ -121,32 +153,76 @@ export class DshTaskWorkflow {
|
|
|
121
153
|
async create(agent, task, type, operationId, signal, replay) {
|
|
122
154
|
const owner = await this.owner(agent);
|
|
123
155
|
const request = text.max(16384).parse(task), taskType = z.enum(TASK_TYPES).parse(type);
|
|
124
|
-
const grounded = resolveGroundedIntakeProfile({
|
|
125
|
-
|
|
156
|
+
const grounded = resolveGroundedIntakeProfile({
|
|
157
|
+
task: request,
|
|
158
|
+
cwd: owner.repositoryRoot,
|
|
159
|
+
deferTaskTypeInference: true,
|
|
160
|
+
profileHints: { taskType },
|
|
161
|
+
});
|
|
162
|
+
const skills = await this.options.skills.snapshot({
|
|
163
|
+
scope: agent,
|
|
164
|
+
cwd: owner.repositoryRoot,
|
|
165
|
+
signal,
|
|
166
|
+
});
|
|
126
167
|
if (!skills.complete)
|
|
127
|
-
throw new KiokukoError(
|
|
128
|
-
const schemas = stableLispCapabilities(await this.options.tools.schemas(agent), agent);
|
|
129
|
-
const capabilities = [
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
throw new KiokukoError("CONFLICT", "Native capability inventory is incomplete");
|
|
169
|
+
const schemas = stableLispCapabilities((await this.options.tools.schemas(agent)), agent);
|
|
170
|
+
const capabilities = [
|
|
171
|
+
...skills.skills
|
|
172
|
+
.filter((skill) => skill.invocation?.modelInvocable !== false)
|
|
173
|
+
.map((skill) => ({
|
|
174
|
+
kind: "skill",
|
|
175
|
+
name: skill.name,
|
|
176
|
+
...(skill.description ? { description: skill.description } : {}),
|
|
177
|
+
})),
|
|
178
|
+
...schemas.map((tool) => ({
|
|
179
|
+
kind: "tool",
|
|
180
|
+
name: tool.name,
|
|
181
|
+
...(tool.description ? { description: tool.description } : {}),
|
|
182
|
+
})),
|
|
183
|
+
];
|
|
132
184
|
signal.throwIfAborted();
|
|
133
185
|
this.current(agent);
|
|
134
186
|
const result = await this.options.runtime.withDatabase(async (db) => {
|
|
135
187
|
const project = await resolveProjectWorkspaceReadOnly(db, owner.repositoryRoot, { allowDirectory: true });
|
|
136
188
|
if (!project)
|
|
137
|
-
throw new KiokukoError(
|
|
189
|
+
throw new KiokukoError("CONFLICT", "Task workspace changed");
|
|
138
190
|
this.current(agent);
|
|
139
191
|
signal.throwIfAborted();
|
|
140
192
|
return this.modelReceipt(db, owner, replay, () => {
|
|
141
193
|
const current = findSessionTask(db, owner);
|
|
142
|
-
if (current &&
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
if (current &&
|
|
195
|
+
new LedgerStore(db).readRun(current.runId)?.metadata.requestId !==
|
|
196
|
+
operationId)
|
|
197
|
+
throw new KiokukoError("CONFLICT", "A current task already exists; use an allowed operation or finish it before creating another");
|
|
198
|
+
const opened = new DshRunIntakeService(db, {
|
|
199
|
+
deferTaskTypeInference: true,
|
|
200
|
+
}).openRun({
|
|
201
|
+
idempotencyKey: operationId,
|
|
202
|
+
dshSessionId: owner.sessionId,
|
|
203
|
+
request: {
|
|
204
|
+
apiVersion: "1",
|
|
205
|
+
workspace: project.workspace,
|
|
206
|
+
task: {
|
|
207
|
+
title: request,
|
|
208
|
+
query: request,
|
|
209
|
+
profileHints: grounded.profileHints,
|
|
210
|
+
},
|
|
211
|
+
captureProfile: "minimal",
|
|
212
|
+
coverage: {
|
|
213
|
+
run: "unavailable",
|
|
214
|
+
tool: "unavailable",
|
|
215
|
+
command: "unavailable",
|
|
216
|
+
file: "unavailable",
|
|
217
|
+
approval: "unavailable",
|
|
218
|
+
},
|
|
219
|
+
capabilities,
|
|
220
|
+
metadata: {
|
|
221
|
+
taskStateExplicit: true,
|
|
222
|
+
requestId: operationId,
|
|
223
|
+
repositoryRoot: owner.repositoryRoot,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
150
226
|
});
|
|
151
227
|
return bindTaskRepository(db, { ...owner, taskId: opened.runId });
|
|
152
228
|
});
|
|
@@ -154,33 +230,57 @@ export class DshTaskWorkflow {
|
|
|
154
230
|
return replay?.replayed ? result : this.publish(result);
|
|
155
231
|
}
|
|
156
232
|
async operate(agent, operation, signal) {
|
|
157
|
-
return this.apply(agent, operation, signal, {
|
|
233
|
+
return this.apply(agent, operation, signal, {
|
|
234
|
+
explicit: true,
|
|
235
|
+
operationId: `task-ui:${randomUUID()}`,
|
|
236
|
+
});
|
|
158
237
|
}
|
|
159
238
|
async modelOperation(agent, raw, execution) {
|
|
160
239
|
const input = TaskModelOperationSchema.parse(raw);
|
|
161
|
-
const operation = input.operation ===
|
|
162
|
-
? {
|
|
163
|
-
|
|
164
|
-
|
|
240
|
+
const operation = input.operation === "create"
|
|
241
|
+
? {
|
|
242
|
+
operation: "create",
|
|
243
|
+
taskType: input.taskType,
|
|
244
|
+
request: this.options.demand?.controlTask(execution) ?? "",
|
|
245
|
+
}
|
|
246
|
+
: {
|
|
247
|
+
operation: input.operation,
|
|
248
|
+
reason: input.reason,
|
|
249
|
+
evidence: input.evidence,
|
|
250
|
+
expected: {
|
|
251
|
+
taskId: input.taskId,
|
|
252
|
+
revision: input.expectedRevision,
|
|
253
|
+
generation: input.expectedGeneration,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
165
256
|
execution.signal.throwIfAborted();
|
|
166
257
|
const owner = await this.owner(agent);
|
|
167
|
-
const replay = {
|
|
168
|
-
|
|
169
|
-
|
|
258
|
+
const replay = {
|
|
259
|
+
scope: `task-model:${canonicalContentHash(owner.sessionId)}`,
|
|
260
|
+
key: `task-tool:${execution.callId}`,
|
|
261
|
+
owner,
|
|
262
|
+
request: { owner, operation },
|
|
263
|
+
replayed: false,
|
|
264
|
+
};
|
|
265
|
+
const previous = await this.options.runtime.withDatabase((db) => this.modelReceipt(db, owner, replay));
|
|
170
266
|
this.current(agent);
|
|
171
267
|
execution.signal.throwIfAborted();
|
|
172
268
|
if (previous)
|
|
173
269
|
return previous;
|
|
174
270
|
let result;
|
|
175
271
|
try {
|
|
176
|
-
result = await this.apply(agent, operation, execution.signal, {
|
|
272
|
+
result = await this.apply(agent, operation, execution.signal, {
|
|
273
|
+
explicit: false,
|
|
274
|
+
operationId: replay.key,
|
|
275
|
+
replay,
|
|
276
|
+
});
|
|
177
277
|
}
|
|
178
278
|
catch (error) {
|
|
179
279
|
// A concurrent identical call can commit between our receipt lookup and
|
|
180
280
|
// the ordinary CAS check. Recover only its exact durable receipt.
|
|
181
281
|
execution.signal.throwIfAborted();
|
|
182
282
|
const currentOwner = await this.owner(agent);
|
|
183
|
-
const committed = await this.options.runtime.withDatabase(db => this.modelReceipt(db, currentOwner, replay));
|
|
283
|
+
const committed = await this.options.runtime.withDatabase((db) => this.modelReceipt(db, currentOwner, replay));
|
|
184
284
|
this.current(agent);
|
|
185
285
|
execution.signal.throwIfAborted();
|
|
186
286
|
if (!committed)
|
|
@@ -193,78 +293,117 @@ export class DshTaskWorkflow {
|
|
|
193
293
|
}
|
|
194
294
|
async apply(agent, operation, signal, source) {
|
|
195
295
|
signal.throwIfAborted();
|
|
196
|
-
if (operation.operation ===
|
|
197
|
-
throw new KiokukoError(
|
|
198
|
-
if (operation.operation ===
|
|
296
|
+
if (operation.operation === "ignore" && !operation.reason?.trim())
|
|
297
|
+
throw new KiokukoError("VALIDATION_ERROR", "Ignore requires an explicit reason");
|
|
298
|
+
if (operation.operation === "create")
|
|
199
299
|
return this.create(agent, operation.request, operation.taskType, source.operationId, signal, source.replay);
|
|
200
300
|
const owner = await this.owner(agent);
|
|
201
|
-
const task = await this.options.runtime.withDatabase(db => operation.expected || operation.taskId
|
|
202
|
-
? readTaskSnapshot(db, {
|
|
301
|
+
const task = await this.options.runtime.withDatabase((db) => operation.expected || operation.taskId
|
|
302
|
+
? readTaskSnapshot(db, {
|
|
303
|
+
...owner,
|
|
304
|
+
taskId: operation.expected?.taskId ?? operation.taskId,
|
|
305
|
+
})
|
|
203
306
|
: findSessionTask(db, owner, { includeLatestTerminal: true }));
|
|
204
307
|
if (!task)
|
|
205
|
-
throw new KiokukoError(
|
|
206
|
-
if (operation.expected &&
|
|
207
|
-
|
|
208
|
-
|
|
308
|
+
throw new KiokukoError("CONFLICT", "No task is selected for this session");
|
|
309
|
+
if (operation.expected &&
|
|
310
|
+
(operation.expected.revision !== task.revision ||
|
|
311
|
+
(operation.expected.generation !== undefined &&
|
|
312
|
+
operation.expected.generation !== task.generation)))
|
|
313
|
+
throw new KiokukoError("CONFLICT", "Task changed; read the current snapshot before operating");
|
|
209
314
|
const identity = { ...owner, taskId: task.taskId };
|
|
210
|
-
const input = {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
315
|
+
const input = {
|
|
316
|
+
operation: operation.operation,
|
|
317
|
+
operationId: source.operationId,
|
|
318
|
+
expectedRevision: task.revision,
|
|
319
|
+
expectedGeneration: task.generation,
|
|
320
|
+
reason: operation.reason ?? `Explicit ${operation.operation} operation`,
|
|
321
|
+
evidence: [...(operation.evidence ?? [])],
|
|
322
|
+
explicit: source.explicit,
|
|
323
|
+
};
|
|
324
|
+
if (input.operation === "complete") {
|
|
214
325
|
const lisp = this.options.lisp?.();
|
|
215
|
-
const idle = lisp?.taskJobsIdle
|
|
216
|
-
|
|
326
|
+
const idle = lisp?.taskJobsIdle
|
|
327
|
+
? await lisp.taskJobsIdle(agent)
|
|
328
|
+
: !(await this.options.runtime.withDatabase((db) => db
|
|
329
|
+
.prepare("SELECT 1 FROM dsh_lisp_sessions WHERE session_id=? AND enabled=1")
|
|
330
|
+
.get(task.sessionId)));
|
|
217
331
|
if (!idle)
|
|
218
|
-
throw new KiokukoError(
|
|
332
|
+
throw new KiokukoError("CONFLICT", "Task still has running or unconfirmed Lisp work");
|
|
219
333
|
}
|
|
220
334
|
this.current(agent);
|
|
221
335
|
signal.throwIfAborted();
|
|
222
|
-
const result = await this.options.runtime.withDatabase(db => withImmediateTransaction(db, () => {
|
|
336
|
+
const result = await this.options.runtime.withDatabase((db) => withImmediateTransaction(db, () => {
|
|
223
337
|
this.current(agent);
|
|
224
338
|
signal.throwIfAborted();
|
|
225
339
|
return this.modelReceipt(db, owner, source.replay, () => {
|
|
226
|
-
if (input.operation ===
|
|
340
|
+
if (input.operation === "complete") {
|
|
227
341
|
const lisp = this.options.lisp?.();
|
|
228
|
-
if (lisp?.taskJobsIdleSync
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
342
|
+
if (lisp?.taskJobsIdleSync
|
|
343
|
+
? !lisp.taskJobsIdleSync(agent)
|
|
344
|
+
: !!db
|
|
345
|
+
.prepare("SELECT 1 FROM dsh_lisp_sessions WHERE session_id=? AND enabled=1")
|
|
346
|
+
.get(task.sessionId))
|
|
347
|
+
throw new KiokukoError("CONFLICT", "Task still has running or unconfirmed Lisp work");
|
|
348
|
+
if (db
|
|
349
|
+
.prepare("SELECT 1 FROM dsh_owned_operations WHERE run_id=? AND state='started' LIMIT 1")
|
|
350
|
+
.get(task.taskId))
|
|
351
|
+
throw new KiokukoError("CONFLICT", "Task still has running owned work; collect its terminal result before completion");
|
|
233
352
|
const assessment = assessTaskCompletion(db, task.runId);
|
|
234
353
|
if (assessment.verified) {
|
|
235
354
|
const ref = `verification:${canonicalContentHash({ taskId: task.taskId, generation: task.generation, assessment })}`;
|
|
236
|
-
recordTaskEvidence(db, identity, {
|
|
355
|
+
recordTaskEvidence(db, identity, {
|
|
356
|
+
ref,
|
|
357
|
+
kind: "verification",
|
|
358
|
+
generation: task.generation,
|
|
359
|
+
verified: true,
|
|
360
|
+
});
|
|
237
361
|
input.evidence.push(ref);
|
|
238
362
|
}
|
|
239
363
|
else if (!assessment.coding && assessment.required === 0) {
|
|
240
364
|
const observed = observedTaskCompletionEvidence(db, task, input.evidence);
|
|
241
365
|
if (observed.length > 0) {
|
|
242
366
|
const ref = `verification:${canonicalContentHash({ taskId: task.taskId, generation: task.generation, observed })}`;
|
|
243
|
-
recordTaskEvidence(db, identity, {
|
|
244
|
-
|
|
367
|
+
recordTaskEvidence(db, identity, {
|
|
368
|
+
ref,
|
|
369
|
+
kind: "verification",
|
|
370
|
+
generation: task.generation,
|
|
371
|
+
verified: true,
|
|
372
|
+
details: {
|
|
373
|
+
source: "explicit-completion",
|
|
374
|
+
observationRefs: observed,
|
|
375
|
+
},
|
|
376
|
+
});
|
|
245
377
|
input.evidence.push(ref);
|
|
246
378
|
}
|
|
247
379
|
}
|
|
248
380
|
}
|
|
249
|
-
return task.state ===
|
|
250
|
-
|
|
381
|
+
return task.state === "Working" &&
|
|
382
|
+
(input.operation === "wait" || input.operation === "ignore")
|
|
383
|
+
? requestTaskStop(db, identity, input)
|
|
384
|
+
: transitionTask(db, identity, input, {
|
|
385
|
+
...(this.sourceEndSeq(agent) === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: { sourceEndSeq: this.sourceEndSeq(agent) }),
|
|
388
|
+
});
|
|
251
389
|
});
|
|
252
390
|
}));
|
|
253
391
|
if (source.replay?.replayed)
|
|
254
392
|
return result;
|
|
255
393
|
this.publish(result);
|
|
256
394
|
if (result.pendingStop) {
|
|
257
|
-
if (source.explicit && agent.status !==
|
|
258
|
-
agent.cancel?.({ kind:
|
|
395
|
+
if (source.explicit && agent.status !== "idle")
|
|
396
|
+
agent.cancel?.({ kind: "hook", reason: "Explicit Kiokuko task stop" });
|
|
259
397
|
this.finishStop(agent, result);
|
|
260
398
|
}
|
|
261
|
-
if (result.state ===
|
|
399
|
+
if (result.state === "Done")
|
|
262
400
|
this.finishCompletion(agent, result);
|
|
263
401
|
return result;
|
|
264
402
|
}
|
|
265
403
|
sourceEndSeq(agent) {
|
|
266
404
|
const seq = Number.isSafeInteger(agent.session.seq) && agent.session.seq > 0
|
|
267
|
-
? agent.session.seq - 1
|
|
405
|
+
? agent.session.seq - 1
|
|
406
|
+
: agent.session.snapshotEvents?.().at(-1)?.seq;
|
|
268
407
|
return Number.isSafeInteger(seq) && seq >= 0 ? seq : undefined;
|
|
269
408
|
}
|
|
270
409
|
async stopWork(agent, task, signal) {
|
|
@@ -274,10 +413,11 @@ export class DshTaskWorkflow {
|
|
|
274
413
|
const assertCurrent = async () => {
|
|
275
414
|
signal.throwIfAborted();
|
|
276
415
|
this.current(agent);
|
|
277
|
-
await this.options.runtime.withDatabase(db => {
|
|
416
|
+
await this.options.runtime.withDatabase((db) => {
|
|
278
417
|
const current = readTaskSnapshot(db, task);
|
|
279
|
-
if (current.revision !== task.revision ||
|
|
280
|
-
|
|
418
|
+
if (current.revision !== task.revision ||
|
|
419
|
+
current.generation !== task.generation)
|
|
420
|
+
throw new KiokukoError("CONFLICT", "Task changed before stopping its work");
|
|
281
421
|
});
|
|
282
422
|
signal.throwIfAborted();
|
|
283
423
|
this.current(agent);
|
|
@@ -289,12 +429,14 @@ export class DshTaskWorkflow {
|
|
|
289
429
|
else {
|
|
290
430
|
// During unload the service may already be unavailable while workers drain.
|
|
291
431
|
// A persisted enabled session cannot be assumed stopped from service absence.
|
|
292
|
-
const enabled = await this.options.runtime.withDatabase(db => db
|
|
432
|
+
const enabled = await this.options.runtime.withDatabase((db) => db
|
|
433
|
+
.prepare("SELECT 1 FROM dsh_lisp_sessions WHERE session_id=? AND enabled=1")
|
|
434
|
+
.get(task.sessionId));
|
|
293
435
|
if (enabled)
|
|
294
436
|
return false;
|
|
295
437
|
}
|
|
296
438
|
await assertCurrent();
|
|
297
|
-
if (!await stopTaskNativeChildren(this.options.runtime, task, signal))
|
|
439
|
+
if (!(await stopTaskNativeChildren(this.options.runtime, task, signal)))
|
|
298
440
|
return false;
|
|
299
441
|
await assertCurrent();
|
|
300
442
|
return stopTaskOwnedExecutions(this.options.runtime, task, signal);
|
|
@@ -309,95 +451,145 @@ export class DshTaskWorkflow {
|
|
|
309
451
|
if (!stopped || !agent.whenIdle)
|
|
310
452
|
return;
|
|
311
453
|
await agent.whenIdle();
|
|
312
|
-
if (agent.status !==
|
|
454
|
+
if (agent.status !== "idle")
|
|
313
455
|
return;
|
|
314
456
|
this.current(agent);
|
|
315
457
|
await this.options.checkpointSource?.(agent);
|
|
316
458
|
this.current(agent);
|
|
317
|
-
const result = await this.options.runtime.withDatabase(db => withImmediateTransaction(db, () => {
|
|
459
|
+
const result = await this.options.runtime.withDatabase((db) => withImmediateTransaction(db, () => {
|
|
318
460
|
this.current(agent);
|
|
319
|
-
if (agent.status !==
|
|
461
|
+
if (agent.status !== "idle")
|
|
320
462
|
return undefined;
|
|
321
463
|
const current = readTaskSnapshot(db, task);
|
|
322
464
|
if (current.pendingStop?.operationId !== operationId)
|
|
323
465
|
return undefined;
|
|
324
466
|
const ref = `stopped:${operationId}:${current.generation}`;
|
|
325
|
-
recordTaskEvidence(db, task, {
|
|
326
|
-
|
|
467
|
+
recordTaskEvidence(db, task, {
|
|
468
|
+
ref,
|
|
469
|
+
kind: "stopped",
|
|
470
|
+
generation: current.generation,
|
|
471
|
+
verified: true,
|
|
472
|
+
});
|
|
473
|
+
const confirmed = confirmTaskStop(db, task, {
|
|
474
|
+
operationId,
|
|
475
|
+
expectedRevision: current.revision,
|
|
476
|
+
evidenceRef: ref,
|
|
477
|
+
});
|
|
327
478
|
return confirmed;
|
|
328
479
|
}));
|
|
329
480
|
if (result)
|
|
330
481
|
this.publish(result);
|
|
331
|
-
})()
|
|
482
|
+
})()
|
|
483
|
+
.catch(() => {
|
|
484
|
+
/* Preserve the durable unresolved stop and its denied authority. */
|
|
485
|
+
})
|
|
486
|
+
.finally(() => this.#stops.delete(task.taskId));
|
|
332
487
|
this.#stops.set(task.taskId, promise);
|
|
333
488
|
}
|
|
334
489
|
/** Wait for the final native answer, then seal only this exact Done generation. */
|
|
335
490
|
finishCompletion(agent, task) {
|
|
336
491
|
const key = `${task.taskId}:${task.revision}:${task.generation}`;
|
|
337
|
-
if (task.state !==
|
|
492
|
+
if (task.state !== "Done" ||
|
|
493
|
+
this.#closed ||
|
|
494
|
+
this.#completions.has(key) ||
|
|
495
|
+
!agent.whenIdle)
|
|
338
496
|
return;
|
|
339
497
|
const promise = (async () => {
|
|
340
498
|
await agent.whenIdle();
|
|
341
499
|
this.current(agent);
|
|
342
|
-
if (agent.status !==
|
|
500
|
+
if (agent.status !== "idle")
|
|
343
501
|
return;
|
|
344
502
|
await this.options.checkpointSource?.(agent);
|
|
345
503
|
this.current(agent);
|
|
346
|
-
const completed = await this.options.runtime.withDatabase(db => withImmediateTransaction(db, () => {
|
|
504
|
+
const completed = await this.options.runtime.withDatabase((db) => withImmediateTransaction(db, () => {
|
|
347
505
|
this.current(agent);
|
|
348
|
-
if (agent.status !==
|
|
506
|
+
if (agent.status !== "idle")
|
|
349
507
|
return false;
|
|
350
508
|
const current = readTaskSnapshot(db, task);
|
|
351
|
-
if (current.state !==
|
|
509
|
+
if (current.state !== "Done" ||
|
|
510
|
+
current.revision !== task.revision ||
|
|
511
|
+
current.generation !== task.generation)
|
|
352
512
|
return false;
|
|
353
513
|
return true;
|
|
354
514
|
}));
|
|
355
515
|
if (completed)
|
|
356
516
|
try {
|
|
357
|
-
this.options.onCompleted?.(task);
|
|
517
|
+
await this.options.onCompleted?.(task);
|
|
358
518
|
}
|
|
359
|
-
catch {
|
|
360
|
-
|
|
519
|
+
catch {
|
|
520
|
+
/* A later idle callback can retry the exact Done generation. */
|
|
521
|
+
}
|
|
522
|
+
})()
|
|
523
|
+
.catch(() => {
|
|
524
|
+
/* A later native idle/reload retries the same canonical Done state. */
|
|
525
|
+
})
|
|
361
526
|
.finally(() => this.#completions.delete(key));
|
|
362
527
|
this.#completions.set(key, promise);
|
|
363
528
|
}
|
|
364
529
|
async handoff(source, target, expected, signal) {
|
|
365
530
|
const sourceOwner = await this.owner(source), targetOwner = await this.owner(target);
|
|
366
|
-
if (sourceOwner.repositoryRoot !== targetOwner.repositoryRoot ||
|
|
367
|
-
|
|
368
|
-
|
|
531
|
+
if (sourceOwner.repositoryRoot !== targetOwner.repositoryRoot ||
|
|
532
|
+
target.session.header?.parentSession !== source.session.id)
|
|
533
|
+
throw new KiokukoError("CONFLICT", "Task handoff requires a primary continuation in the same repository");
|
|
534
|
+
const selected = await this.options.runtime.withDatabase((db) => {
|
|
369
535
|
signal.throwIfAborted();
|
|
370
536
|
this.current(source);
|
|
371
537
|
this.current(target);
|
|
372
|
-
const task = readTaskSnapshot(db, {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
538
|
+
const task = readTaskSnapshot(db, {
|
|
539
|
+
...sourceOwner,
|
|
540
|
+
taskId: expected.taskId,
|
|
541
|
+
});
|
|
542
|
+
if (task.revision !== expected.revision ||
|
|
543
|
+
(expected.generation !== undefined &&
|
|
544
|
+
task.generation !== expected.generation) ||
|
|
545
|
+
task.pendingStop)
|
|
546
|
+
throw new KiokukoError("CONFLICT", "Task changed before handoff or its stop is unresolved");
|
|
376
547
|
return task;
|
|
377
548
|
});
|
|
378
549
|
const stopped = await this.stopWork(source, selected, signal);
|
|
379
|
-
if (!stopped || source.status !==
|
|
380
|
-
throw new KiokukoError(
|
|
550
|
+
if (!stopped || source.status !== "idle")
|
|
551
|
+
throw new KiokukoError("CONFLICT", "Task source has not confirmed its stop");
|
|
381
552
|
signal.throwIfAborted();
|
|
382
553
|
this.current(source);
|
|
383
554
|
this.current(target);
|
|
384
|
-
const result = await this.options.runtime.withDatabase(db => withImmediateTransaction(db, () => {
|
|
555
|
+
const result = await this.options.runtime.withDatabase((db) => withImmediateTransaction(db, () => {
|
|
385
556
|
signal.throwIfAborted();
|
|
386
557
|
this.current(source);
|
|
387
558
|
this.current(target);
|
|
388
|
-
if (source.status !==
|
|
389
|
-
throw new KiokukoError(
|
|
390
|
-
const task = readTaskSnapshot(db, {
|
|
391
|
-
|
|
392
|
-
|
|
559
|
+
if (source.status !== "idle")
|
|
560
|
+
throw new KiokukoError("CONFLICT", "Task source resumed before handoff");
|
|
561
|
+
const task = readTaskSnapshot(db, {
|
|
562
|
+
...sourceOwner,
|
|
563
|
+
taskId: expected.taskId,
|
|
564
|
+
});
|
|
565
|
+
if (task.revision !== expected.revision ||
|
|
566
|
+
(expected.generation !== undefined &&
|
|
567
|
+
task.generation !== expected.generation))
|
|
568
|
+
throw new KiokukoError("CONFLICT", "Task changed before handoff");
|
|
393
569
|
const ref = `handoff-stop:${randomUUID()}`;
|
|
394
|
-
recordTaskEvidence(db, task, {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
570
|
+
recordTaskEvidence(db, task, {
|
|
571
|
+
ref,
|
|
572
|
+
kind: "stopped",
|
|
573
|
+
generation: task.generation,
|
|
574
|
+
verified: true,
|
|
575
|
+
});
|
|
576
|
+
return transferTaskOwner(db, task, {
|
|
577
|
+
sessionId: targetOwner.sessionId,
|
|
578
|
+
operationId: `handoff:${randomUUID()}`,
|
|
579
|
+
expectedRevision: task.revision,
|
|
580
|
+
expectedGeneration: task.generation,
|
|
581
|
+
evidence: [ref],
|
|
582
|
+
reason: "Trusted native primary continuation handoff",
|
|
583
|
+
...(this.sourceEndSeq(source) === undefined
|
|
584
|
+
? {}
|
|
585
|
+
: { sourceEndSeq: this.sourceEndSeq(source) }),
|
|
586
|
+
});
|
|
398
587
|
}));
|
|
399
588
|
return this.publish(result);
|
|
400
589
|
}
|
|
401
|
-
dispose() {
|
|
590
|
+
dispose() {
|
|
591
|
+
this.#closed = true;
|
|
592
|
+
this.#lifecycle.abort(new Error("Task workflow disposed"));
|
|
593
|
+
}
|
|
402
594
|
}
|
|
403
595
|
//# sourceMappingURL=task-state-host.js.map
|