cli-aimlock 7.0.33 → 7.0.34

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.
@@ -0,0 +1,105 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { resolve } from 'node:path'
3
+ import { executeCoordinatorOperation } from 'cli-swarm/coordinator'
4
+ import { loadOfficialSkillContext } from './installer.mjs'
5
+ import { invokeOfficialSkill } from './broker.mjs'
6
+ import { fail, sha256 } from './aimlock-local-fs.mjs'
7
+ import { errorRecord } from './aimlock-chain-model.mjs'
8
+ import { saveExecution } from './aimlock-chain-store.mjs'
9
+ import { executeCommand } from './aimlock-chain-process.mjs'
10
+
11
+ export function resolveContexts(root, plan) {
12
+ return plan.skills.map((skill) => {
13
+ const context = loadOfficialSkillContext(resolve(root, skill.packageRoot))
14
+ if (context.skillName !== skill.skillId) fail('AIMLOCK_CHAIN_SKILL_INVALID', 'installed skill does not match plan skillId')
15
+ return { skillId: skill.skillId, context }
16
+ })
17
+ }
18
+
19
+ export function verifyContexts(root, state) {
20
+ if (JSON.stringify(resolveContexts(root, state.plan)) !== JSON.stringify(state.contexts)) {
21
+ fail('AIMLOCK_CHAIN_SKILL_CHANGED', 'installed package metadata changed after execution initialization')
22
+ }
23
+ }
24
+
25
+ function skillContext(state, skillId) {
26
+ const found = state.contexts.find((entry) => entry.skillId === skillId)
27
+ if (!found) fail('AIMLOCK_CHAIN_SKILL_MISSING', 'execution needs an explicitly declared ' + skillId + ' package')
28
+ return found.context
29
+ }
30
+
31
+ async function startCall(session, kind, operation, input) {
32
+ const call = { callId: 'call-' + randomUUID(), kind, operation, inputDigest: sha256(JSON.stringify(input)),
33
+ requestId: null, status: 'started', pid: null, receipt: null, error: null,
34
+ startedAt: new Date().toISOString(), completedAt: null }
35
+ session.record.calls.push(call)
36
+ await saveExecution(session.file, session.state)
37
+ return call
38
+ }
39
+
40
+ async function failCall(session, call, error) {
41
+ call.status = call.requestId || call.pid !== null ? 'uncertain' : 'failed'
42
+ call.error = errorRecord(error)
43
+ call.completedAt = new Date().toISOString()
44
+ await saveExecution(session.file, session.state)
45
+ }
46
+
47
+ export async function callSkill(session, skillId, operation, input) {
48
+ const context = skillContext(session.state, skillId)
49
+ const call = await startCall(session, 'skill', operation, input)
50
+ try {
51
+ const dependencies = session.dependencies
52
+ const invocation = await invokeOfficialSkill(context, operation, input, {
53
+ environment: dependencies.environment, credentialAccess: dependencies.credentialAccess,
54
+ request: async (url, options) => {
55
+ const request = JSON.parse(options.body).input
56
+ call.requestId = request.requestId
57
+ call.status = 'dispatched'
58
+ await saveExecution(session.file, session.state)
59
+ return dependencies.request(url, options)
60
+ },
61
+ })
62
+ call.receipt = invocation
63
+ call.status = 'recorded'
64
+ call.completedAt = new Date().toISOString()
65
+ await saveExecution(session.file, session.state)
66
+ return invocation.response.output
67
+ } catch (error) {
68
+ await failCall(session, call, error)
69
+ throw error
70
+ }
71
+ }
72
+
73
+ export async function callCoordinator(session, operation, input) {
74
+ const call = await startCall(session, 'coordinator', operation, input)
75
+ try {
76
+ const output = await executeCoordinatorOperation(operation, session.root, input)
77
+ call.status = 'recorded'
78
+ call.receipt = output
79
+ call.completedAt = new Date().toISOString()
80
+ await saveExecution(session.file, session.state)
81
+ return output
82
+ } catch (error) {
83
+ await failCall(session, call, error)
84
+ throw error
85
+ }
86
+ }
87
+
88
+ export async function callCommand(session, input) {
89
+ const call = await startCall(session, 'command', 'exec', input)
90
+ try {
91
+ const output = await executeCommand(session.root, input, call.callId, async (pid) => {
92
+ call.pid = pid
93
+ call.status = 'dispatched'
94
+ await saveExecution(session.file, session.state)
95
+ })
96
+ call.status = output.status === 'uncertain' ? 'uncertain' : 'recorded'
97
+ call.receipt = output
98
+ call.completedAt = new Date().toISOString()
99
+ await saveExecution(session.file, session.state)
100
+ return output
101
+ } catch (error) {
102
+ await failCall(session, call, error)
103
+ throw error
104
+ }
105
+ }
@@ -0,0 +1,49 @@
1
+ import { stdin, stdout } from 'node:process'
2
+ import { answerExecution, executionStatus, initializeExecution, resumeExecution } from './aimlock-chain-executor.mjs'
3
+ import { errorRecord } from './aimlock-chain-model.mjs'
4
+ import { fail } from './aimlock-local-fs.mjs'
5
+
6
+ const PLAN_MAX_BYTES = 1_048_576
7
+ export const CHAIN_USAGE = [
8
+ ' cli-aimlock chain init <repositoryRoot> < execution-plan.json',
9
+ ' cli-aimlock chain resume <repositoryRoot> <chainId>',
10
+ ' cli-aimlock chain status <repositoryRoot> <chainId>',
11
+ ' cli-aimlock chain answer <repositoryRoot> <chainId> <actorId>',
12
+ ' Persist explicit steps; resume invokes real broker/coordinator/process operations.',
13
+ ' answer requires a live terminal. Recorded results and nextStep never imply execution.',
14
+ ].join('\n')
15
+
16
+ async function readPlan(input) {
17
+ let source = ''
18
+ for await (const chunk of input) {
19
+ source += chunk
20
+ if (Buffer.byteLength(source) > PLAN_MAX_BYTES) fail('AIMLOCK_CHAIN_INPUT_TOO_LARGE', 'execution plan exceeds 1 MiB')
21
+ }
22
+ if (!source.trim()) fail('AIMLOCK_CHAIN_INPUT_REQUIRED', 'execution plan is required on stdin')
23
+ return JSON.parse(source)
24
+ }
25
+
26
+ export async function dispatchChain(args) {
27
+ const [operation, repositoryRoot, chainId, actorId] = args
28
+ const expected = { init: 2, status: 3, resume: 3, answer: 4 }
29
+ if (!Object.hasOwn(expected, operation) || args.length !== expected[operation]) {
30
+ fail('AIMLOCK_CHAIN_USAGE_INVALID', CHAIN_USAGE)
31
+ }
32
+ const dependencies = { environment: process.env, request: fetch }
33
+ if (operation === 'init') return initializeExecution(repositoryRoot, await readPlan(stdin))
34
+ if (operation === 'status') return executionStatus(repositoryRoot, chainId)
35
+ if (operation === 'resume') return resumeExecution(repositoryRoot, chainId, dependencies)
36
+ return answerExecution(repositoryRoot, chainId, { input: stdin, output: stdout, actorId }, dependencies)
37
+ }
38
+
39
+ export async function runChainCli(args) {
40
+ try {
41
+ const state = await dispatchChain(args)
42
+ stdout.write(JSON.stringify(state) + '\n')
43
+ if (state.status === 'blocked') process.exitCode = 2
44
+ else if (state.status === 'failed' || state.status === 'uncertain') process.exitCode = 1
45
+ } catch (error) {
46
+ stdout.write(JSON.stringify({ status: 'failed', error: errorRecord(error) }) + '\n')
47
+ process.exitCode = 1
48
+ }
49
+ }
@@ -0,0 +1,233 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { assertChainNotSuspended } from './aimlock-coordination.mjs'
3
+ import { fail } from './aimlock-local-fs.mjs'
4
+ import { bindInput, errorRecord, validatePlan } from './aimlock-chain-model.mjs'
5
+ import { assertNewExecution, executionStatus, initialState, loadExecution, recoverInterrupted,
6
+ saveExecution, withExecutionLock } from './aimlock-chain-store.mjs'
7
+ import { callCommand, callCoordinator, callSkill, resolveContexts, verifyContexts } from './aimlock-chain-calls.mjs'
8
+ import { answerPending, prepareHuman } from './aimlock-chain-human.mjs'
9
+ import { skillOutcome } from './aimlock-chain-outcomes.mjs'
10
+
11
+ const COORDINATION_CODES = new Set(['AIMLOCK_COORDINATION_WAITING', 'AIMLOCK_COORDINATION_BLOCKED'])
12
+ const BLOCKED_LOCAL_STATUSES = new Set(['blocked', 'queued', 'denied'])
13
+ const FAILED_LOCAL_STATUSES = new Set(['failed', 'reclaimed'])
14
+ const QUEUE_BLOCK_CODES = new Set(['SWARM_COORD_QUEUE_GRANT_INVALID', 'SWARM_COORD_QUEUE_GRANT_EXPIRED',
15
+ 'SWARM_COORD_QUEUE_NOT_FOUND', 'SWARM_COORD_QUEUE_STATE_INVALID', 'SWARM_COORD_TASK_BLOCKED',
16
+ 'SWARM_COORD_BASELINE_HANDSHAKE_REQUIRED'])
17
+
18
+ export async function initializeExecution(repositoryRoot, planInput) {
19
+ const plan = validatePlan(planInput)
20
+ return withExecutionLock(repositoryRoot, plan.chainId, async (root, file) => {
21
+ await assertNewExecution(file)
22
+ const state = initialState(plan, resolveContexts(root, plan))
23
+ await saveExecution(file, state)
24
+ return state
25
+ })
26
+ }
27
+
28
+ async function failedStep(session, error) {
29
+ const { record, state, file } = session
30
+ record.error = errorRecord(error)
31
+ record.status = record.calls.at(-1)?.status === 'uncertain' ? 'uncertain' : 'failed'
32
+ record.completedAt = new Date().toISOString()
33
+ await saveExecution(file, state)
34
+ }
35
+
36
+ async function readyForWork(session, step) {
37
+ if (step.kind === 'coordinator' || step.skillId === 'confirm-protocol') return true
38
+ try {
39
+ await assertChainNotSuspended({ repositoryRoot: session.root, chainId: session.state.chainId })
40
+ return true
41
+ } catch (error) {
42
+ if (!COORDINATION_CODES.has(error.code)) throw error
43
+ session.record.status = 'blocked'
44
+ session.record.error = errorRecord(error)
45
+ session.record.pending = { kind: 'coordination-guard' }
46
+ await saveExecution(session.file, session.state)
47
+ return false
48
+ }
49
+ }
50
+
51
+ function finish(record, output, status) {
52
+ record.output = output
53
+ record.status = status
54
+ record.pending = null
55
+ record.completedAt = new Date().toISOString()
56
+ }
57
+
58
+ async function stillWaiting(session, output, pending) {
59
+ if (output.status === 'waiting') return true
60
+ const releasedByHuman = pending.decision?.status === 'resolved'
61
+ && pending.decision.answer === 'resume:' + pending.input.agentId
62
+ if (output.status !== 'blocked' || (output.wakePackage?.reason !== 'event-received' && !releasedByHuman)) return false
63
+ const current = (await callCoordinator(session, 'status', {})).state
64
+ const task = current.tasks.find((item) => item.taskId === pending.input.taskId)
65
+ if (!task || ['completed', 'failed', 'reclaimed'].includes(task.status)) return false
66
+ return current.waits.some((wait) => wait.taskId === task.taskId && wait.status === 'active')
67
+ || current.decisions.some((decision) => decision.status === 'pending' && decision.agents.includes(task.agentId))
68
+ }
69
+
70
+ async function driveWait(session) {
71
+ const pending = session.record.pending
72
+ const output = await callCoordinator(session, 'wait-for-event', pending.input)
73
+ session.record.output = output
74
+ if (await stillWaiting(session, output, pending)) {
75
+ session.record.status = 'waiting'
76
+ await saveExecution(session.file, session.state)
77
+ return
78
+ }
79
+ if (output.confirmProtocolRequests.length) {
80
+ await prepareHuman(session, output.confirmProtocolRequests[0].input.interaction, pending.input)
81
+ return
82
+ }
83
+ const releasedByHuman = pending.decision?.status === 'resolved'
84
+ && pending.decision.answer === 'resume:' + pending.input.agentId
85
+ && output.status === 'resolved'
86
+ if (releasedByHuman) {
87
+ finish(session.record, { ...output, status: 'resolved-by-human',
88
+ humanDecision: pending.decision, confirmation: pending.confirmation }, 'succeeded')
89
+ } else if (output.status === 'resolved' && output.wakePackage.reason === 'event-received') {
90
+ finish(session.record, output, 'succeeded')
91
+ } else {
92
+ finish(session.record, output, 'blocked')
93
+ session.record.error = { code: 'AIMLOCK_CHAIN_DEPENDENCY_UNFULFILLED',
94
+ message: 'Dependency ended without its event or a human decision releasing this task' }
95
+ }
96
+ await saveExecution(session.file, session.state)
97
+ }
98
+
99
+ function saveQueuePending(record, output, input) {
100
+ record.pending = { kind: 'coordinator-lock-queue', input: { queueId: output.queued.queueId,
101
+ taskId: input.taskId, agentId: input.agentId, chainId: input.chainId } }
102
+ record.status = 'waiting'
103
+ record.completedAt = null
104
+ }
105
+
106
+ async function driveQueue(session) {
107
+ await callCoordinator(session, 'tick', { now: new Date().toISOString() })
108
+ try {
109
+ const output = await callCoordinator(session, 'lock-queue-status', session.record.pending.input)
110
+ session.record.output = output
111
+ if (output.status === 'queued') session.record.status = 'waiting'
112
+ else finish(session.record, output, output.status === 'granted' ? 'succeeded' : 'blocked')
113
+ } catch (error) {
114
+ if (!QUEUE_BLOCK_CODES.has(error.code)) throw error
115
+ session.record.error = errorRecord(error)
116
+ finish(session.record, { status: 'blocked', queueId: session.record.pending.input.queueId,
117
+ error: session.record.error }, 'blocked')
118
+ }
119
+ await saveExecution(session.file, session.state)
120
+ }
121
+
122
+ async function coordinatorStep(session, step, input) {
123
+ const output = await callCoordinator(session, step.operation, input)
124
+ session.record.output = output
125
+ if (step.operation === 'lock-acquire' && output.status === 'queued') {
126
+ saveQueuePending(session.record, output, input)
127
+ } else if (step.operation === 'dependency-wait') {
128
+ const waitInput = { taskId: input.taskId, agentId: input.waiter, chainId: input.chainId, waitId: output.wait.waitId }
129
+ session.record.pending = { kind: 'coordinator-wait', input: waitInput }
130
+ session.record.status = 'waiting'
131
+ await saveExecution(session.file, session.state)
132
+ await driveWait(session)
133
+ } else if (step.operation === 'wait-for-event') {
134
+ session.record.pending = { kind: 'coordinator-wait', input }
135
+ session.record.status = 'waiting'
136
+ await saveExecution(session.file, session.state)
137
+ if (await stillWaiting(session, output, session.record.pending)) {
138
+ session.record.status = 'waiting'
139
+ } else if (output.confirmProtocolRequests.length) {
140
+ await prepareHuman(session, output.confirmProtocolRequests[0].input.interaction, input)
141
+ } else if (output.status === 'resolved' && output.wakePackage.reason === 'event-received') {
142
+ finish(session.record, output, 'succeeded')
143
+ } else finish(session.record, output, 'blocked')
144
+ } else {
145
+ const status = BLOCKED_LOCAL_STATUSES.has(output.status) || output.allowed === false
146
+ || output.undeclaredWait === true || output.decisions?.some((item) => item.confirmationRequired)
147
+ ? 'blocked' : FAILED_LOCAL_STATUSES.has(output.status) ? 'failed' : 'succeeded'
148
+ finish(session.record, output, status)
149
+ }
150
+ }
151
+
152
+ async function executeStep(session, step) {
153
+ if (!await readyForWork(session, step)) return
154
+ const input = bindInput(step, session.state)
155
+ session.record.input = input
156
+ session.record.status = 'running'
157
+ session.record.error = null
158
+ session.record.startedAt = new Date().toISOString()
159
+ session.record.attemptId = 'attempt-' + randomUUID()
160
+ await saveExecution(session.file, session.state)
161
+ if (step.kind === 'command') {
162
+ const output = await callCommand(session, input)
163
+ finish(session.record, output, output.status)
164
+ } else if (step.kind === 'coordinator') await coordinatorStep(session, step, input)
165
+ else {
166
+ const output = await callSkill(session, step.skillId, step.operation, input)
167
+ if (step.skillId === 'confirm-protocol' && step.operation === 'interaction-request' && output.status === 'succeeded') {
168
+ session.record.status = 'waiting'
169
+ session.record.output = output
170
+ session.record.pending = { kind: 'human', interaction: output.interaction, waitInput: null,
171
+ presentation: output.chatFallback, response: output,
172
+ continueWhen: Object.hasOwn(step, 'continueWhen') ? step.continueWhen : null }
173
+ } else {
174
+ const outcome = skillOutcome(step, output)
175
+ finish(session.record, output, outcome.status)
176
+ session.record.error = outcome.error
177
+ }
178
+ }
179
+ await saveExecution(session.file, session.state)
180
+ }
181
+
182
+ async function advance(root, file, state, dependencies) {
183
+ for (const step of state.plan.steps) {
184
+ const record = state.steps.find((item) => item.stepId === step.stepId)
185
+ if (record.status === 'succeeded') continue
186
+ if (record.status === 'blocked' && step.kind === 'coordinator' && step.operation === 'lock-acquire'
187
+ && record.output?.status === 'queued' && record.input) saveQueuePending(record, record.output, record.input)
188
+ const session = { root, file, state, record, dependencies }
189
+ if (['failed', 'uncertain'].includes(record.status)) return state
190
+ if (record.status === 'waiting' && record.pending?.kind === 'human') return state
191
+ if (record.status === 'blocked' && record.pending?.kind !== 'coordination-guard') return state
192
+ if (step.dependsOn.some((id) => state.steps.find((item) => item.stepId === id).status !== 'succeeded')) {
193
+ fail('AIMLOCK_CHAIN_DEPENDENCY_INVALID', 'previous dependency has not succeeded')
194
+ }
195
+ try {
196
+ if (record.status === 'waiting' && record.pending?.kind === 'coordinator-lock-queue') await driveQueue(session)
197
+ else if (record.status === 'waiting' && record.pending?.kind === 'coordinator-wait') await driveWait(session)
198
+ else await executeStep(session, step)
199
+ } catch (error) { await failedStep(session, error) }
200
+ if (record.status !== 'succeeded') return state
201
+ }
202
+ return state
203
+ }
204
+
205
+ export async function resumeExecution(repositoryRoot, chainId, dependencies) {
206
+ return withExecutionLock(repositoryRoot, chainId, async (root, file) => {
207
+ const state = await loadExecution(file)
208
+ verifyContexts(root, state)
209
+ recoverInterrupted(state)
210
+ await saveExecution(file, state)
211
+ return advance(root, file, state, dependencies)
212
+ })
213
+ }
214
+
215
+ export async function answerExecution(repositoryRoot, chainId, terminal, dependencies) {
216
+ return withExecutionLock(repositoryRoot, chainId, async (root, file) => {
217
+ const state = await loadExecution(file)
218
+ verifyContexts(root, state)
219
+ recoverInterrupted(state)
220
+ await saveExecution(file, state)
221
+ const record = state.steps.find((item) => item.status !== 'succeeded')
222
+ if (!record) fail('AIMLOCK_CHAIN_NOT_WAITING_FOR_HUMAN', 'execution is complete')
223
+ const session = { root, file, state, record, dependencies }
224
+ try { await answerPending(session, terminal) } catch (error) {
225
+ if (record.status !== 'running') throw error
226
+ await failedStep(session, error)
227
+ return state
228
+ }
229
+ return advance(root, file, state, dependencies)
230
+ })
231
+ }
232
+
233
+ export { executionStatus }
@@ -0,0 +1,99 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { createInterface } from 'node:readline/promises'
3
+ import { fail, identifier } from './aimlock-local-fs.mjs'
4
+ import { callCoordinator, callSkill } from './aimlock-chain-calls.mjs'
5
+ import { saveExecution } from './aimlock-chain-store.mjs'
6
+ import { answerMatchesContinuation } from './aimlock-chain-outcomes.mjs'
7
+
8
+ const ANSWER_PROMPT = '答案 / Answer / Ответ > '
9
+
10
+ export async function prepareHuman(session, interaction, waitInput) {
11
+ const output = await callSkill(session, 'confirm-protocol', 'interaction-request', { interaction })
12
+ if (output.status !== 'succeeded' || JSON.stringify(output.interaction) !== JSON.stringify(interaction)) {
13
+ fail('AIMLOCK_CHAIN_CONFIRM_REJECTED', 'Confirm Protocol did not accept the bound interaction')
14
+ }
15
+ session.record.status = 'waiting'
16
+ session.record.pending = { kind: 'human', interaction: output.interaction, waitInput,
17
+ presentation: output.chatFallback, response: output }
18
+ await saveExecution(session.file, session.state)
19
+ }
20
+
21
+ function normalizeSelection(interaction, source) {
22
+ const text = source.trim()
23
+ if (!text) fail('AIMLOCK_CHAIN_ANSWER_REQUIRED', 'an explicit answer is required')
24
+ if (interaction.type === 'input') return text
25
+ const selected = interaction.type === 'multi' ? text.split(',').map((part) => part.trim()) : [text]
26
+ const ids = interaction.options.map((option) => option.id)
27
+ if (new Set(selected).size !== selected.length || selected.some((item) => !ids.includes(item))) {
28
+ fail('AIMLOCK_CHAIN_ANSWER_INVALID', 'answer must use the displayed option IDs')
29
+ }
30
+ return interaction.type === 'multi' ? selected : selected[0]
31
+ }
32
+
33
+ async function readHumanAnswer(pending, input, output) {
34
+ if (input.isTTY !== true || output.isTTY !== true) {
35
+ fail('AIMLOCK_CHAIN_TTY_REQUIRED', 'the answer command requires an interactive terminal; redirected answers are not accepted')
36
+ }
37
+ output.write(pending.interaction.question + '\n')
38
+ if (pending.interaction.riskDescription) output.write(pending.interaction.riskDescription + '\n')
39
+ for (const option of pending.interaction.options) output.write(option.id + ': ' + option.label + '\n')
40
+ const terminal = createInterface({ input, output })
41
+ try {
42
+ return normalizeSelection(pending.interaction, await terminal.question(ANSWER_PROMPT))
43
+ } finally { terminal.close() }
44
+ }
45
+
46
+ function verifyAnswer(output, pending, submitted) {
47
+ const audit = output.auditEntry
48
+ const callback = output.callbackRequest
49
+ if (output.status !== 'succeeded' || !audit || !callback
50
+ || audit.requestId !== pending.interaction.requestId || audit.actorId !== submitted.actorId
51
+ || JSON.stringify(audit.answer) !== JSON.stringify(submitted.answer) || audit.remembered !== false
52
+ || audit.question !== pending.interaction.question
53
+ || audit.risk !== pending.interaction.risk || audit.answeredAt !== submitted.answeredAt
54
+ || audit.auditId !== submitted.auditId || callback.operation !== pending.interaction.callback.operation
55
+ || callback.payload.requestId !== pending.interaction.requestId
56
+ || JSON.stringify(callback.payload.answer) !== JSON.stringify(submitted.answer)) {
57
+ fail('AIMLOCK_CHAIN_CONFIRM_BINDING_INVALID', 'confirmed response is not bound to the displayed question and actual answer')
58
+ }
59
+ for (const [key, value] of Object.entries(pending.interaction.callback.payload)) {
60
+ if (JSON.stringify(callback.payload[key]) !== JSON.stringify(value)) {
61
+ fail('AIMLOCK_CHAIN_CONFIRM_BINDING_INVALID', 'callback payload changed')
62
+ }
63
+ }
64
+ }
65
+
66
+ export async function answerPending(session, terminal) {
67
+ const pending = session.record.pending
68
+ if (session.record.status !== 'waiting' || pending?.kind !== 'human') {
69
+ fail('AIMLOCK_CHAIN_NOT_WAITING_FOR_HUMAN', 'this step has no pending human question')
70
+ }
71
+ const actorId = identifier(terminal.actorId, 'actorId')
72
+ const answer = await readHumanAnswer(pending, terminal.input, terminal.output)
73
+ const submitted = { interaction: pending.interaction, answer, actorId, remembered: false,
74
+ answeredAt: new Date().toISOString(), auditId: 'audit-' + randomUUID() }
75
+ session.record.status = 'running'
76
+ await saveExecution(session.file, session.state)
77
+ const output = await callSkill(session, 'confirm-protocol', 'interaction-answer', submitted)
78
+ verifyAnswer(output, pending, submitted)
79
+ if (pending.waitInput !== null) {
80
+ const decisionId = pending.interaction.callback.payload.decisionId
81
+ if (pending.interaction.callback.operation !== 'resolve-human' || decisionId !== pending.interaction.requestId) {
82
+ fail('AIMLOCK_CHAIN_CONFIRM_BINDING_INVALID', 'coordinator decision callback does not match')
83
+ }
84
+ const resolution = await callCoordinator(session, 'resolve-human', { decisionId, answer, actorId })
85
+ session.record.output = { ...session.record.output, humanResolution: resolution, confirmation: output }
86
+ session.record.pending = { kind: 'coordinator-wait', input: pending.waitInput,
87
+ decision: resolution.decision, confirmation: output }
88
+ session.record.status = 'waiting'
89
+ } else {
90
+ session.record.output = { interaction: pending.interaction, ...output }
91
+ session.record.pending = null
92
+ const authorized = answerMatchesContinuation(pending.continueWhen, output.auditEntry.answer)
93
+ session.record.status = authorized ? 'succeeded' : 'blocked'
94
+ session.record.error = authorized ? null : { code: 'AIMLOCK_CHAIN_CONTINUATION_NOT_AUTHORIZED',
95
+ message: 'The actual answer did not satisfy an explicit continuation condition' }
96
+ session.record.completedAt = new Date().toISOString()
97
+ }
98
+ await saveExecution(session.file, session.state)
99
+ }