kiokuko-dsh 0.2.4 → 0.2.6

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 (170) hide show
  1. package/dist/client.cjs +78 -8
  2. package/dist/dsh/agenticreplay-event-mapper.d.ts +7 -0
  3. package/dist/dsh/agenticreplay-event-mapper.d.ts.map +1 -1
  4. package/dist/dsh/agenticreplay-event-mapper.js +52 -13
  5. package/dist/dsh/agenticreplay-event-mapper.js.map +1 -1
  6. package/dist/dsh/agenticreplay-hooks.d.ts.map +1 -1
  7. package/dist/dsh/agenticreplay-hooks.js +10 -1
  8. package/dist/dsh/agenticreplay-hooks.js.map +1 -1
  9. package/dist/dsh/agenticreplay-recorder.d.ts +5 -0
  10. package/dist/dsh/agenticreplay-recorder.d.ts.map +1 -1
  11. package/dist/dsh/agenticreplay-recorder.js +193 -20
  12. package/dist/dsh/agenticreplay-recorder.js.map +1 -1
  13. package/dist/dsh/answer-context.d.ts +8 -6
  14. package/dist/dsh/answer-context.d.ts.map +1 -1
  15. package/dist/dsh/answer-context.js +85 -21
  16. package/dist/dsh/answer-context.js.map +1 -1
  17. package/dist/dsh/coding-workflow/contracts.d.ts +5 -0
  18. package/dist/dsh/coding-workflow/contracts.d.ts.map +1 -1
  19. package/dist/dsh/coding-workflow/contracts.js +20 -3
  20. package/dist/dsh/coding-workflow/contracts.js.map +1 -1
  21. package/dist/dsh/coding-workflow/host.d.ts +4 -1
  22. package/dist/dsh/coding-workflow/host.d.ts.map +1 -1
  23. package/dist/dsh/coding-workflow/host.js +72 -44
  24. package/dist/dsh/coding-workflow/host.js.map +1 -1
  25. package/dist/dsh/coding-workflow/intent.d.ts +8 -0
  26. package/dist/dsh/coding-workflow/intent.d.ts.map +1 -1
  27. package/dist/dsh/coding-workflow/intent.js +33 -2
  28. package/dist/dsh/coding-workflow/intent.js.map +1 -1
  29. package/dist/dsh/coding-workflow/service.d.ts +214 -0
  30. package/dist/dsh/coding-workflow/service.d.ts.map +1 -1
  31. package/dist/dsh/coding-workflow/service.js +86 -6
  32. package/dist/dsh/coding-workflow/service.js.map +1 -1
  33. package/dist/dsh/coding-workflow/store.d.ts.map +1 -1
  34. package/dist/dsh/coding-workflow/store.js +10 -1
  35. package/dist/dsh/coding-workflow/store.js.map +1 -1
  36. package/dist/dsh/core/host.d.ts.map +1 -1
  37. package/dist/dsh/core/host.js +288 -48
  38. package/dist/dsh/core/host.js.map +1 -1
  39. package/dist/dsh/core/tasks.d.ts +7 -0
  40. package/dist/dsh/core/tasks.d.ts.map +1 -1
  41. package/dist/dsh/core/tasks.js +59 -8
  42. package/dist/dsh/core/tasks.js.map +1 -1
  43. package/dist/dsh/decisions/contracts.d.ts +3 -2
  44. package/dist/dsh/decisions/contracts.d.ts.map +1 -1
  45. package/dist/dsh/decisions/contracts.js +1 -1
  46. package/dist/dsh/decisions/contracts.js.map +1 -1
  47. package/dist/dsh/decisions/request-intent.d.ts +121 -0
  48. package/dist/dsh/decisions/request-intent.d.ts.map +1 -0
  49. package/dist/dsh/decisions/request-intent.js +260 -0
  50. package/dist/dsh/decisions/request-intent.js.map +1 -0
  51. package/dist/dsh/decisions/service.d.ts +5 -1
  52. package/dist/dsh/decisions/service.d.ts.map +1 -1
  53. package/dist/dsh/decisions/service.js +2 -1
  54. package/dist/dsh/decisions/service.js.map +1 -1
  55. package/dist/dsh/execution-selection.d.ts +13 -0
  56. package/dist/dsh/execution-selection.d.ts.map +1 -1
  57. package/dist/dsh/execution-selection.js +11 -0
  58. package/dist/dsh/execution-selection.js.map +1 -1
  59. package/dist/dsh/execution-support.d.ts +2 -0
  60. package/dist/dsh/execution-support.d.ts.map +1 -1
  61. package/dist/dsh/execution-support.js +36 -8
  62. package/dist/dsh/execution-support.js.map +1 -1
  63. package/dist/dsh/head-worker-configuration.d.ts +27 -0
  64. package/dist/dsh/head-worker-configuration.d.ts.map +1 -0
  65. package/dist/dsh/head-worker-configuration.js +69 -0
  66. package/dist/dsh/head-worker-configuration.js.map +1 -0
  67. package/dist/dsh/host-adapter/admission.d.ts +4 -1
  68. package/dist/dsh/host-adapter/admission.d.ts.map +1 -1
  69. package/dist/dsh/host-adapter/admission.js +297 -12
  70. package/dist/dsh/host-adapter/admission.js.map +1 -1
  71. package/dist/dsh/host-adapter/routing.d.ts +4 -1
  72. package/dist/dsh/host-adapter/routing.d.ts.map +1 -1
  73. package/dist/dsh/host-adapter/routing.js +42 -23
  74. package/dist/dsh/host-adapter/routing.js.map +1 -1
  75. package/dist/dsh/host-adapter/turn-state.d.ts +6 -0
  76. package/dist/dsh/host-adapter/turn-state.d.ts.map +1 -1
  77. package/dist/dsh/host-adapter/turn-state.js +19 -1
  78. package/dist/dsh/host-adapter/turn-state.js.map +1 -1
  79. package/dist/dsh/host-adapter.d.ts.map +1 -1
  80. package/dist/dsh/host-adapter.js +217 -9
  81. package/dist/dsh/host-adapter.js.map +1 -1
  82. package/dist/dsh/intake-clarification.d.ts +2 -2
  83. package/dist/dsh/intake-clarification.d.ts.map +1 -1
  84. package/dist/dsh/intake-clarification.js +13 -4
  85. package/dist/dsh/intake-clarification.js.map +1 -1
  86. package/dist/dsh/intake-gate.d.ts +5 -0
  87. package/dist/dsh/intake-gate.d.ts.map +1 -1
  88. package/dist/dsh/intake-gate.js +19 -5
  89. package/dist/dsh/intake-gate.js.map +1 -1
  90. package/dist/dsh/lisp/candidate.d.ts +1 -1
  91. package/dist/dsh/lisp/surface.d.ts +1 -1
  92. package/dist/dsh/memory-owner.d.ts +24 -0
  93. package/dist/dsh/memory-owner.d.ts.map +1 -0
  94. package/dist/dsh/memory-owner.js +35 -0
  95. package/dist/dsh/memory-owner.js.map +1 -0
  96. package/dist/dsh/memory-request-fence.d.ts +37 -0
  97. package/dist/dsh/memory-request-fence.d.ts.map +1 -0
  98. package/dist/dsh/memory-request-fence.js +48 -0
  99. package/dist/dsh/memory-request-fence.js.map +1 -0
  100. package/dist/dsh/message-sources.d.ts.map +1 -1
  101. package/dist/dsh/message-sources.js +2 -1
  102. package/dist/dsh/message-sources.js.map +1 -1
  103. package/dist/dsh/model-selection-ui.d.ts +2 -0
  104. package/dist/dsh/model-selection-ui.d.ts.map +1 -1
  105. package/dist/dsh/model-selection-ui.js +69 -5
  106. package/dist/dsh/model-selection-ui.js.map +1 -1
  107. package/dist/dsh/models/contracts.d.ts +30 -2
  108. package/dist/dsh/models/contracts.d.ts.map +1 -1
  109. package/dist/dsh/models/contracts.js +6 -0
  110. package/dist/dsh/models/contracts.js.map +1 -1
  111. package/dist/dsh/models/profiles.d.ts +25 -0
  112. package/dist/dsh/models/profiles.d.ts.map +1 -1
  113. package/dist/dsh/models/profiles.js +34 -1
  114. package/dist/dsh/models/profiles.js.map +1 -1
  115. package/dist/dsh/models/service.d.ts +67 -3
  116. package/dist/dsh/models/service.d.ts.map +1 -1
  117. package/dist/dsh/models/service.js +210 -30
  118. package/dist/dsh/models/service.js.map +1 -1
  119. package/dist/dsh/models/session-selection.d.ts +25 -0
  120. package/dist/dsh/models/session-selection.d.ts.map +1 -0
  121. package/dist/dsh/models/session-selection.js +16 -0
  122. package/dist/dsh/models/session-selection.js.map +1 -0
  123. package/dist/dsh/models/surface.d.ts +1 -0
  124. package/dist/dsh/models/surface.d.ts.map +1 -1
  125. package/dist/dsh/models/surface.js +24 -4
  126. package/dist/dsh/models/surface.js.map +1 -1
  127. package/dist/dsh/native-worker-routing.d.ts +69 -0
  128. package/dist/dsh/native-worker-routing.d.ts.map +1 -0
  129. package/dist/dsh/native-worker-routing.js +147 -0
  130. package/dist/dsh/native-worker-routing.js.map +1 -0
  131. package/dist/dsh/on-demand-intake.d.ts +71 -5
  132. package/dist/dsh/on-demand-intake.d.ts.map +1 -1
  133. package/dist/dsh/on-demand-intake.js +299 -70
  134. package/dist/dsh/on-demand-intake.js.map +1 -1
  135. package/dist/dsh/owned-execution.d.ts +3 -0
  136. package/dist/dsh/owned-execution.d.ts.map +1 -1
  137. package/dist/dsh/owned-execution.js +7 -0
  138. package/dist/dsh/owned-execution.js.map +1 -1
  139. package/dist/dsh/request-memory.d.ts +2 -0
  140. package/dist/dsh/request-memory.d.ts.map +1 -1
  141. package/dist/dsh/request-memory.js +33 -2
  142. package/dist/dsh/request-memory.js.map +1 -1
  143. package/dist/dsh/setup.d.ts +2 -2
  144. package/dist/dsh/setup.d.ts.map +1 -1
  145. package/dist/dsh/setup.js +37 -14
  146. package/dist/dsh/setup.js.map +1 -1
  147. package/dist/dsh/skill-prompts.json +1 -1
  148. package/dist/dsh/standard-skills.d.ts +2 -1
  149. package/dist/dsh/standard-skills.d.ts.map +1 -1
  150. package/dist/dsh/standard-skills.js +2 -1
  151. package/dist/dsh/standard-skills.js.map +1 -1
  152. package/dist/dsh/task-intake.d.ts +6 -0
  153. package/dist/dsh/task-intake.d.ts.map +1 -1
  154. package/dist/dsh/task-intake.js +7 -2
  155. package/dist/dsh/task-intake.js.map +1 -1
  156. package/dist/locale/en.json +1 -1
  157. package/dist/locale/ja.json +1 -1
  158. package/dist/locale/ko.json +1 -1
  159. package/dist/locale/zh-CN.json +1 -1
  160. package/dist/models-client.cjs +78 -8
  161. package/dist/models-client.d.ts.map +1 -1
  162. package/dist/models-client.js +76 -8
  163. package/dist/models-client.js.map +1 -1
  164. package/docs/coding-workflow.md +3 -1
  165. package/docs/dsh-plugin.md +28 -5
  166. package/docs/kiokuko-models.md +47 -0
  167. package/docs/on-demand-intake.md +13 -7
  168. package/migrations/037_native_worker_routing.sql +8 -0
  169. package/package.json +3 -2
  170. package/skills/japanese-translation-for-oss-models/SKILL.md +5 -3
@@ -1,13 +1,23 @@
1
+ import { ExecutionSelectionPending } from './execution-pending.js';
1
2
  import { executionClarification } from './intake-clarification.js';
2
3
  import { randomUUID } from 'node:crypto';
4
+ import { z } from 'zod';
3
5
  import { TASK_TYPES } from '../akinator/types.js';
4
6
  import { canonicalContentHash } from '../serialization/validate.js';
5
7
  import { KIOKUKO_DSH_SOURCE_KIND } from './plugin-source.js';
6
- import { deriveProfile } from '../akinator/domain.js';
7
- import { codingIntent } from './coding-workflow/intent.js';
8
+ import { bindMainModelRequestIntent, requestIntentReferenceIds, requestIntentTaskType, RequestIntentSubmissionSchema } from './decisions/request-intent.js';
9
+ import { memoryOwnerKind } from './memory-owner.js';
8
10
  export const TASK_PREPARE_TOOL = 'prepare_requested_work';
9
- export const ON_DEMAND_GUIDANCE = 'The original user request may be answered now without selecting a task category. No execution task has been prepared yet. Preserve all requested actions, negations, alternatives and target uncertainty. Native read, glob, grep, web_search and web_fetch demands automatically prepare the original request with research advice when no non-chat action type is resolved. Use these tools directly to gather requested evidence; do not call prepare_requested_work merely to read a plan or inspect files. An explicit development request is prepared automatically on its first tool demand. prepare_requested_work(taskType) remains an advisory control operation. Do not silently replace a requested action with a text-only answer or claim work was done. Ask a concrete clarification in ordinary conversation when the actual target or requested action is unclear. Installed Skill reads need no preparation and do not activate execution. Do not prepare work merely to load a skill. Owned development tools use their own authorization; Plan, goal, cancellation and execution ownership still apply. If only run_code is exposed and the request is ambiguous, prepare with exactly return await tools.prepare_requested_work({"taskType":"research"}) using the appropriate advisory type; the host intercepts that preparation-only carrier without evaluating code. For an explicit development request, its first run_code demand prepares automatically before evaluating the program.';
11
+ export const ON_DEMAND_GUIDANCE = 'The original user request may be answered now without selecting a task category. No execution task has been prepared yet. Preserve the original utterance, every requested action, negation, quotation, target uncertainty, and current-session references; never replace it with a model rewrite. Use only the supplied active Goal and proposal references from this exact native session, never another chat. Classify relation as new, same_scope_continuation, scope_change, or unknown, and action as answer, research, verify, change, operate, or unknown; do not infer either from a fixed phrase or tool name. “Check how it behaves” is verification/research even when the next tool is Bash. This interpretation grants no native permission. When the decision provider is unavailable, abstains, or returns unknown, interpret the same context in the main model; ask the user only if the action or referenced target remains unclear, and preserve active work while waiting. Native read, glob, grep, web_search and web_fetch demands can gather evidence without preparation. Do not call prepare_requested_work merely to read a plan or inspect files. prepare_requested_work accepts either the legacy taskType alone or a requestIntent with exact evidenceRefs from the supplied context; it never accepts a rewritten task or target and never changes native permissions. Installed Skill reads need no preparation and do not activate execution. Do not prepare work merely to load a skill. Plan, Goal, cancellation, memory-owner and execution ownership still apply. If only run_code is exposed, use the exact preparation-only carrier returned by this tool contract; the host intercepts it without evaluating code.';
10
12
  const ACTION_TYPES = TASK_TYPES.filter(type => type !== 'chat');
13
+ const preparationFieldsSchema = z.object({
14
+ taskType: z.enum(ACTION_TYPES).optional(),
15
+ requestIntent: RequestIntentSubmissionSchema.optional(),
16
+ }).strict();
17
+ function parsePreparationRequest(value) {
18
+ const parsed = preparationFieldsSchema.safeParse(value);
19
+ return parsed.success && (parsed.data.taskType !== undefined || parsed.data.requestIntent !== undefined) ? parsed.data : undefined;
20
+ }
11
21
  function humanMessages(messages) {
12
22
  return messages.filter((value) => value?.role === 'user' && (!value.source || value.source.kind === 'user'));
13
23
  }
@@ -21,9 +31,17 @@ function eventTurn(event) {
21
31
  return Number.isSafeInteger(data?.turn) ? data.turn : undefined;
22
32
  }
23
33
  /** Tool names select advice only; admission and native permissions remain authoritative. */
24
- function automaticPreparationType(intent, name, task) {
25
- if (codingIntent(task))
26
- return 'build';
34
+ function automaticPreparationType(intent, requestIntent, name) {
35
+ if (requestIntent?.relation === 'unknown' || requestIntent?.action === 'unknown') {
36
+ // Gathering evidence grants no change/process authority. An active Goal
37
+ // still requires its reference to be resolved before opening another run.
38
+ return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name) ? 'research' : undefined;
39
+ }
40
+ if (requestIntent?.action === 'answer')
41
+ return undefined;
42
+ const interpreted = requestIntentTaskType(requestIntent, intent.taskType);
43
+ if (interpreted && interpreted !== 'chat')
44
+ return interpreted;
27
45
  if (intent.taskType && intent.taskType !== 'chat')
28
46
  return intent.taskType;
29
47
  return ['read', 'glob', 'grep', 'web_search', 'web_fetch'].includes(name) ? 'research' : undefined;
@@ -35,7 +53,7 @@ export function preparationCarrier(value) {
35
53
  return undefined;
36
54
  const args = value;
37
55
  if (Object.keys(args).sort().join(',') !== 'code,description' || typeof args.description !== 'string' || !args.description.trim()
38
- || typeof args.code !== 'string' || args.code.length > 256)
56
+ || typeof args.code !== 'string' || args.code.length > 8192)
39
57
  return undefined;
40
58
  const code = args.code.trim().replace(/;$/u, '');
41
59
  const prefix = 'return await tools.prepare_requested_work(';
@@ -43,10 +61,10 @@ export function preparationCarrier(value) {
43
61
  return undefined;
44
62
  try {
45
63
  const input = JSON.parse(code.slice(prefix.length, -1));
46
- if (!input || Array.isArray(input) || Object.keys(input).length !== 1 || !ACTION_TYPES.includes(input.taskType)
47
- || code !== prefix + JSON.stringify(input) + ')')
64
+ const parsed = parsePreparationRequest(input);
65
+ if (!parsed || code !== prefix + JSON.stringify(input) + ')')
48
66
  return undefined;
49
- return input.taskType;
67
+ return parsed;
50
68
  }
51
69
  catch {
52
70
  return undefined;
@@ -78,6 +96,15 @@ export class OnDemandIntake {
78
96
  const last = execution.agent?.session?.snapshotEvents?.().filter(event => event.type === 'turn/start' || event.type === 'turn/end').at(-1);
79
97
  return last?.type === 'turn/start' ? eventTurn(last) : undefined;
80
98
  }
99
+ acknowledgeExisting(state) {
100
+ this.assertCurrent(state);
101
+ state.existing = true;
102
+ state.status = 'prepared';
103
+ const taskType = requestIntentTaskType(state.requestIntent, state.intent.taskType);
104
+ if (taskType !== undefined)
105
+ state.taskType = taskType;
106
+ state.preparedIntentDigest = canonicalContentHash({ taskType: state.taskType ?? null, requestIntent: state.requestIntent ?? null });
107
+ }
81
108
  /** True means ordinary text may continue; false leaves an existing execution on its original path. */
82
109
  async capture(input) {
83
110
  input.signal.throwIfAborted();
@@ -98,6 +125,8 @@ export class OnDemandIntake {
98
125
  if (!fresh.length) {
99
126
  if (previous.capture && !await previous.capture)
100
127
  return false;
128
+ if (previous.status === 'stale')
129
+ throw previous.captureError ?? new Error('Previous on-demand request is stale');
101
130
  return previous.status !== 'prepared';
102
131
  }
103
132
  previous.status = 'stale';
@@ -109,15 +138,34 @@ export class OnDemandIntake {
109
138
  merged.set(humanId(message), message);
110
139
  input = { ...input, messages: [...merged.values()] };
111
140
  }
141
+ if (previous.input.turn !== input.turn && !humanMessages(input.messages).length && this.host.ready(input.agent)) {
142
+ // A host-driven next turn may carry no new human message. Leave its
143
+ // active execution bound to the host instead of creating an empty intake.
144
+ if (previous.status === 'closed') {
145
+ await this.host.validate(input);
146
+ input.signal.throwIfAborted();
147
+ await this.host.advance?.(input);
148
+ this.#turns.delete(sessionId);
149
+ this.host.answerContext?.retire(input.agent);
150
+ return false;
151
+ }
152
+ }
112
153
  if (previous.input.turn !== input.turn && !['closed', 'prepared'].includes(previous.status))
113
154
  throw new Error('Previous on-demand turn has not ended');
114
155
  previous.controller.abort(new Error('A newer native turn superseded this request'));
115
156
  }
157
+ else if (!humanMessages(input.messages).length && this.host.ready(input.agent)) {
158
+ await this.host.validate(input);
159
+ input.signal.throwIfAborted();
160
+ await this.host.advance?.(input);
161
+ return false;
162
+ }
163
+ this.host.answerContext?.retire(input.agent);
116
164
  // Install the pending state synchronously, before any host or classifier awaits.
117
165
  const controller = new AbortController();
118
166
  const state = { input: { ...input, signal: AbortSignal.any([input.signal, controller.signal]), messages: Object.freeze(structuredClone([...input.messages])) }, task: humanTask(input.messages),
119
167
  humanContents: new Map(humanMessages(input.messages).map(message => [humanId(message), canonicalContentHash(message)])),
120
- status: 'capturing', intent: { deferInference: true }, preparationCalls: new Set(), controller };
168
+ status: 'capturing', intent: { deferInference: true }, memoryOwner: 'none', preparationCalls: new Set(), controller };
121
169
  this.#turns.set(sessionId, state);
122
170
  const promise = (async () => {
123
171
  await this.host.validate(input);
@@ -126,18 +174,6 @@ export class OnDemandIntake {
126
174
  // validated owner's first reader once, including absence; never rebind it.
127
175
  if (!this.#skillReaders.has(input.agent))
128
176
  this.#skillReaders.set(input.agent, this.#startupSkillReader ?? this.#tools?.get('skill', input.agent)?.execute);
129
- if (previous?.input.turn !== input.turn && await this.host.existing(input)) {
130
- this.assertCurrent(state);
131
- // Retain the exact resumed request for the public preparation receipt.
132
- // Existing ownership alone grants nothing: readiness is rechecked at dispatch.
133
- if (state.task) {
134
- state.existing = true;
135
- state.status = 'prepared';
136
- }
137
- else
138
- this.#turns.delete(sessionId);
139
- return false;
140
- }
141
177
  if (!humanMessages(input.messages).length)
142
178
  throw new Error('Original human input is required before on-demand intake');
143
179
  try {
@@ -147,21 +183,75 @@ export class OnDemandIntake {
147
183
  input.signal.throwIfAborted();
148
184
  state.intent = { deferInference: true };
149
185
  }
150
- if (!state.intent.taskType && !state.intent.deferInference && !executionClarification(state.task)) {
151
- const type = deriveProfile(state.task).taskType;
152
- if (type === 'build' || type === 'debug' || type === 'devops')
153
- state.intent = { ...state.intent, taskType: type };
186
+ try {
187
+ const interpreted = await this.host.interpret?.(state.input, state.task);
188
+ if (interpreted) {
189
+ state.requestIntentContext = interpreted.context;
190
+ state.requestIntent = interpreted.decision;
191
+ state.requestIntentContextDigest = canonicalContentHash(interpreted.context);
192
+ }
193
+ }
194
+ catch (error) {
195
+ input.signal.throwIfAborted();
154
196
  }
197
+ state.memoryOwner = memoryOwnerKind(state.requestIntent, state.intent.taskType);
155
198
  this.assertCurrent(state);
199
+ if (previous?.input.turn !== input.turn)
200
+ await this.host.advance?.(input, state.requestIntent);
201
+ this.assertCurrent(state);
202
+ const resumeConfirmation = previous?.input.turn !== input.turn && state.task
203
+ ? await this.host.resumeConfirmation?.(input) ?? false : false;
204
+ if (process.env.KIOKUKO_INTENT_DEBUG === '1' && previous?.input.turn !== input.turn) {
205
+ console.error('on-demand confirmation route', { turn: input.turn, previousTurn: previous?.input.turn,
206
+ hasTask: state.task.length > 0, resumeConfirmation });
207
+ }
208
+ if (resumeConfirmation) {
209
+ this.assertCurrent(state);
210
+ state.confirmationResume = true;
211
+ state.status = 'answer';
212
+ if (previous?.taskType !== undefined)
213
+ state.taskType = previous.taskType;
214
+ this.host.answerContext?.retire(input.agent);
215
+ return false;
216
+ }
217
+ if (process.env.KIOKUKO_INTENT_DEBUG === '1' && previous?.input.turn !== input.turn && state.task && this.host.resumeConfirmation) {
218
+ console.error('confirmation resume not selected', input.turn, state.task.length);
219
+ }
220
+ if (state.task && await this.host.replay?.(state.input)) {
221
+ this.acknowledgeExisting(state);
222
+ return false;
223
+ }
224
+ const canContinue = state.requestIntent?.relation === 'same_scope_continuation'
225
+ && state.requestIntent.action !== 'answer' && state.requestIntent.action !== 'unknown';
226
+ if (previous?.input.turn !== input.turn && state.task && canContinue
227
+ && await this.host.existing(input, state.requestIntent)) {
228
+ // Persisted ownership is only an acknowledgement when this exact native
229
+ // agent/session/turn is already bound. A cold resume stays conversational
230
+ // until explicit preparation reconstructs that binding.
231
+ if (this.host.ready(state.input.agent, state.input.turn)) {
232
+ this.acknowledgeExisting(state);
233
+ return false;
234
+ }
235
+ }
156
236
  // Eager mode still binds known work before generation. Classifier uncertainty
157
237
  // instead reaches ordinary reasoning, with no execution grant or purpose UI.
158
- if (this.options.onlyWhenDeferred && !state.intent.deferInference) {
238
+ const requestIntentUnresolved = state.requestIntent !== undefined
239
+ && (state.requestIntent.relation === 'unknown' || state.requestIntent.action === 'unknown');
240
+ const requestAsAnswer = state.requestIntent?.action === 'answer';
241
+ const knownIntent = this.host.interpret === undefined
242
+ ? !state.intent.deferInference && state.intent.taskType !== undefined && state.intent.taskType !== 'chat'
243
+ : state.requestIntent !== undefined && !requestIntentUnresolved && !requestAsAnswer
244
+ && state.requestIntent.relation !== 'unknown' && state.requestIntent.action !== 'unknown';
245
+ if (this.options.onlyWhenDeferred && knownIntent) {
159
246
  this.#turns.delete(sessionId);
160
247
  state.status = 'closed';
161
248
  controller.abort(new Error('Known intent continues through eager admission'));
162
249
  return false;
163
250
  }
164
- await this.host.answerContext?.prepare(state.input, state.task, state.intent.taskType ?? null);
251
+ // The current request is on the conversational path until actual admission.
252
+ // Provisional intent cannot select a different run's memory validator.
253
+ state.memoryOwner = 'conversation';
254
+ await this.host.answerContext?.prepare(state.input, state.task, state.intent.taskType ?? null, state.memoryOwner);
165
255
  this.assertCurrent(state);
166
256
  state.status = 'answer';
167
257
  return true;
@@ -172,8 +262,10 @@ export class OnDemandIntake {
172
262
  return await promise;
173
263
  }
174
264
  catch (error) {
175
- if (this.#turns.get(sessionId) === state)
265
+ if (this.#turns.get(sessionId) === state) {
176
266
  state.status = 'stale';
267
+ state.captureError = error;
268
+ }
177
269
  throw error;
178
270
  }
179
271
  finally {
@@ -197,6 +289,11 @@ export class OnDemandIntake {
197
289
  return undefined;
198
290
  return state;
199
291
  }
292
+ ownerIdentity(state) {
293
+ const session = state.input.agent.session;
294
+ return { agentId: state.input.agent.id, sessionId: session.id, turn: state.input.turn,
295
+ requestDigest: canonicalContentHash(state.task) };
296
+ }
200
297
  definition(execution) {
201
298
  try {
202
299
  return this.#tools?.get(execution.name, execution.agent);
@@ -205,17 +302,26 @@ export class OnDemandIntake {
205
302
  return undefined;
206
303
  }
207
304
  }
208
- async prepare(state, taskType, signal) {
305
+ async prepare(state, taskType, signal, requestIntent, explicitIntent = false) {
209
306
  this.assertCurrent(state);
210
307
  signal.throwIfAborted();
211
- const clarification = executionClarification(state.task);
308
+ if (state.selectionPending)
309
+ throw new ExecutionSelectionPending();
310
+ const resolved = requestIntent ?? state.requestIntent;
311
+ const clarification = executionClarification(state.task, resolved, state.requestIntentContext);
212
312
  if (clarification)
213
313
  throw new Error(clarification);
214
314
  if (!ACTION_TYPES.includes(taskType))
215
315
  throw new Error('Answer conversational questions directly; prepare_requested_work is for requested work');
316
+ if (state.status === 'prepared' && state.confirmationResume && explicitIntent
317
+ && (state.requestIntent?.relation === 'unknown' || state.requestIntent?.action === 'unknown'))
318
+ state.status = 'answer';
216
319
  if (state.status === 'prepared') {
217
320
  if (state.taskType !== taskType)
218
321
  throw new Error('Prepared intent cannot be replaced within a turn');
322
+ if (explicitIntent && state.preparedIntentDigest !== canonicalContentHash({ taskType, requestIntent: requestIntent ?? null })) {
323
+ throw new Error('Prepared interpretation cannot be replaced within a turn');
324
+ }
219
325
  return;
220
326
  }
221
327
  if (state.preparation) {
@@ -227,12 +333,17 @@ export class OnDemandIntake {
227
333
  state.taskType = taskType;
228
334
  const operation = (async () => {
229
335
  // Retain exact original messages. The only caller-supplied input is an advisory task type.
230
- const admitted = await this.host.prepare({ ...state.input, signal: AbortSignal.any([state.input.signal, signal]) }, taskType);
336
+ const admitted = await this.host.prepare({ ...state.input, signal: AbortSignal.any([state.input.signal, signal]) }, taskType, requestIntent ?? state.requestIntent, state.requestIntentContext);
231
337
  this.assertCurrent(state);
232
338
  signal.throwIfAborted();
233
339
  if (!admitted || !this.host.ready(state.input.agent, state.input.turn))
234
340
  throw new Error('Execution preparation did not admit this exact request');
341
+ const preparedIntent = requestIntent ?? state.requestIntent;
342
+ if (preparedIntent !== undefined)
343
+ state.requestIntent = preparedIntent;
344
+ state.preparedIntentDigest = canonicalContentHash({ taskType, requestIntent: state.requestIntent ?? null });
235
345
  state.status = 'prepared';
346
+ delete state.confirmationResume;
236
347
  })();
237
348
  this.watch(operation);
238
349
  state.preparation = operation;
@@ -240,30 +351,76 @@ export class OnDemandIntake {
240
351
  await operation;
241
352
  }
242
353
  catch (error) {
243
- if (!['closed', 'stale'].includes(state.status))
244
- state.status = 'stale';
354
+ if (!['closed', 'stale'].includes(state.status)) {
355
+ // A declined selection preserves this request for a conversational acknowledgement.
356
+ // It is not admission: preparation remains consumed and tools still need a ready host.
357
+ state.selectionPending = error instanceof ExecutionSelectionPending && !state.input.signal.aborted && !signal.aborted;
358
+ state.status = state.selectionPending ? 'answer' : 'stale';
359
+ }
245
360
  throw error;
246
361
  }
247
362
  finally {
248
363
  delete state.preparation;
249
364
  }
250
365
  }
366
+ bindIntent(state, submission) {
367
+ if (!submission)
368
+ return state.requestIntent;
369
+ if (!state.requestIntentContext || state.requestIntentContextDigest !== canonicalContentHash(state.requestIntentContext)) {
370
+ throw new Error('Request interpretation context is stale');
371
+ }
372
+ const bound = bindMainModelRequestIntent(submission, state.requestIntentContext);
373
+ if (!bound)
374
+ throw new Error('Request interpretation references another request, stale revision, or unknown proposal');
375
+ const prior = state.requestIntent;
376
+ if (prior?.source === 'decision-provider' && prior.relation !== 'unknown' && prior.action !== 'unknown') {
377
+ if (prior.relation !== bound.relation || prior.action !== bound.action) {
378
+ throw new Error('Main-model interpretation conflicts with the configured decision provider');
379
+ }
380
+ return { ...prior, evidence: bound.evidence };
381
+ }
382
+ return bound;
383
+ }
251
384
  async answerMessages(input, messages) {
252
- return this.guidance(await this.host.answerContext?.project(input, messages) ?? messages);
385
+ const state = this.#turns.get(input.agent.session.id);
386
+ // Native pre-step may augment its payload while downstream hooks run.
387
+ // Project the immutable human batch captured before that waterfall.
388
+ if (state?.input.turn === input.turn && state.input.agent !== input.agent)
389
+ throw new Error('Answer projection native agent changed');
390
+ const projected = state?.input.turn === input.turn && !state.confirmationResume && this.host.answerContext
391
+ ? await this.host.answerContext.project(state.input, messages, this.ownerIdentity(state)) : messages;
392
+ return this.guidance(projected, state?.input.turn === input.turn ? state : undefined);
393
+ }
394
+ async beforeRequest(agent) {
395
+ const state = agent.session && this.#turns.get(agent.session.id);
396
+ if (state && state.input.agent === agent && state.status === 'answer' && this.pending(agent)) {
397
+ await this.host.answerContext?.beforeRequest(state.input, this.ownerIdentity(state));
398
+ }
253
399
  }
254
- async beforeRequest(agent) { if (this.pending(agent))
255
- await this.host.answerContext?.beforeRequest(agent); }
256
400
  async fence(agent, request) {
257
- if (this.pending(agent))
258
- await this.host.answerContext?.fence(agent, request);
401
+ const state = agent.session && this.#turns.get(agent.session.id);
402
+ if (state && state.input.agent === agent && state.status === 'answer' && this.pending(agent)) {
403
+ await this.host.answerContext?.fence(state.input, request, this.ownerIdentity(state));
404
+ }
259
405
  }
260
- guidance(messages) {
261
- return [...messages, { id: randomUUID(), role: 'user', content: [{ type: 'text', text: ON_DEMAND_GUIDANCE }],
262
- source: { kind: KIOKUKO_DSH_SOURCE_KIND, form: 'snapshot', sections: [{ name: 'on-demand-intake', text: ON_DEMAND_GUIDANCE }] } }];
406
+ guidance(messages, state) {
407
+ const context = state?.requestIntentContext;
408
+ const interpretation = state?.requestIntent;
409
+ const intentText = context ? `\n\nCurrent request references and same-session work (host-bound, advisory only):\n${JSON.stringify({
410
+ references: requestIntentReferenceIds(context),
411
+ activeGoal: context.activeGoal ?? null,
412
+ proposals: context.proposals.map(({ id, revision, digest, text }) => ({ id, revision, digest, text })),
413
+ decision: interpretation ? { relation: interpretation.relation, action: interpretation.action, source: interpretation.source,
414
+ policyVersion: interpretation.policyVersion, evidence: interpretation.evidence, unresolvedReason: interpretation.unresolvedReason ?? null } : null,
415
+ })}\nUse only these exact references in requestIntent.evidenceRefs. The original human message remains unchanged.` : '';
416
+ const text = ON_DEMAND_GUIDANCE + intentText;
417
+ return [...messages, { id: randomUUID(), role: 'user', content: [{ type: 'text', text }],
418
+ source: { kind: KIOKUKO_DSH_SOURCE_KIND, form: 'snapshot', sections: [{ name: 'on-demand-intake', text }] } }];
263
419
  }
264
420
  snapshot(sessionId) {
265
421
  const state = this.#turns.get(sessionId);
266
- return state && { task: state.task, taskType: state.taskType ?? state.intent.taskType ?? null, status: state.status, turn: state.input.turn };
422
+ return state && { task: state.task, taskType: state.taskType ?? state.intent.taskType ?? null, status: state.status, turn: state.input.turn,
423
+ ...(state.requestIntent ? { requestIntent: structuredClone(state.requestIntent) } : {}), memoryOwner: state.memoryOwner };
267
424
  }
268
425
  /** A current host-validated control call, never arbitrary code or a name-based exemption. */
269
426
  preparationOnly(execution) {
@@ -273,13 +430,14 @@ export class OnDemandIntake {
273
430
  || state.preparationCalls.has(execution.callId))
274
431
  return false;
275
432
  if (proof.carrier)
276
- return execution.name === 'run_code' && state.status === 'answer' && preparationCarrier(execution.arguments) === proof.carrier;
433
+ return execution.name === 'run_code' && state.status === 'answer'
434
+ && canonicalContentHash(preparationCarrier(execution.arguments) ?? null) === proof.carrierDigest;
277
435
  return proof.requestPreparation === true && execution.name === TASK_PREPARE_TOOL
278
436
  && (state.status === 'answer' || state.existing === true && state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn));
279
437
  }
280
438
  pending(agent) {
281
439
  const state = agent.session && this.#turns.get(agent.session.id);
282
- return !!state && state.input.agent === agent && !['prepared', 'closed', 'stale'].includes(state.status);
440
+ return !!state && !state.confirmationResume && state.input.agent === agent && !['prepared', 'closed', 'stale'].includes(state.status);
283
441
  }
284
442
  /** An admitted host continuation still needs capture to retire the previous turn's receipt. */
285
443
  continuing(agent) { return !this.#stopped && this.host.ready(agent); }
@@ -288,6 +446,7 @@ export class OnDemandIntake {
288
446
  if (state?.input.agent.session === session && state.input.turn === turn) {
289
447
  state.status = 'closed';
290
448
  state.controller.abort(new Error('Native turn ended'));
449
+ this.host.answerContext?.retire(state.input.agent);
291
450
  }
292
451
  }
293
452
  mount(context, tools, systemPrompt) {
@@ -298,27 +457,66 @@ export class OnDemandIntake {
298
457
  // When absent, capture binds the validated agent's installed preset reader.
299
458
  this.#startupSkillReader = tools.get('skill')?.execute;
300
459
  if (systemPrompt && !this.options.onlyWhenDeferred)
301
- this.#disposers.push(systemPrompt.section({ name: 'kiokuko:on-demand-intake', order: -99999, text: 'On-demand host contract: text answers do not require execution intake. The public prepare_requested_work tool accepts an advisory type only; task_prepare/task_answer remain host-only. ' + ON_DEMAND_GUIDANCE }));
460
+ this.#disposers.push(systemPrompt.section({ name: 'kiokuko:on-demand-intake', order: -99999, text: 'On-demand host contract: text answers do not require execution intake. The public prepare_requested_work tool accepts an advisory taskType or a source-bound requestIntent; task_prepare/task_answer remain host-only. ' + ON_DEMAND_GUIDANCE }));
302
461
  const definition = { name: TASK_PREPARE_TOOL, modelFacing: true,
303
- description: 'Prepare the original user request for tool-backed work. Supply only an advisory taskType, never a rewritten task or target. This does not grant native tool permissions. Answer ordinary questions directly; clarify unknown actions or targets first.',
304
- parameters: { type: 'object', properties: { taskType: { type: 'string', enum: ACTION_TYPES } }, required: ['taskType'], additionalProperties: false },
462
+ description: 'Prepare the exact original user request for tool-backed work. Supply either legacy advisory taskType or requestIntent with exact evidenceRefs from the host context; never rewrite the task or invent a target. This does not grant native tool permissions. Answer ordinary questions directly; clarify unknown actions or targets first.',
463
+ parameters: { ...JSON.parse(JSON.stringify(z.toJSONSchema(preparationFieldsSchema))), anyOf: [{ required: ['taskType'] }, { required: ['requestIntent'] }] },
305
464
  output: { schema: {}, render: (_args, value) => [{ type: 'text', text: JSON.stringify(value) }] },
306
465
  execute: async (args, execution) => {
307
466
  const state = this.bound(execution);
308
- const value = args;
309
- if (!state || execution.name !== TASK_PREPARE_TOOL || this.definition(execution)?.execute !== definition.execute
310
- || !value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length !== 1 || !ACTION_TYPES.includes(value.taskType))
467
+ const value = parsePreparationRequest(args);
468
+ if (!state || execution.name !== TASK_PREPARE_TOOL || this.definition(execution)?.execute !== definition.execute || !value)
311
469
  throw new Error('Invalid task preparation request');
312
- if (state.preparationCalls.has(execution.callId) || !(state.status === 'answer' || state.existing && state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)))
470
+ if (state.preparationCalls.has(execution.callId) || !(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)))
313
471
  throw new Error('Task preparation was already consumed for this turn');
314
472
  state.preparationCalls.add(execution.callId);
315
473
  if (state.existing) {
316
474
  this.assertCurrent(state);
317
475
  execution.signal.throwIfAborted();
476
+ const boundIntent = this.bindIntent(state, value.requestIntent);
477
+ if (boundIntent?.relation === 'same_scope_continuation' && boundIntent.action !== 'answer' && boundIntent.action !== 'unknown') {
478
+ const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
479
+ if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
480
+ throw new Error('The advisory taskType conflicts with the bound request action');
481
+ }
482
+ state.requestIntent = boundIntent;
483
+ }
484
+ else {
485
+ if (!boundIntent || boundIntent.relation === 'unknown' || boundIntent.action === 'unknown' || boundIntent.action === 'answer') {
486
+ throw new Error('Clarify whether this request continues, changes, or starts work before preparing it');
487
+ }
488
+ const intentType = requestIntentTaskType(boundIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
489
+ if (!intentType || intentType === 'chat' || value.taskType !== undefined && value.taskType !== intentType) {
490
+ throw new Error('The advisory taskType conflicts with the bound request action');
491
+ }
492
+ state.existing = false;
493
+ state.status = 'answer';
494
+ state.memoryOwner = memoryOwnerKind(boundIntent, intentType);
495
+ await this.prepare(state, intentType, execution.signal, boundIntent, value.requestIntent !== undefined);
496
+ }
318
497
  }
319
- else
320
- await this.prepare(state, value.taskType, execution.signal);
321
- return { prepared: true, taskType: state.taskType ?? value.taskType, originalTask: state.task, permissions: 'unchanged_native_policy' };
498
+ else {
499
+ const suppliedIntent = this.bindIntent(state, value.requestIntent);
500
+ const currentIntent = suppliedIntent ?? state.requestIntent;
501
+ const unresolved = currentIntent?.relation === 'unknown' || currentIntent?.action === 'unknown';
502
+ if (unresolved && (state.requestIntentContext?.activeGoal || value.requestIntent !== undefined || value.taskType === undefined)) {
503
+ throw new Error('Clarify the request before preparing work');
504
+ }
505
+ if (state.requestIntentContext?.activeGoal && currentIntent === undefined) {
506
+ throw new Error('Clarify whether this request continues, changes, or starts the active Goal');
507
+ }
508
+ const intentType = unresolved ? undefined : requestIntentTaskType(currentIntent, state.taskType ?? state.intent.taskType ?? value.taskType);
509
+ if (value.taskType !== undefined && intentType !== undefined && value.taskType !== intentType) {
510
+ throw new Error('The advisory taskType conflicts with the bound request action');
511
+ }
512
+ const taskType = intentType ?? value.taskType;
513
+ if (!taskType || taskType === 'chat' || !ACTION_TYPES.includes(taskType))
514
+ throw new Error('Answer ordinary questions directly; clarify unknown actions before preparing work');
515
+ state.memoryOwner = memoryOwnerKind(currentIntent, taskType);
516
+ await this.prepare(state, taskType, execution.signal, currentIntent, value.requestIntent !== undefined);
517
+ }
518
+ return { prepared: true, taskType: state.taskType ?? value.taskType ?? requestIntentTaskType(state.requestIntent), originalTask: state.task,
519
+ ...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner, permissions: 'unchanged_native_policy' };
322
520
  } };
323
521
  this.#disposers.push(tools.register(definition));
324
522
  this.#disposers.push(context.on('tools/pre-execute', async (execution, next) => {
@@ -343,25 +541,29 @@ export class OnDemandIntake {
343
541
  }
344
542
  try {
345
543
  this.assertCurrent(state);
544
+ if (state.confirmationResume && execution.name !== TASK_PREPARE_TOOL && this.host.ready(state.input.agent, state.input.turn))
545
+ this.acknowledgeExisting(state);
346
546
  const skillReader = this.#skillReaders.get(state.input.agent);
347
547
  const skillRead = execution.name === 'skill' && skillReader !== undefined && actual.execute === skillReader && state.status === 'answer';
348
- const carrier = execution.name === 'run_code' && state.status !== 'prepared' ? preparationCarrier(execution.arguments) : undefined;
349
- if (execution.name === 'run_code' && state.status !== 'prepared' && !carrier && !automaticPreparationType(state.intent, execution.name, state.task))
548
+ const carrier = execution.name === 'run_code' ? preparationCarrier(execution.arguments) : undefined;
549
+ const carrierDigest = carrier ? canonicalContentHash(carrier) : undefined;
550
+ if (execution.name === 'run_code' && state.status !== 'prepared' && !carrier
551
+ && !automaticPreparationType(state.intent, state.requestIntent, execution.name))
350
552
  return denied('Clarify the requested work before code execution.');
351
553
  if (carrier) {
352
- if (state.status !== 'answer' || state.preparationCalls.has(execution.callId))
554
+ if (!(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId))
353
555
  return denied('Preparation carrier is not current');
354
556
  }
355
557
  else if (execution.name === TASK_PREPARE_TOOL) {
356
- if (actual.execute !== definition.execute || !(state.status === 'answer' || state.existing && state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId))
558
+ if (actual.execute !== definition.execute || !(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId))
357
559
  return denied('Task preparation is not current');
358
560
  }
359
561
  else if (!skillRead) {
360
562
  if (state.status !== 'prepared') {
361
- const type = automaticPreparationType(state.intent, execution.name, state.task);
563
+ const type = automaticPreparationType(state.intent, state.requestIntent, execution.name);
362
564
  if (!type)
363
565
  return denied('Before tool-backed work, call prepare_requested_work with its advisory taskType. Clarify unknown actions or targets first; the original request is preserved.');
364
- await this.prepare(state, type, execution.signal);
566
+ await this.prepare(state, type, execution.signal, state.requestIntent);
365
567
  }
366
568
  if (!this.host.ready(state.input.agent, state.input.turn))
367
569
  return denied('Prepared execution is no longer current');
@@ -369,7 +571,9 @@ export class OnDemandIntake {
369
571
  this.assertCurrent(state);
370
572
  if (this.definition(execution)?.execute !== actual.execute)
371
573
  return denied('Native tool definition changed during preparation');
372
- this.#executions.set(execution, { turn: state, execute: actual.execute, ...(carrier ? { carrier } : {}), ...(skillRead ? { skillRead: true } : {}), ...(execution.name === TASK_PREPARE_TOOL ? { requestPreparation: true } : {}) });
574
+ this.#executions.set(execution, { turn: state, execute: actual.execute,
575
+ ...(carrier && carrierDigest ? { carrier, carrierDigest } : {}), ...(skillRead ? { skillRead: true } : {}),
576
+ ...(execution.name === TASK_PREPARE_TOOL ? { requestPreparation: true } : {}) });
373
577
  // Preserve native allow/deny/ask unchanged. Approval is resolved by the native registry AFTER this waterfall.
374
578
  const decision = await next();
375
579
  // DSH 0.1.5 does not understand cancel and otherwise dispatches it without running guards.
@@ -392,11 +596,32 @@ export class OnDemandIntake {
392
596
  execution.signal = AbortSignal.any([execution.signal, state.input.signal, this.#lifecycle.signal]);
393
597
  if (!proof.carrier)
394
598
  return next();
395
- if (state.status !== 'answer' || state.preparationCalls.has(execution.callId) || preparationCarrier(execution.arguments) !== proof.carrier)
599
+ const carrier = preparationCarrier(execution.arguments);
600
+ if (!(state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) || state.preparationCalls.has(execution.callId)
601
+ || !carrier || canonicalContentHash(carrier) !== proof.carrierDigest)
396
602
  throw new Error('Preparation carrier was already consumed or changed');
397
603
  state.preparationCalls.add(execution.callId);
398
- await this.prepare(state, proof.carrier, execution.signal);
399
- const result = { prepared: true, taskType: state.taskType, originalTask: state.task, permissions: 'unchanged_native_policy', programExecuted: false };
604
+ const requestIntent = this.bindIntent(state, carrier.requestIntent);
605
+ const clarification = executionClarification(state.task, requestIntent, state.requestIntentContext);
606
+ if (clarification)
607
+ throw new Error(clarification);
608
+ const unresolved = requestIntent?.relation === 'unknown' || requestIntent?.action === 'unknown';
609
+ const legacyTaskTypeAllowed = carrier.taskType !== undefined && !state.requestIntentContext?.activeGoal
610
+ && (state.requestIntentContext?.proposals.length ?? 0) === 0;
611
+ if (unresolved && !legacyTaskTypeAllowed)
612
+ throw new Error('Clarify the requested action and scope before preparing work');
613
+ const intentType = unresolved ? undefined : requestIntentTaskType(requestIntent, state.taskType ?? state.intent.taskType ?? carrier.taskType);
614
+ const taskType = intentType ?? carrier.taskType;
615
+ if (carrier.taskType !== undefined && taskType !== carrier.taskType)
616
+ throw new Error('The advisory taskType conflicts with the bound request action');
617
+ if (!taskType || taskType === 'chat') {
618
+ throw new Error('Clarify the requested action and scope before preparing work');
619
+ }
620
+ state.memoryOwner = memoryOwnerKind(requestIntent, taskType);
621
+ await this.prepare(state, taskType, execution.signal, requestIntent, carrier.requestIntent !== undefined);
622
+ const result = { prepared: true, taskType: state.taskType, originalTask: state.task,
623
+ ...(state.requestIntent ? { requestIntent: state.requestIntent } : {}), memoryOwner: state.memoryOwner,
624
+ permissions: 'unchanged_native_policy', programExecuted: false };
400
625
  return { isError: false, value: { logs: ['Host intake prepared. No program was evaluated.'], result }, content: [{ type: 'text', text: JSON.stringify(result) }] };
401
626
  }, { prepend: true }));
402
627
  this.#disposers.push(tools.guard(execution => {
@@ -417,19 +642,23 @@ export class OnDemandIntake {
417
642
  if (!proof || proof.turn !== state || proof.execute !== actual.execute || ['closed', 'stale', 'capturing'].includes(state.status))
418
643
  return 'Tool execution has no current on-demand preparation';
419
644
  if (proof.carrier)
420
- return state.status === 'answer' && preparationCarrier(execution.arguments) === proof.carrier ? undefined : 'Preparation carrier is not current';
645
+ return (state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn))
646
+ && canonicalContentHash(preparationCarrier(execution.arguments) ?? null) === proof.carrierDigest ? undefined : 'Preparation carrier is not current';
421
647
  if (proof.skillRead)
422
648
  return state.status === 'answer' && actual.execute === this.#skillReaders.get(state.input.agent) ? undefined : 'Conversational Skill read is not current';
423
649
  if (execution.name === TASK_PREPARE_TOOL)
424
- return actual.execute === definition.execute && (state.status === 'answer' || state.existing && state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) ? undefined : 'Task preparation is not current';
650
+ return actual.execute === definition.execute && (state.status === 'answer' || state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn)) ? undefined : 'Task preparation is not current';
425
651
  return state.status === 'prepared' && this.host.ready(state.input.agent, state.input.turn) ? undefined : 'Execution task is not prepared';
426
652
  }));
427
653
  }
428
654
  answerOwner(session) {
429
655
  if (this.#stopped)
430
656
  return undefined;
431
- const state = this.#turns.get(session.id), owner = this.host.answerContext?.owner(session);
432
- return state && state.status !== 'stale' && state.input.agent === owner?.agent ? owner : undefined;
657
+ const state = this.#turns.get(session.id);
658
+ if (!state || state.status !== 'answer' || state.input.agent.session !== session || state.memoryOwner !== 'conversation')
659
+ return undefined;
660
+ const owner = this.host.answerContext?.owner(session, this.ownerIdentity(state));
661
+ return state.input.agent === owner?.agent ? owner : undefined;
433
662
  }
434
663
  retire(agent) {
435
664
  this.host.answerContext?.retire(agent);