({
processId,
hookId,
shellCommand,
asyncResponse,
hookEvent,
hookName,
command,
asyncRewake,
pluginId,
}: {
processId: string
hookId: string
shellCommand: ShellCommand
asyncResponse: AsyncHookJSONOutput
hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion'
hookName: string
command: string
asyncRewake?: boolean
pluginId?: string
})
| 182 | } |
| 183 | |
| 184 | function executeInBackground({ |
| 185 | processId, |
| 186 | hookId, |
| 187 | shellCommand, |
| 188 | asyncResponse, |
| 189 | hookEvent, |
| 190 | hookName, |
| 191 | command, |
| 192 | asyncRewake, |
| 193 | pluginId, |
| 194 | }: { |
| 195 | processId: string |
| 196 | hookId: string |
| 197 | shellCommand: ShellCommand |
| 198 | asyncResponse: AsyncHookJSONOutput |
| 199 | hookEvent: HookEvent | 'StatusLine' | 'FileSuggestion' |
| 200 | hookName: string |
| 201 | command: string |
| 202 | asyncRewake?: boolean |
| 203 | pluginId?: string |
| 204 | }): boolean { |
| 205 | if (asyncRewake) { |
| 206 | // asyncRewake hooks bypass the registry entirely. On completion, if exit |
| 207 | // code 2 (blocking error), enqueue as a task-notification so it wakes the |
| 208 | // model via useQueueProcessor (idle) or gets injected mid-query via |
| 209 | // queued_command attachments (busy). |
| 210 | // |
| 211 | // NOTE: We deliberately do NOT call shellCommand.background() here, because |
| 212 | // it calls taskOutput.spillToDisk() which breaks in-memory stdout/stderr |
| 213 | // capture (getStderr() returns '' in disk mode). The StreamWrappers stay |
| 214 | // attached and pipe data into the in-memory TaskOutput buffers. The abort |
| 215 | // handler already no-ops on 'interrupt' reason (user submitted a new |
| 216 | // message), so the hook survives new prompts. A hard cancel (Escape) WILL |
| 217 | // kill the hook via the abort handler, which is the desired behavior. |
| 218 | void shellCommand.result.then(async result => { |
| 219 | // result resolves on 'exit', but stdio 'data' events may still be |
| 220 | // pending. Yield to I/O so the StreamWrapper data handlers drain into |
| 221 | // TaskOutput before we read it. |
| 222 | await new Promise(resolve => setImmediate(resolve)) |
| 223 | const stdout = await shellCommand.taskOutput.getStdout() |
| 224 | const stderr = shellCommand.taskOutput.getStderr() |
| 225 | shellCommand.cleanup() |
| 226 | emitHookResponse({ |
| 227 | hookId, |
| 228 | hookName, |
| 229 | hookEvent, |
| 230 | output: stdout + stderr, |
| 231 | stdout, |
| 232 | stderr, |
| 233 | exitCode: result.code, |
| 234 | outcome: result.code === 0 ? 'success' : 'error', |
| 235 | }) |
| 236 | if (result.code === 2) { |
| 237 | enqueuePendingNotification({ |
| 238 | value: wrapInSystemReminder( |
| 239 | `Stop hook blocking error from command "${hookName}": ${stderr || stdout}`, |
| 240 | ), |
| 241 | mode: 'task-notification', |
no test coverage detected