@phyzess/pappra 0.0.0-stage → 0.1.0-beta.2

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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -2
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/integrations/skills/pappra/SKILL.md +68 -0
  5. package/assets/integrations/skills/pappra/references/hosts.md +30 -0
  6. package/assets/integrations/skills/pappra/references/lifecycle.md +37 -0
  7. package/assets/integrations/skills/pappra/references/operations.md +14 -0
  8. package/assets/manifest.json +4 -0
  9. package/dist/app/collaboration.d.ts +170 -0
  10. package/dist/app/collaboration.js +1650 -0
  11. package/dist/app/doctor.d.ts +9 -0
  12. package/dist/app/doctor.js +1030 -0
  13. package/dist/app/init.d.ts +143 -0
  14. package/dist/app/init.js +1111 -0
  15. package/dist/cli/commands/code.d.ts +16 -0
  16. package/dist/cli/commands/code.js +127 -0
  17. package/dist/cli/commands/collaboration.d.ts +13 -0
  18. package/dist/cli/commands/collaboration.js +186 -0
  19. package/dist/cli/commands/continuity.d.ts +29 -0
  20. package/dist/cli/commands/continuity.js +133 -0
  21. package/dist/cli/commands/docs.d.ts +11 -0
  22. package/dist/cli/commands/docs.js +133 -0
  23. package/dist/cli/commands/doctor.d.ts +14 -0
  24. package/dist/cli/commands/doctor.js +57 -0
  25. package/dist/cli/commands/host.d.ts +13 -0
  26. package/dist/cli/commands/host.js +125 -0
  27. package/dist/cli/commands/init.d.ts +51 -0
  28. package/dist/cli/commands/init.js +668 -0
  29. package/dist/cli/main.d.ts +2 -0
  30. package/dist/cli/main.js +10 -0
  31. package/dist/cli/output.d.ts +4 -0
  32. package/dist/cli/output.js +1 -0
  33. package/dist/cli/run.d.ts +2 -0
  34. package/dist/cli/run.js +597 -0
  35. package/dist/code/budget.d.ts +14 -0
  36. package/dist/code/budget.js +39 -0
  37. package/dist/code/exec.d.ts +23 -0
  38. package/dist/code/exec.js +133 -0
  39. package/dist/code/identity.d.ts +13 -0
  40. package/dist/code/identity.js +214 -0
  41. package/dist/code/index.d.ts +6 -0
  42. package/dist/code/index.js +3 -0
  43. package/dist/code/providers/codegraph.d.ts +59 -0
  44. package/dist/code/providers/codegraph.js +567 -0
  45. package/dist/code/registration.d.ts +9 -0
  46. package/dist/code/registration.js +293 -0
  47. package/dist/code/service.d.ts +53 -0
  48. package/dist/code/service.js +633 -0
  49. package/dist/code/text.d.ts +3 -0
  50. package/dist/code/text.js +202 -0
  51. package/dist/collab/admission.d.ts +450 -0
  52. package/dist/collab/admission.js +797 -0
  53. package/dist/collab/canonical.d.ts +26 -0
  54. package/dist/collab/canonical.js +136 -0
  55. package/dist/collab/contracts.d.ts +73 -0
  56. package/dist/collab/contracts.js +76 -0
  57. package/dist/collab/envelope.d.ts +52 -0
  58. package/dist/collab/envelope.js +241 -0
  59. package/dist/collab/fingerprint.d.ts +7 -0
  60. package/dist/collab/fingerprint.js +58 -0
  61. package/dist/collab/handoff.d.ts +350 -0
  62. package/dist/collab/handoff.js +513 -0
  63. package/dist/collab/index.d.ts +9 -0
  64. package/dist/collab/index.js +8 -0
  65. package/dist/collab/lifecycle.d.ts +449 -0
  66. package/dist/collab/lifecycle.js +534 -0
  67. package/dist/collab/messages.d.ts +148 -0
  68. package/dist/collab/messages.js +583 -0
  69. package/dist/collab/notify.d.ts +47 -0
  70. package/dist/collab/notify.js +140 -0
  71. package/dist/collab/preflight.d.ts +31 -0
  72. package/dist/collab/preflight.js +271 -0
  73. package/dist/collab/resources.d.ts +119 -0
  74. package/dist/collab/resources.js +451 -0
  75. package/dist/collab/store.d.ts +143 -0
  76. package/dist/collab/store.js +606 -0
  77. package/dist/collab/types.d.ts +354 -0
  78. package/dist/collab/types.js +1 -0
  79. package/dist/collab/validation.d.ts +10 -0
  80. package/dist/collab/validation.js +303 -0
  81. package/dist/continuity/context.d.ts +24 -0
  82. package/dist/continuity/context.js +179 -0
  83. package/dist/continuity/index.d.ts +3 -0
  84. package/dist/continuity/index.js +1 -0
  85. package/dist/continuity/paths.d.ts +37 -0
  86. package/dist/continuity/paths.js +258 -0
  87. package/dist/continuity/service.d.ts +59 -0
  88. package/dist/continuity/service.js +1258 -0
  89. package/dist/hosts/claude/native.d.ts +79 -0
  90. package/dist/hosts/claude/native.js +853 -0
  91. package/dist/hosts/codex/native.d.ts +345 -0
  92. package/dist/hosts/codex/native.js +1528 -0
  93. package/dist/hosts/codex/protocol.d.ts +51 -0
  94. package/dist/hosts/codex/protocol.js +42 -0
  95. package/dist/hosts/detect.d.ts +28 -0
  96. package/dist/hosts/detect.js +73 -0
  97. package/dist/hosts/index.d.ts +10 -0
  98. package/dist/hosts/index.js +10 -0
  99. package/dist/hosts/integration/bundle.d.ts +16 -0
  100. package/dist/hosts/integration/bundle.js +81 -0
  101. package/dist/hosts/integration/hook-entry.d.ts +1 -0
  102. package/dist/hosts/integration/hook-entry.js +68 -0
  103. package/dist/hosts/integration/hook.d.ts +32 -0
  104. package/dist/hosts/integration/hook.js +781 -0
  105. package/dist/hosts/integration/pi-extension-runtime.d.ts +18 -0
  106. package/dist/hosts/integration/pi-extension-runtime.js +89 -0
  107. package/dist/hosts/integration/pi-template.d.ts +3 -0
  108. package/dist/hosts/integration/pi-template.js +5 -0
  109. package/dist/hosts/integration/service.d.ts +41 -0
  110. package/dist/hosts/integration/service.js +1381 -0
  111. package/dist/hosts/integration/support.d.ts +36 -0
  112. package/dist/hosts/integration/support.js +41 -0
  113. package/dist/hosts/integration/templates.d.ts +22 -0
  114. package/dist/hosts/integration/templates.js +70 -0
  115. package/dist/hosts/pi/protocol.d.ts +22 -0
  116. package/dist/hosts/pi/protocol.js +28 -0
  117. package/dist/hosts/pi/query-result.d.ts +22 -0
  118. package/dist/hosts/pi/query-result.js +710 -0
  119. package/dist/hosts/pi/rpc.d.ts +937 -0
  120. package/dist/hosts/pi/rpc.js +1621 -0
  121. package/dist/hosts/pilot.d.ts +485 -0
  122. package/dist/hosts/pilot.js +593 -0
  123. package/dist/index.d.ts +5 -0
  124. package/dist/index.js +4 -0
  125. package/dist/infra/fs.d.ts +27 -0
  126. package/dist/infra/fs.js +130 -0
  127. package/dist/infra/git.d.ts +60 -0
  128. package/dist/infra/git.js +358 -0
  129. package/dist/infra/owned-process-group.d.ts +173 -0
  130. package/dist/infra/owned-process-group.js +571 -0
  131. package/dist/install/apply.d.ts +65 -0
  132. package/dist/install/apply.js +663 -0
  133. package/dist/install/index.d.ts +5 -0
  134. package/dist/install/index.js +5 -0
  135. package/dist/install/ownership.d.ts +85 -0
  136. package/dist/install/ownership.js +430 -0
  137. package/dist/install/plan.d.ts +39 -0
  138. package/dist/install/plan.js +97 -0
  139. package/dist/install/rollback.d.ts +18 -0
  140. package/dist/install/rollback.js +544 -0
  141. package/dist/install/runtime.d.ts +76 -0
  142. package/dist/install/runtime.js +1145 -0
  143. package/dist/knowledge/catalog.d.ts +47 -0
  144. package/dist/knowledge/catalog.js +308 -0
  145. package/dist/knowledge/context.d.ts +28 -0
  146. package/dist/knowledge/context.js +61 -0
  147. package/dist/knowledge/index.d.ts +3 -0
  148. package/dist/knowledge/index.js +1 -0
  149. package/dist/knowledge/journal.d.ts +16 -0
  150. package/dist/knowledge/journal.js +109 -0
  151. package/dist/knowledge/lifecycle.d.ts +8 -0
  152. package/dist/knowledge/lifecycle.js +1001 -0
  153. package/dist/knowledge/paths.d.ts +37 -0
  154. package/dist/knowledge/paths.js +236 -0
  155. package/dist/knowledge/refs.d.ts +36 -0
  156. package/dist/knowledge/refs.js +267 -0
  157. package/dist/knowledge/service.d.ts +21 -0
  158. package/dist/knowledge/service.js +393 -0
  159. package/dist/knowledge/types.d.ts +171 -0
  160. package/dist/knowledge/types.js +1 -0
  161. package/dist/knowledge/write.d.ts +8 -0
  162. package/dist/knowledge/write.js +1223 -0
  163. package/dist/package-info.d.ts +6 -0
  164. package/dist/package-info.js +19 -0
  165. package/dist/schemas/code.d.ts +377 -0
  166. package/dist/schemas/code.js +145 -0
  167. package/dist/schemas/collaboration.d.ts +115 -0
  168. package/dist/schemas/collaboration.js +106 -0
  169. package/dist/schemas/continuity.d.ts +315 -0
  170. package/dist/schemas/continuity.js +199 -0
  171. package/dist/schemas/diagnostics.d.ts +28 -0
  172. package/dist/schemas/diagnostics.js +27 -0
  173. package/dist/schemas/doctor.d.ts +157 -0
  174. package/dist/schemas/doctor.js +75 -0
  175. package/dist/schemas/document.d.ts +644 -0
  176. package/dist/schemas/document.js +296 -0
  177. package/dist/schemas/errors.d.ts +177 -0
  178. package/dist/schemas/errors.js +176 -0
  179. package/dist/schemas/evidence.d.ts +24 -0
  180. package/dist/schemas/evidence.js +16 -0
  181. package/dist/schemas/host.d.ts +436 -0
  182. package/dist/schemas/host.js +201 -0
  183. package/dist/schemas/index.d.ts +15 -0
  184. package/dist/schemas/index.js +15 -0
  185. package/dist/schemas/init.d.ts +121 -0
  186. package/dist/schemas/init.js +94 -0
  187. package/dist/schemas/install.d.ts +213 -0
  188. package/dist/schemas/install.js +139 -0
  189. package/dist/schemas/machine.d.ts +43 -0
  190. package/dist/schemas/machine.js +96 -0
  191. package/dist/schemas/outcome.d.ts +23 -0
  192. package/dist/schemas/outcome.js +17 -0
  193. package/dist/schemas/paths.d.ts +19 -0
  194. package/dist/schemas/paths.js +63 -0
  195. package/dist/schemas/workspace.d.ts +120 -0
  196. package/dist/schemas/workspace.js +201 -0
  197. package/dist/workspace/config-file.d.ts +36 -0
  198. package/dist/workspace/config-file.js +237 -0
  199. package/dist/workspace/config.d.ts +28 -0
  200. package/dist/workspace/config.js +79 -0
  201. package/dist/workspace/discover.d.ts +34 -0
  202. package/dist/workspace/discover.js +195 -0
  203. package/dist/workspace/documents.d.ts +21 -0
  204. package/dist/workspace/documents.js +395 -0
  205. package/dist/workspace/ignore.d.ts +10 -0
  206. package/dist/workspace/ignore.js +48 -0
  207. package/dist/workspace/index.d.ts +7 -0
  208. package/dist/workspace/index.js +3 -0
  209. package/dist/workspace/repositories.d.ts +24 -0
  210. package/dist/workspace/repositories.js +411 -0
  211. package/dist/workspace/resolve.d.ts +24 -0
  212. package/dist/workspace/resolve.js +268 -0
  213. package/dist/workspace/types.d.ts +71 -0
  214. package/dist/workspace/types.js +1 -0
  215. package/dist/workspace/validation.d.ts +18 -0
  216. package/dist/workspace/validation.js +197 -0
  217. package/package.json +105 -4
  218. package/skills/pappra/SKILL.md +68 -0
  219. package/skills/pappra/references/hosts.md +30 -0
  220. package/skills/pappra/references/lifecycle.md +37 -0
  221. package/skills/pappra/references/operations.md +14 -0
@@ -0,0 +1,853 @@
1
+ import { errorCode, object, parseJson } from '../../collab/validation.js';
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { buildMessage } from '../../collab/envelope.js';
7
+ import { submitMessage } from '../../collab/messages.js';
8
+ import { readRecord } from '../../collab/store.js';
9
+ // Native background-session query/cancel for Claude Code 2.1.292 as installed.
10
+ //
11
+ // Observed installed shape (evidence: claude-background-agents.json,
12
+ // claude-raw-stop-proof.json, task completion-20261016-a9f2):
13
+ // active row: {pid, id, cwd, kind:'background', startedAt, sessionId, name,
14
+ // status:'idle'|'busy', state:'blocked'|'working'}
15
+ // terminal row: {id, cwd, kind:'background', startedAt, sessionId, name, state:'stopped'|'done'}
16
+ //
17
+ // Terminal shape observed on 2.1.292 after a real model reply plus native stop: `claude agents
18
+ // --json --all --cwd` returns `state:'done'` (not `stopped`) with the same id/sessionId/cwd/kind
19
+ // and no pid/status. `done` is a terminal lifecycle observation only; it is never a completed
20
+ // business result (evidence/claude-model-recovery.json).
21
+ //
22
+ // * `sessionId` is the canonical native UUID; `id` (8 hex) is the CLI command key.
23
+ // `claude logs <UUID>` exits 1 while `claude logs <id>` succeeds, so logs/stop always use
24
+ // the exact associated command key, and the UUID is matched exactly (never a name/prefix).
25
+ // * `--bg` ignores `--session-id` and picks its own UUID, so a requested UUID is never
26
+ // treated as the observed one.
27
+ // * `status:'idle'` is a live background process, not terminal task completion.
28
+ // * The post-stop row keeps id/sessionId/cwd/kind with `state:'stopped'` and exposes no
29
+ // pid/status; its `startedAt` is a service start time, not the process start time, so it is
30
+ // never compared against the active row's `startedAt`. Absence of a terminal row is
31
+ // `unknown`, not proof.
32
+ //
33
+ // It is deliberately NOT a print-session takeover: it never prompts, resumes, removes,
34
+ // respawns or attaches. It never uses the shared record as native status.
35
+ const SCHEMA = 'harness-collab/claude-native-session/1';
36
+ const MAX_TIMEOUT_MS = 30000;
37
+ const DEFAULT_TIMEOUT_MS = 10000;
38
+ const MAX_BYTES = 4 * 1024 * 1024;
39
+ const DEFAULT_POLL_MS = 500;
40
+ const DEFAULT_STOP_TIMEOUT_MS = 15000;
41
+ const DEFAULT_STREAM_GRACE_MS = 500;
42
+ const ACCEPTED_HARNESSES = new Set(['claude', 'claude-code']);
43
+ const EXPECTED_RUNNER = 'claude-background-cli';
44
+ const COMMAND_KEY = /^[0-9a-f]{8}$/;
45
+ const ACTIVE_STATUS = new Set(['idle', 'busy', 'running', 'active', 'working']);
46
+ const ACTIVE_STATE = new Set(['blocked', 'working']);
47
+ // `done` is the observed post-stop shape after a real model reply (2.1.292). It is terminal for
48
+ // lifecycle purposes only and is never mapped to a completed task result.
49
+ const TERMINAL_STATE = new Set([
50
+ 'done',
51
+ 'stopped',
52
+ 'exited',
53
+ 'dead',
54
+ 'ended',
55
+ 'finished',
56
+ 'completed',
57
+ 'cancelled',
58
+ ]);
59
+ export class ClaudeNativeRefusal extends Error {
60
+ status;
61
+ code;
62
+ detail;
63
+ constructor(status, code, detail = null) {
64
+ super(code);
65
+ this.status = status;
66
+ this.code = code;
67
+ this.detail = detail;
68
+ this.status = status;
69
+ this.code = code;
70
+ this.detail = detail;
71
+ }
72
+ }
73
+ function refuse(code, status = 'invalid', detail = null) {
74
+ throw new ClaudeNativeRefusal(status, code, detail);
75
+ }
76
+ const sha256 = (bytes) => `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
77
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
78
+ const nowIso = () => new Date().toISOString();
79
+ const realpath = (file) => {
80
+ try {
81
+ return fs.realpathSync(file);
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ };
87
+ const resolvePath = (file) => realpath(file) ?? path.resolve(file);
88
+ function absolute(value, code) {
89
+ if (typeof value !== 'string' ||
90
+ !path.isAbsolute(value) ||
91
+ value.includes('\0'))
92
+ refuse(code);
93
+ return value;
94
+ }
95
+ function bounded(value, fallback, code, cap = MAX_TIMEOUT_MS) {
96
+ const chosen = value ?? fallback;
97
+ if (!Number.isSafeInteger(chosen) || chosen < 1 || chosen > cap)
98
+ refuse(code);
99
+ return chosen;
100
+ }
101
+ function normalize(options) {
102
+ const timeoutMs = bounded(options.timeoutMs, DEFAULT_TIMEOUT_MS, 'timeout_out_of_range');
103
+ if (!Number.isSafeInteger(options.attempt) || options.attempt < 1)
104
+ refuse('invalid_attempt');
105
+ if (!Number.isSafeInteger(options.ownerEpoch) || options.ownerEpoch < 1)
106
+ refuse('invalid_owner_epoch');
107
+ for (const key of ['taskRoot', 'agentId', 'sessionId']) {
108
+ if (typeof options[key] !== 'string' || !options[key].trim())
109
+ refuse(`${key}_required`);
110
+ }
111
+ return {
112
+ schema: SCHEMA,
113
+ claudeBin: typeof options.claudeBin === 'string' && options.claudeBin
114
+ ? options.claudeBin
115
+ : 'claude',
116
+ env: options.env ?? undefined,
117
+ taskRoot: absolute(options.taskRoot, 'task_root_must_be_absolute'),
118
+ subtaskId: typeof options.subtaskId === 'string' && options.subtaskId.trim()
119
+ ? options.subtaskId
120
+ : refuse('subtask_required'),
121
+ attempt: options.attempt,
122
+ ownerEpoch: options.ownerEpoch,
123
+ agentId: options.agentId,
124
+ sessionId: options.sessionId,
125
+ nativeSessionId: options.nativeSessionId ?? null,
126
+ cwd: absolute(options.cwd, 'cwd_must_be_absolute'),
127
+ expectedPid: options.expectedPid ?? null,
128
+ expectedStartedAt: options.expectedStartedAt ?? null,
129
+ evidenceDir: options.evidenceDir === undefined || options.evidenceDir === null
130
+ ? null
131
+ : absolute(options.evidenceDir, 'evidence_dir_must_be_absolute'),
132
+ timeoutMs,
133
+ pollMs: bounded(options.pollMs, DEFAULT_POLL_MS, 'poll_ms_out_of_range'),
134
+ stopTimeoutMs: bounded(options.stopTimeoutMs, DEFAULT_STOP_TIMEOUT_MS, 'stop_timeout_out_of_range'),
135
+ maxBytes: bounded(options.maxBytes, MAX_BYTES, 'max_bytes_out_of_range', MAX_BYTES),
136
+ streamGraceMs: bounded(options.streamGraceMs, DEFAULT_STREAM_GRACE_MS, 'stream_grace_out_of_range'),
137
+ };
138
+ }
139
+ /**
140
+ * Bounded local subprocess capture. Parent exit is separate from stream close: after the parent
141
+ * exits, only this module's own pipe handles are detached after a bounded grace, so a descendant
142
+ * that inherited stdout cannot hold the promise open past its deadline. No descendant is
143
+ * signalled. `streams_detached: true` means the output may be incomplete.
144
+ */
145
+ export function runBounded(bin, args, options = {}) {
146
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
147
+ const maxBytes = options.maxBytes ?? MAX_BYTES;
148
+ const streamGraceMs = options.streamGraceMs ?? DEFAULT_STREAM_GRACE_MS;
149
+ return new Promise((resolve) => {
150
+ const settle = (payload) => resolve(payload);
151
+ let child;
152
+ try {
153
+ child = spawn(bin, args, {
154
+ cwd: options.cwd,
155
+ env: options.env,
156
+ stdio: ['ignore', 'pipe', 'pipe'],
157
+ });
158
+ }
159
+ catch (error) {
160
+ settle({
161
+ exit_code: null,
162
+ signal: null,
163
+ parent_exited: false,
164
+ stream_closed: false,
165
+ streams_detached: false,
166
+ timed_out: false,
167
+ killed_after_eof_timeout: false,
168
+ spawn_failed: true,
169
+ spawn_error: error instanceof Error ? error.message : String(error),
170
+ truncated: false,
171
+ stdout: Buffer.alloc(0),
172
+ stderr: Buffer.alloc(0),
173
+ });
174
+ return;
175
+ }
176
+ const out = [];
177
+ const err = [];
178
+ let bytes = 0;
179
+ let truncated = false;
180
+ let timedOut = false;
181
+ let killed = false;
182
+ let spawnError = null;
183
+ let parentExited = false;
184
+ let streamClosed = false;
185
+ let streamsDetached = false;
186
+ let exitCode = null;
187
+ let signal = null;
188
+ let done = false;
189
+ let timer;
190
+ let grace;
191
+ const finish = () => {
192
+ if (done)
193
+ return;
194
+ done = true;
195
+ clearTimeout(timer);
196
+ clearTimeout(grace);
197
+ settle({
198
+ exit_code: exitCode,
199
+ signal,
200
+ parent_exited: parentExited,
201
+ stream_closed: streamClosed,
202
+ streams_detached: streamsDetached,
203
+ timed_out: timedOut,
204
+ killed_after_eof_timeout: killed,
205
+ spawn_failed: spawnError !== null,
206
+ spawn_error: spawnError?.message ?? null,
207
+ truncated,
208
+ stdout: Buffer.concat(out),
209
+ stderr: Buffer.concat(err),
210
+ });
211
+ };
212
+ const collect = (list) => (chunk) => {
213
+ const room = Math.max(0, maxBytes - bytes);
214
+ if (room > 0)
215
+ list.push(chunk.subarray(0, room));
216
+ bytes += Math.min(room, chunk.length);
217
+ if (chunk.length > room)
218
+ truncated = true;
219
+ };
220
+ for (const [stream, list] of [
221
+ [child.stdout, out],
222
+ [child.stderr, err],
223
+ ]) {
224
+ stream?.on('data', collect(list));
225
+ stream?.on('error', () => {
226
+ /* drained; the exit path reports the outcome */
227
+ });
228
+ }
229
+ child.stdin?.on('error', () => {
230
+ /* drained */
231
+ });
232
+ child.on('error', (error) => {
233
+ spawnError ??= error;
234
+ });
235
+ child.on('exit', (code, sig) => {
236
+ parentExited = true;
237
+ exitCode = code;
238
+ signal = sig;
239
+ grace = setTimeout(() => {
240
+ if (streamClosed)
241
+ return;
242
+ streamsDetached = true;
243
+ for (const stream of [child.stdin, child.stdout, child.stderr]) {
244
+ try {
245
+ stream?.destroy();
246
+ }
247
+ catch {
248
+ /* already gone */
249
+ }
250
+ }
251
+ setTimeout(finish, 25);
252
+ }, streamGraceMs);
253
+ });
254
+ child.on('close', (code, sig) => {
255
+ parentExited = true;
256
+ if (exitCode === null) {
257
+ exitCode = code;
258
+ signal = sig;
259
+ }
260
+ streamClosed = true;
261
+ clearTimeout(grace);
262
+ finish();
263
+ });
264
+ timer = setTimeout(() => {
265
+ timedOut = true;
266
+ killed = true;
267
+ try {
268
+ child.kill('SIGKILL');
269
+ }
270
+ catch {
271
+ /* already gone */
272
+ }
273
+ }, timeoutMs);
274
+ });
275
+ }
276
+ /** The record supplies the expected identity only; it never supplies queried execution facts. */
277
+ function requireRecordTarget(opts) {
278
+ const record = readRecord(opts.taskRoot);
279
+ if (!record)
280
+ refuse('task_record_unavailable', 'needs_input');
281
+ if (record.coordinator?.owner_epoch !== opts.ownerEpoch)
282
+ refuse('stale_epoch', 'needs_input');
283
+ const target = (record.attempts ?? []).find((item) => item.subtask_id === opts.subtaskId && item.attempt === opts.attempt);
284
+ if (!target)
285
+ refuse('unknown_attempt', 'needs_input');
286
+ const assignee = target.assignee ?? {};
287
+ if (assignee.agent_id !== opts.agentId ||
288
+ assignee.session_id !== opts.sessionId)
289
+ refuse('wrong_assignee', 'needs_input');
290
+ if (!ACCEPTED_HARNESSES.has(assignee.harness ?? ''))
291
+ refuse('native_harness_mismatch', 'needs_input', `assignee harness ${assignee.harness ?? 'unknown'}`);
292
+ const native = target.native ?? {};
293
+ if (native.runner !== EXPECTED_RUNNER)
294
+ refuse('native_runner_mismatch', 'needs_input', `native runner ${native.runner ?? 'unknown'}`);
295
+ const nativeSessionId = native.session_id ?? null;
296
+ const commandKey = native.run_id ?? null;
297
+ if (nativeSessionId === null)
298
+ refuse('native_session_unrecorded', 'needs_input');
299
+ if (opts.nativeSessionId !== null && opts.nativeSessionId !== nativeSessionId)
300
+ refuse('native_session_ref_mismatch', 'needs_input');
301
+ if (typeof commandKey !== 'string' || !COMMAND_KEY.test(commandKey))
302
+ refuse('native_command_key_unrecorded', 'needs_input');
303
+ const recordedCwd = target.cwd ?? assignee.cwd ?? null;
304
+ if (recordedCwd !== null &&
305
+ resolvePath(recordedCwd) !== resolvePath(opts.cwd))
306
+ refuse('native_cwd_mismatch', 'needs_input');
307
+ opts.record = record;
308
+ opts.assigneeAgent = assignee.agent_id;
309
+ opts.assigneeSession = assignee.session_id;
310
+ opts.assigneeHarness = assignee.harness;
311
+ return { record, target, nativeSessionId, commandKey };
312
+ }
313
+ async function listSessions(opts) {
314
+ const result = await runBounded(opts.claudeBin, ['agents', '--json', '--all', '--cwd', opts.cwd], {
315
+ cwd: opts.cwd,
316
+ env: opts.env,
317
+ timeoutMs: opts.timeoutMs,
318
+ maxBytes: opts.maxBytes,
319
+ streamGraceMs: opts.streamGraceMs,
320
+ });
321
+ if (result.spawn_failed)
322
+ refuse('spawn_failed', 'needs_input', result.spawn_error);
323
+ if (result.timed_out)
324
+ refuse('native_discovery_timeout', 'needs_input');
325
+ if (!result.parent_exited)
326
+ refuse('native_discovery_parent_not_exited', 'needs_input');
327
+ if (result.streams_detached)
328
+ refuse('native_discovery_stream_unproven', 'needs_input');
329
+ if (result.exit_code !== 0)
330
+ refuse('native_discovery_failed', 'needs_input', `agents exited ${result.exit_code}`);
331
+ if (result.truncated)
332
+ refuse('native_discovery_truncated', 'needs_input');
333
+ const text = result.stdout.toString('utf8').trim();
334
+ let parsed;
335
+ try {
336
+ parsed = parseJson(text);
337
+ }
338
+ catch {
339
+ refuse('native_agents_json_invalid', 'needs_input');
340
+ }
341
+ if (!Array.isArray(parsed))
342
+ refuse('native_agents_json_invalid', 'needs_input');
343
+ if (!parsed.every(object))
344
+ refuse('native_agents_json_invalid', 'needs_input');
345
+ return { rows: parsed, exit_code: result.exit_code, captured_at: nowIso() };
346
+ }
347
+ function classifyRow(row) {
348
+ if (typeof row.state === 'string' && TERMINAL_STATE.has(row.state))
349
+ return {
350
+ observation: 'terminal',
351
+ native_state: row.state,
352
+ native_status: typeof row.status === 'string' ? row.status : null,
353
+ };
354
+ if (typeof row.status === 'string' && ACTIVE_STATUS.has(row.status))
355
+ return {
356
+ observation: 'active',
357
+ native_state: typeof row.state === 'string' ? row.state : null,
358
+ native_status: row.status,
359
+ };
360
+ if (typeof row.state === 'string' && ACTIVE_STATE.has(row.state))
361
+ return {
362
+ observation: 'active',
363
+ native_state: row.state,
364
+ native_status: typeof row.status === 'string' ? row.status : null,
365
+ };
366
+ refuse('native_status_unsupported', 'needs_input', `status=${row.status ?? 'null'} state=${row.state ?? 'null'}`);
367
+ }
368
+ /**
369
+ * Exact canonical UUID + exact cwd + exact CLI command key + background kind. Pins (pid and the
370
+ * active-row startedAt) are only applied to a live row, because the post-stop row deliberately
371
+ * has no pid/status and its startedAt is a different thing entirely.
372
+ */
373
+ function matchRow(rows, { nativeSessionId, commandKey, cwd, expectedPid, expectedStartedAt, }) {
374
+ const matches = rows.filter((row) => row.sessionId === nativeSessionId);
375
+ if (matches.length === 0)
376
+ refuse('native_session_absent', 'needs_input');
377
+ if (matches.length > 1)
378
+ refuse('native_session_ambiguous', 'needs_input');
379
+ const row = matches[0];
380
+ if (row.kind !== 'background')
381
+ refuse('native_kind_unsupported', 'needs_input', String(row.kind ?? 'unknown'));
382
+ if (row.id !== commandKey)
383
+ refuse('native_command_key_mismatch', 'needs_input', `row id ${row.id ?? 'unknown'}`);
384
+ if (typeof row.cwd !== 'string' || resolvePath(row.cwd) !== resolvePath(cwd))
385
+ refuse('native_cwd_mismatch', 'needs_input');
386
+ const classified = classifyRow(row);
387
+ if (classified.observation === 'active') {
388
+ if (expectedPid !== null &&
389
+ row.pid !== undefined &&
390
+ row.pid !== expectedPid)
391
+ refuse('native_pid_mismatch', 'needs_input');
392
+ if (expectedPid !== null && row.pid === undefined)
393
+ refuse('native_pid_unsupported', 'needs_input');
394
+ if (expectedStartedAt !== null && row.startedAt !== expectedStartedAt)
395
+ refuse('native_started_at_mismatch', 'needs_input');
396
+ }
397
+ return {
398
+ row,
399
+ observation: classified.observation,
400
+ native_state: classified.native_state,
401
+ native_status: classified.native_status,
402
+ command_key: row.id,
403
+ };
404
+ }
405
+ function tryWritePrivate(file, bytes) {
406
+ try {
407
+ fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
408
+ return { ref: file, digest: sha256(bytes), bytes: bytes.length };
409
+ }
410
+ catch (error) {
411
+ return {
412
+ error: errorCode(error) === 'EEXIST'
413
+ ? 'evidence_exists'
414
+ : 'evidence_write_failed',
415
+ };
416
+ }
417
+ }
418
+ /** Bounded exact `logs <command-key>` read; raw output is only persisted privately. */
419
+ async function readLogs(opts, commandKey) {
420
+ const result = await runBounded(opts.claudeBin, ['logs', commandKey], {
421
+ cwd: opts.cwd,
422
+ env: opts.env,
423
+ timeoutMs: opts.timeoutMs,
424
+ maxBytes: opts.maxBytes,
425
+ streamGraceMs: opts.streamGraceMs,
426
+ });
427
+ if (result.spawn_failed)
428
+ refuse('spawn_failed', 'needs_input', result.spawn_error);
429
+ if (result.timed_out)
430
+ refuse('native_logs_timeout', 'needs_input');
431
+ const body = result.stdout;
432
+ const evidence = {
433
+ command_key: commandKey,
434
+ exit_code: result.exit_code,
435
+ parent_exited: result.parent_exited,
436
+ stream_closed: result.stream_closed,
437
+ streams_detached: result.streams_detached,
438
+ timed_out: result.timed_out,
439
+ truncated: result.truncated,
440
+ lines: body
441
+ .toString('utf8')
442
+ .split('\n')
443
+ .filter((line) => line.length > 0).length,
444
+ bytes: body.length,
445
+ digest: sha256(body),
446
+ // Native terminal output commonly carries ANSI control sequences; the raw bytes are kept
447
+ // private and never interpreted into a model receipt.
448
+ ansi_sequences_present: body.includes(0x1b),
449
+ captured_at: nowIso(),
450
+ };
451
+ if (opts.evidenceDir) {
452
+ try {
453
+ fs.mkdirSync(opts.evidenceDir, { recursive: true, mode: 0o700 });
454
+ }
455
+ catch {
456
+ /* reported below */
457
+ }
458
+ const written = tryWritePrivate(path.join(opts.evidenceDir, 'claude-native-logs.txt'), body);
459
+ if (written.error)
460
+ evidence.evidence_error = written.error;
461
+ else
462
+ evidence.raw_evidence = written;
463
+ }
464
+ return evidence;
465
+ }
466
+ function ids(opts) {
467
+ if (!opts.record?.coordinator || !opts.assigneeAgent || !opts.assigneeSession)
468
+ refuse('protocol_identity_unavailable');
469
+ const coordinator = opts.record.coordinator;
470
+ return {
471
+ taskId: opts.record.task_id,
472
+ from: {
473
+ agent_id: coordinator.agent_id,
474
+ session_id: coordinator.session_id,
475
+ harness: coordinator.harness ?? 'pi',
476
+ },
477
+ to: {
478
+ agent_id: opts.assigneeAgent,
479
+ session_id: opts.assigneeSession,
480
+ harness: opts.assigneeHarness ?? 'claude',
481
+ },
482
+ };
483
+ }
484
+ /** Registers the query intent before any native read; returns the message id. */
485
+ function registerStatusQuery(opts) {
486
+ const { taskId, from, to } = ids(opts);
487
+ const query = submitMessage(opts.taskRoot, buildMessage({
488
+ type: 'STATUS_QUERY',
489
+ task_id: taskId,
490
+ from,
491
+ to,
492
+ owner_epoch: opts.ownerEpoch,
493
+ body: { reason: 'native background-session observation' },
494
+ }));
495
+ if (query.ok !== true)
496
+ refuse('status_query_not_recorded', 'invalid', query.reason ?? null);
497
+ return query.registration.message_id;
498
+ }
499
+ /** Reports the factual native observation; never maps a terminal session to task completion. */
500
+ function reportStatus(opts, queryId, observed) {
501
+ const { taskId, from, to } = ids(opts);
502
+ const status = submitMessage(opts.taskRoot, buildMessage({
503
+ type: 'STATUS',
504
+ task_id: taskId,
505
+ from: to,
506
+ to: from,
507
+ owner_epoch: opts.ownerEpoch,
508
+ reply_to: queryId,
509
+ subtask_id: opts.subtaskId,
510
+ attempt: opts.attempt,
511
+ body: {
512
+ status: observed.observation === 'active' ? 'running' : 'unknown',
513
+ native_session_id: observed.native_session_id,
514
+ native_command_key: observed.command_key,
515
+ native_state: observed.native_state,
516
+ native_status: observed.native_status,
517
+ native_cwd: observed.cwd,
518
+ observation: observed.observation,
519
+ evidence_ref: typeof observed.evidence_ref === 'string'
520
+ ? observed.evidence_ref
521
+ : null,
522
+ },
523
+ }));
524
+ if (status.ok !== true)
525
+ refuse('status_not_recorded', 'invalid', status.reason ?? null);
526
+ return status.registration.message_id;
527
+ }
528
+ function nativePayload(opts, nativeSessionId, matched, extra) {
529
+ return {
530
+ schema: SCHEMA,
531
+ task_id: opts.taskId ?? undefined,
532
+ subtask_id: opts.subtaskId,
533
+ attempt: opts.attempt,
534
+ owner_epoch: opts.ownerEpoch,
535
+ native: {
536
+ session_id: nativeSessionId,
537
+ command_key: matched.command_key,
538
+ cwd: matched.row.cwd,
539
+ kind: matched.row.kind,
540
+ status: matched.row.status ?? null,
541
+ state: matched.row.state ?? null,
542
+ pid: Number.isSafeInteger(matched.row.pid) ? matched.row.pid : null,
543
+ started_at: Number.isSafeInteger(matched.row.startedAt)
544
+ ? matched.row.startedAt
545
+ : null,
546
+ },
547
+ ...extra,
548
+ };
549
+ }
550
+ function refusalResult(schema, error, extra = {}) {
551
+ const known = error instanceof ClaudeNativeRefusal;
552
+ return {
553
+ schema,
554
+ status: known ? error.status : 'invalid',
555
+ blockers: [known ? error.code : 'internal_error'],
556
+ detail: known
557
+ ? error.detail
558
+ : String(error instanceof Error ? error.message : error),
559
+ ...extra,
560
+ claims_retained: true,
561
+ };
562
+ }
563
+ /**
564
+ * Read-only observation of one exact recorded background session. Registers the intent first,
565
+ * then reads native state; a missing/unknown row is `unknown`, never `stopped`.
566
+ */
567
+ export async function queryClaudeSession(options) {
568
+ const schema = SCHEMA;
569
+ let statusQueryId = null;
570
+ try {
571
+ const opts = normalize(options);
572
+ const target = requireRecordTarget(opts);
573
+ opts.taskId = target.record.task_id ?? null;
574
+ opts.record = target.record;
575
+ opts.assigneeAgent = target.target.assignee?.agent_id ?? opts.agentId;
576
+ opts.assigneeSession = target.target.assignee?.session_id ?? opts.sessionId;
577
+ opts.assigneeHarness = target.target.assignee?.harness ?? 'claude';
578
+ statusQueryId = registerStatusQuery(opts);
579
+ const listed = await listSessions(opts);
580
+ const matched = matchRow(listed.rows, {
581
+ nativeSessionId: target.nativeSessionId,
582
+ commandKey: target.commandKey,
583
+ cwd: opts.cwd,
584
+ expectedPid: opts.expectedPid,
585
+ expectedStartedAt: opts.expectedStartedAt,
586
+ });
587
+ const logs = await readLogs(opts, target.commandKey);
588
+ const result = {
589
+ ...nativePayload(opts, target.nativeSessionId, matched, {
590
+ status: matched.observation,
591
+ blockers: [],
592
+ observation: matched.observation,
593
+ task_result_claimed: false,
594
+ transport: {
595
+ kind: 'claude-cli-agents-json',
596
+ queried_at: listed.captured_at,
597
+ agents_exit_code: listed.exit_code,
598
+ session_count: listed.rows.length,
599
+ },
600
+ logs,
601
+ }),
602
+ evidence: {
603
+ request: ['agents --json --all --cwd', 'logs <command-key>'],
604
+ observed_ids: {
605
+ session_id: target.nativeSessionId,
606
+ command_key: target.commandKey,
607
+ },
608
+ queried_at: listed.captured_at,
609
+ },
610
+ reconciliation_required: false,
611
+ claims_retained: true,
612
+ };
613
+ try {
614
+ const statusId = reportStatus(opts, statusQueryId, {
615
+ observation: matched.observation,
616
+ native_session_id: target.nativeSessionId,
617
+ command_key: matched.command_key,
618
+ native_state: matched.native_state,
619
+ native_status: matched.native_status,
620
+ cwd: matched.row.cwd,
621
+ evidence_ref: object(logs.raw_evidence)
622
+ ? (logs.raw_evidence['digest'] ?? logs.digest)
623
+ : logs.digest,
624
+ });
625
+ result.protocol = { status_query: statusQueryId, status: statusId };
626
+ }
627
+ catch (error) {
628
+ result.record_error =
629
+ error instanceof ClaudeNativeRefusal
630
+ ? errorCode(error)
631
+ : 'internal_error';
632
+ result.protocol = { status_query: statusQueryId, status: null };
633
+ }
634
+ return result;
635
+ }
636
+ catch (error) {
637
+ return refusalResult(schema, error, { status_query: statusQueryId });
638
+ }
639
+ }
640
+ /**
641
+ * Request a stop of one exact recorded background session. A fresh active observation is
642
+ * required first, the CANCEL intent is recorded, and only the matching command key is stopped.
643
+ * The command's exit code is never the proof: the polled terminal native row is. Claims are
644
+ * retained, no CANCELLED is sent, and any failure after the recorded intent still returns
645
+ * `reconciliation_required: true` together with the known native observation.
646
+ */
647
+ export async function cancelClaudeSession(options) {
648
+ const schema = SCHEMA;
649
+ const ctx = {
650
+ cancel_message_id: null,
651
+ intent_recorded: false,
652
+ intent_error: null,
653
+ native_command_key: null,
654
+ before: null,
655
+ stop_command: null,
656
+ last_observation: null,
657
+ evidence_error: null,
658
+ };
659
+ try {
660
+ const opts = normalize(options);
661
+ const target = requireRecordTarget(opts);
662
+ opts.taskId = target.record.task_id ?? null;
663
+ opts.record = target.record;
664
+ opts.assigneeAgent = target.target.assignee?.agent_id ?? opts.agentId;
665
+ opts.assigneeSession = target.target.assignee?.session_id ?? opts.sessionId;
666
+ opts.assigneeHarness = target.target.assignee?.harness ?? 'claude';
667
+ ctx.native_command_key = target.commandKey;
668
+ const before = await listSessions(opts);
669
+ const matched = matchRow(before.rows, {
670
+ nativeSessionId: target.nativeSessionId,
671
+ commandKey: target.commandKey,
672
+ cwd: opts.cwd,
673
+ expectedPid: opts.expectedPid,
674
+ expectedStartedAt: opts.expectedStartedAt,
675
+ });
676
+ ctx.before = {
677
+ observation: matched.observation,
678
+ status: matched.row.status ?? null,
679
+ state: matched.row.state ?? null,
680
+ pid: Number.isSafeInteger(matched.row.pid) ? matched.row.pid : null,
681
+ at: before.captured_at,
682
+ };
683
+ if (matched.observation !== 'active') {
684
+ return {
685
+ ...nativePayload(opts, target.nativeSessionId, matched, {
686
+ status: 'not_cancellable',
687
+ blockers: ['native_not_active'],
688
+ observation: matched.observation,
689
+ task_result_claimed: false,
690
+ }),
691
+ reconciliation_required: false,
692
+ claims_retained: true,
693
+ cancelled_message_sent: false,
694
+ };
695
+ }
696
+ const { taskId, from, to } = ids(opts);
697
+ const intent = submitMessage(opts.taskRoot, buildMessage({
698
+ type: 'CANCEL',
699
+ task_id: taskId,
700
+ from,
701
+ to,
702
+ subtask_id: opts.subtaskId,
703
+ attempt: opts.attempt,
704
+ owner_epoch: opts.ownerEpoch,
705
+ body: {
706
+ reason: options.reason ?? 'native background-session cancellation',
707
+ native_session_id: target.nativeSessionId,
708
+ native_command_key: target.commandKey,
709
+ native_status: matched.native_status,
710
+ },
711
+ }));
712
+ if (intent.ok !== true) {
713
+ ctx.intent_error = intent.reason ?? 'cancel_intent_not_recorded';
714
+ return {
715
+ ...nativePayload(opts, target.nativeSessionId, matched, {
716
+ status: 'invalid',
717
+ blockers: ['cancel_intent_not_recorded'],
718
+ intent_error: ctx.intent_error,
719
+ }),
720
+ reconciliation_required: false,
721
+ claims_retained: true,
722
+ cancelled_message_sent: false,
723
+ };
724
+ }
725
+ ctx.cancel_message_id = intent.registration.message_id;
726
+ ctx.intent_recorded = true;
727
+ const stop = await runBounded(opts.claudeBin, ['stop', target.commandKey], {
728
+ cwd: opts.cwd,
729
+ env: opts.env,
730
+ timeoutMs: opts.timeoutMs,
731
+ maxBytes: opts.maxBytes,
732
+ streamGraceMs: opts.streamGraceMs,
733
+ });
734
+ ctx.stop_command = {
735
+ exit_code: stop.exit_code,
736
+ signal: stop.signal,
737
+ timed_out: stop.timed_out,
738
+ parent_exited: stop.parent_exited,
739
+ streams_detached: stop.streams_detached,
740
+ spawn_failed: stop.spawn_failed,
741
+ spawn_error: stop.spawn_error,
742
+ note: 'exit 0 alone is not stop proof',
743
+ };
744
+ const deadline = Date.now() + opts.stopTimeoutMs;
745
+ let terminal = null;
746
+ for (;;) {
747
+ try {
748
+ const listed = await listSessions(opts);
749
+ const current = matchRow(listed.rows, {
750
+ nativeSessionId: target.nativeSessionId,
751
+ commandKey: target.commandKey,
752
+ cwd: opts.cwd,
753
+ expectedPid: null,
754
+ expectedStartedAt: null,
755
+ });
756
+ ctx.last_observation = {
757
+ observation: current.observation,
758
+ state: current.native_state,
759
+ status: current.native_status,
760
+ at: listed.captured_at,
761
+ };
762
+ if (current.observation === 'terminal') {
763
+ terminal = ctx.last_observation;
764
+ break;
765
+ }
766
+ }
767
+ catch (error) {
768
+ if (!(error instanceof ClaudeNativeRefusal))
769
+ throw error;
770
+ ctx.last_observation = {
771
+ observation: 'unknown',
772
+ blocker: errorCode(error),
773
+ at: nowIso(),
774
+ };
775
+ }
776
+ if (Date.now() >= deadline)
777
+ break;
778
+ await delay(Math.min(opts.pollMs, Math.max(1, deadline - Date.now())));
779
+ }
780
+ const evidence = {
781
+ request: ['agents --json --all --cwd', 'stop <command-key>'],
782
+ observed_ids: {
783
+ session_id: target.nativeSessionId,
784
+ command_key: target.commandKey,
785
+ },
786
+ cancel_message_id: ctx.cancel_message_id,
787
+ before: ctx.before,
788
+ stop_command: ctx.stop_command,
789
+ last_observation: ctx.last_observation,
790
+ queried_at: nowIso(),
791
+ };
792
+ if (opts.evidenceDir) {
793
+ try {
794
+ fs.mkdirSync(opts.evidenceDir, { recursive: true, mode: 0o700 });
795
+ }
796
+ catch {
797
+ /* reported below */
798
+ }
799
+ const written = tryWritePrivate(path.join(opts.evidenceDir, 'claude-native-cancel.json'), Buffer.from(`${JSON.stringify(evidence, null, 2)}\n`));
800
+ if (written.error)
801
+ ctx.evidence_error = written.error;
802
+ else
803
+ evidence.evidence_ref = written;
804
+ }
805
+ if (terminal) {
806
+ return {
807
+ ...nativePayload(opts, target.nativeSessionId, matched, {
808
+ status: 'terminal',
809
+ blockers: [],
810
+ observation: 'terminal',
811
+ task_result_claimed: false,
812
+ }),
813
+ native_terminal: terminal,
814
+ evidence,
815
+ evidence_error: ctx.evidence_error,
816
+ cancel_message_id: ctx.cancel_message_id,
817
+ intent_recorded: ctx.intent_recorded,
818
+ reconciliation_required: true,
819
+ claims_retained: true,
820
+ cancelled_message_sent: false,
821
+ };
822
+ }
823
+ return {
824
+ ...nativePayload(opts, target.nativeSessionId, matched, {
825
+ status: 'needs_input',
826
+ blockers: ['native_terminal_state_not_observed'],
827
+ observation: 'unknown',
828
+ task_result_claimed: false,
829
+ }),
830
+ evidence,
831
+ evidence_error: ctx.evidence_error,
832
+ cancel_message_id: ctx.cancel_message_id,
833
+ intent_recorded: ctx.intent_recorded,
834
+ reconciliation_required: true,
835
+ claims_retained: true,
836
+ cancelled_message_sent: false,
837
+ };
838
+ }
839
+ catch (error) {
840
+ return refusalResult(schema, error, {
841
+ cancel_message_id: ctx.cancel_message_id,
842
+ intent_recorded: ctx.intent_recorded,
843
+ intent_error: ctx.intent_error,
844
+ native_command_key: ctx.native_command_key,
845
+ before: ctx.before,
846
+ stop_command: ctx.stop_command,
847
+ last_observation: ctx.last_observation,
848
+ evidence_error: ctx.evidence_error,
849
+ reconciliation_required: ctx.intent_recorded,
850
+ cancelled_message_sent: false,
851
+ });
852
+ }
853
+ }