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.
Files changed (99) hide show
  1. package/dist/context/replay-model-memory.d.ts +2 -2
  2. package/dist/context/replay-model-memory.d.ts.map +1 -1
  3. package/dist/context/replay-model-memory.js +70 -16
  4. package/dist/context/replay-model-memory.js.map +1 -1
  5. package/dist/dsh/agenticreplay-host.d.ts.map +1 -1
  6. package/dist/dsh/agenticreplay-host.js +5 -1
  7. package/dist/dsh/agenticreplay-host.js.map +1 -1
  8. package/dist/dsh/agenticreplay-libraries.d.ts +1 -0
  9. package/dist/dsh/agenticreplay-libraries.d.ts.map +1 -1
  10. package/dist/dsh/agenticreplay-libraries.js.map +1 -1
  11. package/dist/dsh/agenticreplay-recorder.d.ts.map +1 -1
  12. package/dist/dsh/agenticreplay-recorder.js +22 -10
  13. package/dist/dsh/agenticreplay-recorder.js.map +1 -1
  14. package/dist/dsh/agenticreplay-security.d.ts.map +1 -1
  15. package/dist/dsh/agenticreplay-security.js +40 -6
  16. package/dist/dsh/agenticreplay-security.js.map +1 -1
  17. package/dist/dsh/agenticreplay-types.d.ts +3 -0
  18. package/dist/dsh/agenticreplay-types.d.ts.map +1 -1
  19. package/dist/dsh/agenticreplay-types.js.map +1 -1
  20. package/dist/dsh/coding-workflow/host.d.ts +1 -0
  21. package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
  22. package/dist/dsh/coding-workflow/host.js +10 -0
  23. package/dist/dsh/coding-workflow/host.js.map +1 -1
  24. package/dist/dsh/coding-workflow/service.d.ts +6 -2
  25. package/dist/dsh/coding-workflow/service.d.ts.map +1 -1
  26. package/dist/dsh/coding-workflow/service.js +6 -5
  27. package/dist/dsh/coding-workflow/service.js.map +1 -1
  28. package/dist/dsh/core/host.d.ts.map +1 -1
  29. package/dist/dsh/core/host.js +10 -1
  30. package/dist/dsh/core/host.js.map +1 -1
  31. package/dist/dsh/execution-support.d.ts.map +1 -1
  32. package/dist/dsh/execution-support.js +2 -0
  33. package/dist/dsh/execution-support.js.map +1 -1
  34. package/dist/dsh/host-adapter/lifecycle.d.ts +20 -18
  35. package/dist/dsh/host-adapter/lifecycle.d.ts.map +1 -1
  36. package/dist/dsh/host-adapter/lifecycle.js +298 -102
  37. package/dist/dsh/host-adapter/lifecycle.js.map +1 -1
  38. package/dist/dsh/host-adapter.d.ts +47 -47
  39. package/dist/dsh/host-adapter.d.ts.map +1 -1
  40. package/dist/dsh/host-adapter.js +1213 -592
  41. package/dist/dsh/host-adapter.js.map +1 -1
  42. package/dist/dsh/index.d.ts +42 -41
  43. package/dist/dsh/index.d.ts.map +1 -1
  44. package/dist/dsh/index.js +166 -92
  45. package/dist/dsh/index.js.map +1 -1
  46. package/dist/dsh/lifecycle-memory.d.ts +12 -7
  47. package/dist/dsh/lifecycle-memory.d.ts.map +1 -1
  48. package/dist/dsh/lifecycle-memory.js +606 -172
  49. package/dist/dsh/lifecycle-memory.js.map +1 -1
  50. package/dist/dsh/memory-application.d.ts +9 -9
  51. package/dist/dsh/memory-application.d.ts.map +1 -1
  52. package/dist/dsh/memory-application.js +448 -147
  53. package/dist/dsh/memory-application.js.map +1 -1
  54. package/dist/dsh/on-demand-intake.d.ts +13 -0
  55. package/dist/dsh/on-demand-intake.d.ts.map +1 -1
  56. package/dist/dsh/on-demand-intake.js +30 -7
  57. package/dist/dsh/on-demand-intake.js.map +1 -1
  58. package/dist/dsh/repository-learning-reflector.d.ts +58 -0
  59. package/dist/dsh/repository-learning-reflector.d.ts.map +1 -0
  60. package/dist/dsh/repository-learning-reflector.js +230 -0
  61. package/dist/dsh/repository-learning-reflector.js.map +1 -0
  62. package/dist/dsh/repository-learning.d.ts +34 -0
  63. package/dist/dsh/repository-learning.d.ts.map +1 -0
  64. package/dist/dsh/repository-learning.js +512 -0
  65. package/dist/dsh/repository-learning.js.map +1 -0
  66. package/dist/dsh/task-completion.d.ts +7 -7
  67. package/dist/dsh/task-completion.d.ts.map +1 -1
  68. package/dist/dsh/task-completion.js +554 -194
  69. package/dist/dsh/task-completion.js.map +1 -1
  70. package/dist/dsh/task-state-host.d.ts +10 -10
  71. package/dist/dsh/task-state-host.d.ts.map +1 -1
  72. package/dist/dsh/task-state-host.js +337 -145
  73. package/dist/dsh/task-state-host.js.map +1 -1
  74. package/dist/index.js +1 -0
  75. package/dist/locale/en.json +1 -1
  76. package/dist/locale/ja.json +1 -1
  77. package/dist/locale/ko.json +1 -1
  78. package/dist/locale/zh-CN.json +1 -1
  79. package/dist/memory/forget.d.ts +2 -2
  80. package/dist/memory/forget.d.ts.map +1 -1
  81. package/dist/memory/forget.js +175 -74
  82. package/dist/memory/forget.js.map +1 -1
  83. package/dist/memory/replay-reflection.d.ts +94 -0
  84. package/dist/memory/replay-reflection.d.ts.map +1 -0
  85. package/dist/memory/replay-reflection.js +149 -0
  86. package/dist/memory/replay-reflection.js.map +1 -0
  87. package/dist/memory/replay-review.d.ts +18 -12
  88. package/dist/memory/replay-review.d.ts.map +1 -1
  89. package/dist/memory/replay-review.js +107 -46
  90. package/dist/memory/replay-review.js.map +1 -1
  91. package/dist/memory/repository-learning.d.ts +106 -0
  92. package/dist/memory/repository-learning.d.ts.map +1 -0
  93. package/dist/memory/repository-learning.js +348 -0
  94. package/dist/memory/repository-learning.js.map +1 -0
  95. package/docs/agenticreplay-recording.md +20 -2
  96. package/docs/on-demand-intake.md +8 -2
  97. package/docs/repository-learning.md +109 -0
  98. package/migrations/048_repository_learning.sql +65 -0
  99. package/package.json +6 -1
@@ -1,31 +1,40 @@
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';
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('operation', [
25
- z.object({ operation: z.literal('create'), taskType: z.enum(TASK_TYPES) }).strict(),
26
- ...TASK_OPERATIONS.map(operation => z.object({ operation: z.literal(operation), taskId: text,
27
- expectedRevision: z.number().int().positive(), expectedGeneration: z.number().int().positive(),
28
- reason: text, evidence: z.array(text.max(512)).max(32).default([]) }).strict()),
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 || !agent?.session || this.options.agents.get(agent.id) !== agent
42
- || this.options.sessions.get(agent.session.id) !== agent.session || isNativeSubagent(agent)) {
43
- throw new KiokukoError('CONFLICT', 'Task operation requires the exact current primary native agent and session');
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('CONFLICT', 'Current native workspace is unavailable');
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, { allowDirectory: true });
64
+ const project = await resolveProjectWorkspaceReadOnly(db, cwd, {
65
+ allowDirectory: true,
66
+ });
53
67
  if (!project)
54
- throw new KiokukoError('CONFLICT', 'Current native workspace is not registered');
55
- return { sessionId: agent.session.id, repositoryRoot: project.repositoryRoot };
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('CONFLICT', 'Current native workspace is unavailable');
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, { allowDirectory: true });
67
- return project ? findSessionTask(db, { sessionId: agent.session.id, repositoryRoot: project.repositoryRoot }, { includeLatestTerminal: true }) : undefined;
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 !== 'object')
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) { if (!this.#closed) {
84
- this.options.demand?.taskStateChanged(task);
85
- try {
86
- this.options.emit(task);
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
- catch { /* A UI observer cannot roll back a committed operation. */ }
89
- } return task; }
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 || owner.repositoryRoot !== replay.owner.repositoryRoot)
97
- throw new KiokukoError('CONFLICT', 'Task owner or repository changed');
98
- const absent = Symbol('no-model-operation-receipt');
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, key: replay.key, request: replay.request,
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({ task: request, cwd: owner.repositoryRoot, deferTaskTypeInference: true, profileHints: { taskType } });
125
- const skills = await this.options.skills.snapshot({ scope: agent, cwd: owner.repositoryRoot, signal });
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('CONFLICT', 'Native capability inventory is incomplete');
128
- const schemas = stableLispCapabilities(await this.options.tools.schemas(agent), agent);
129
- const capabilities = [...skills.skills.filter((skill) => skill.invocation?.modelInvocable !== false)
130
- .map((skill) => ({ kind: 'skill', name: skill.name, ...(skill.description ? { description: skill.description } : {}) })),
131
- ...schemas.map((tool) => ({ kind: 'tool', name: tool.name, ...(tool.description ? { description: tool.description } : {}) }))];
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('CONFLICT', 'Task workspace changed');
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 && new LedgerStore(db).readRun(current.runId)?.metadata.requestId !== operationId)
143
- throw new KiokukoError('CONFLICT', 'A current task already exists; use an allowed operation or finish it before creating another');
144
- const opened = new DshRunIntakeService(db, { deferTaskTypeInference: true }).openRun({
145
- idempotencyKey: operationId, dshSessionId: owner.sessionId,
146
- request: { apiVersion: '1', workspace: project.workspace,
147
- task: { title: request, query: request, profileHints: grounded.profileHints },
148
- captureProfile: 'minimal', coverage: { run: 'unavailable', tool: 'unavailable', command: 'unavailable', file: 'unavailable', approval: 'unavailable' },
149
- capabilities, metadata: { taskStateExplicit: true, requestId: operationId, repositoryRoot: owner.repositoryRoot } }
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, { explicit: true, operationId: `task-ui:${randomUUID()}` });
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 === 'create'
162
- ? { operation: 'create', taskType: input.taskType, request: this.options.demand?.controlTask(execution) ?? '' }
163
- : { operation: input.operation, reason: input.reason, evidence: input.evidence,
164
- expected: { taskId: input.taskId, revision: input.expectedRevision, generation: input.expectedGeneration } };
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 = { scope: `task-model:${canonicalContentHash(owner.sessionId)}`,
168
- key: `task-tool:${execution.callId}`, owner, request: { owner, operation }, replayed: false };
169
- const previous = await this.options.runtime.withDatabase(db => this.modelReceipt(db, owner, replay));
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, { explicit: false, operationId: replay.key, replay });
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 === 'ignore' && !operation.reason?.trim())
197
- throw new KiokukoError('VALIDATION_ERROR', 'Ignore requires an explicit reason');
198
- if (operation.operation === 'create')
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, { ...owner, taskId: operation.expected?.taskId ?? operation.taskId })
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('CONFLICT', 'No task is selected for this session');
206
- if (operation.expected && (operation.expected.revision !== task.revision
207
- || operation.expected.generation !== undefined && operation.expected.generation !== task.generation))
208
- throw new KiokukoError('CONFLICT', 'Task changed; read the current snapshot before operating');
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 = { operation: operation.operation, operationId: source.operationId, expectedRevision: task.revision,
211
- expectedGeneration: task.generation, reason: operation.reason ?? `Explicit ${operation.operation} operation`,
212
- evidence: [...(operation.evidence ?? [])], explicit: source.explicit };
213
- if (input.operation === 'complete') {
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 ? await lisp.taskJobsIdle(agent)
216
- : !await this.options.runtime.withDatabase(db => db.prepare('SELECT 1 FROM dsh_lisp_sessions WHERE session_id=? AND enabled=1').get(task.sessionId));
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('CONFLICT', 'Task still has running or unconfirmed Lisp work');
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 === 'complete') {
340
+ if (input.operation === "complete") {
227
341
  const lisp = this.options.lisp?.();
228
- if (lisp?.taskJobsIdleSync ? !lisp.taskJobsIdleSync(agent)
229
- : !!db.prepare('SELECT 1 FROM dsh_lisp_sessions WHERE session_id=? AND enabled=1').get(task.sessionId))
230
- throw new KiokukoError('CONFLICT', 'Task still has running or unconfirmed Lisp work');
231
- if (db.prepare("SELECT 1 FROM dsh_owned_operations WHERE run_id=? AND state='started' LIMIT 1").get(task.taskId))
232
- throw new KiokukoError('CONFLICT', 'Task still has running owned work; collect its terminal result before completion');
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, { ref, kind: 'verification', generation: task.generation, verified: true });
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, { ref, kind: 'verification', generation: task.generation, verified: true,
244
- details: { source: 'explicit-completion', observationRefs: observed } });
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 === 'Working' && (input.operation === 'wait' || input.operation === 'ignore')
250
- ? requestTaskStop(db, identity, input) : transitionTask(db, identity, input, { ...(this.sourceEndSeq(agent) === undefined ? {} : { sourceEndSeq: this.sourceEndSeq(agent) }) });
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 !== 'idle')
258
- agent.cancel?.({ kind: 'hook', reason: 'Explicit Kiokuko task stop' });
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 === 'Done')
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 : agent.session.snapshotEvents?.().at(-1)?.seq;
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 || current.generation !== task.generation)
280
- throw new KiokukoError('CONFLICT', 'Task changed before stopping its work');
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.prepare('SELECT 1 FROM dsh_lisp_sessions WHERE session_id=? AND enabled=1').get(task.sessionId));
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 !== 'idle')
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 !== 'idle')
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, { ref, kind: 'stopped', generation: current.generation, verified: true });
326
- const confirmed = confirmTaskStop(db, task, { operationId, expectedRevision: current.revision, evidenceRef: ref });
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
- })().catch(() => { }).finally(() => this.#stops.delete(task.taskId));
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 !== 'Done' || this.#closed || this.#completions.has(key) || !agent.whenIdle)
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 !== 'idle')
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 !== 'idle')
506
+ if (agent.status !== "idle")
349
507
  return false;
350
508
  const current = readTaskSnapshot(db, task);
351
- if (current.state !== 'Done' || current.revision !== task.revision || current.generation !== task.generation)
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 { /* Optional local memory cannot undo completion. */ }
360
- })().catch(() => { })
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 || target.session.header?.parentSession !== source.session.id)
367
- throw new KiokukoError('CONFLICT', 'Task handoff requires a primary continuation in the same repository');
368
- const selected = await this.options.runtime.withDatabase(db => {
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, { ...sourceOwner, taskId: expected.taskId });
373
- if (task.revision !== expected.revision || expected.generation !== undefined && task.generation !== expected.generation
374
- || task.pendingStop)
375
- throw new KiokukoError('CONFLICT', 'Task changed before handoff or its stop is unresolved');
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 !== 'idle')
380
- throw new KiokukoError('CONFLICT', 'Task source has not confirmed its stop');
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 !== 'idle')
389
- throw new KiokukoError('CONFLICT', 'Task source resumed before handoff');
390
- const task = readTaskSnapshot(db, { ...sourceOwner, taskId: expected.taskId });
391
- if (task.revision !== expected.revision || expected.generation !== undefined && task.generation !== expected.generation)
392
- throw new KiokukoError('CONFLICT', 'Task changed before handoff');
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, { ref, kind: 'stopped', generation: task.generation, verified: true });
395
- return transferTaskOwner(db, task, { sessionId: targetOwner.sessionId, operationId: `handoff:${randomUUID()}`,
396
- expectedRevision: task.revision, expectedGeneration: task.generation, evidence: [ref], reason: 'Trusted native primary continuation handoff',
397
- ...(this.sourceEndSeq(source) === undefined ? {} : { sourceEndSeq: this.sourceEndSeq(source) }) });
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() { this.#closed = true; this.#lifecycle.abort(new Error('Task workflow disposed')); }
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