@raidiant/notifai 11.7.1 → 12.0.0-beta.13
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.
- package/CHANGELOG.md +57 -0
- package/NOTICE +16 -0
- package/THIRD_PARTY_NOTICES +725 -0
- package/bin/notifai.mjs +11420 -0
- package/data/install.ps1 +318 -0
- package/data/release.json +1 -0
- package/inventory.json +1 -0
- package/npm-adapter-files.json +52 -0
- package/package.json +14 -44
- package/README.md +0 -168
- package/dist/agent-update-notice.d.ts +0 -7
- package/dist/agent-update-notice.js +0 -77
- package/dist/agent-update-notice.js.map +0 -1
- package/dist/atomic-file.d.ts +0 -30
- package/dist/atomic-file.js +0 -148
- package/dist/atomic-file.js.map +0 -1
- package/dist/attendant-update.d.ts +0 -15
- package/dist/attendant-update.js +0 -96
- package/dist/attendant-update.js.map +0 -1
- package/dist/claude-wake.d.ts +0 -108
- package/dist/claude-wake.js +0 -369
- package/dist/claude-wake.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -26
- package/dist/cli-bin.js +0 -220
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli-contract.d.ts +0 -16
- package/dist/cli-contract.js +0 -27
- package/dist/cli-contract.js.map +0 -1
- package/dist/cli-release.d.ts +0 -36
- package/dist/cli-release.js +0 -85
- package/dist/cli-release.js.map +0 -1
- package/dist/client.d.ts +0 -109
- package/dist/client.js +0 -217
- package/dist/client.js.map +0 -1
- package/dist/codex-answer-control.d.ts +0 -14
- package/dist/codex-answer-control.js +0 -41
- package/dist/codex-answer-control.js.map +0 -1
- package/dist/codex-answer-presentation.d.ts +0 -18
- package/dist/codex-answer-presentation.js +0 -113
- package/dist/codex-answer-presentation.js.map +0 -1
- package/dist/codex-input-lifecycle.d.ts +0 -10
- package/dist/codex-input-lifecycle.js +0 -63
- package/dist/codex-input-lifecycle.js.map +0 -1
- package/dist/codex-native-control.d.ts +0 -13
- package/dist/codex-native-control.js +0 -120
- package/dist/codex-native-control.js.map +0 -1
- package/dist/codex-native-turn.d.ts +0 -37
- package/dist/codex-native-turn.js +0 -155
- package/dist/codex-native-turn.js.map +0 -1
- package/dist/codex-question-bindings.d.ts +0 -76
- package/dist/codex-question-bindings.js +0 -151
- package/dist/codex-question-bindings.js.map +0 -1
- package/dist/codex-queue-control.d.ts +0 -21
- package/dist/codex-queue-control.js +0 -59
- package/dist/codex-queue-control.js.map +0 -1
- package/dist/codex-tool-messages.d.ts +0 -7
- package/dist/codex-tool-messages.js +0 -126
- package/dist/codex-tool-messages.js.map +0 -1
- package/dist/codex-wake.d.ts +0 -58
- package/dist/codex-wake.js +0 -135
- package/dist/codex-wake.js.map +0 -1
- package/dist/command-session.d.ts +0 -14
- package/dist/command-session.js +0 -31
- package/dist/command-session.js.map +0 -1
- package/dist/commands-acknowledge.d.ts +0 -13
- package/dist/commands-acknowledge.js +0 -116
- package/dist/commands-acknowledge.js.map +0 -1
- package/dist/commands-agent-sessions.d.ts +0 -6
- package/dist/commands-agent-sessions.js +0 -51
- package/dist/commands-agent-sessions.js.map +0 -1
- package/dist/commands-ask.d.ts +0 -62
- package/dist/commands-ask.js +0 -503
- package/dist/commands-ask.js.map +0 -1
- package/dist/commands-auth.d.ts +0 -76
- package/dist/commands-auth.js +0 -421
- package/dist/commands-auth.js.map +0 -1
- package/dist/commands-close.d.ts +0 -6
- package/dist/commands-close.js +0 -273
- package/dist/commands-close.js.map +0 -1
- package/dist/commands-config.d.ts +0 -31
- package/dist/commands-config.js +0 -272
- package/dist/commands-config.js.map +0 -1
- package/dist/commands-core.d.ts +0 -145
- package/dist/commands-core.js +0 -190
- package/dist/commands-core.js.map +0 -1
- package/dist/commands-devices.d.ts +0 -41
- package/dist/commands-devices.js +0 -144
- package/dist/commands-devices.js.map +0 -1
- package/dist/commands-doctor.d.ts +0 -25
- package/dist/commands-doctor.js +0 -1553
- package/dist/commands-doctor.js.map +0 -1
- package/dist/commands-guidance.d.ts +0 -27
- package/dist/commands-guidance.js +0 -150
- package/dist/commands-guidance.js.map +0 -1
- package/dist/commands-harness-context.d.ts +0 -57
- package/dist/commands-harness-context.js +0 -102
- package/dist/commands-harness-context.js.map +0 -1
- package/dist/commands-hook-attend.d.ts +0 -70
- package/dist/commands-hook-attend.js +0 -655
- package/dist/commands-hook-attend.js.map +0 -1
- package/dist/commands-hook-diagnostics.d.ts +0 -37
- package/dist/commands-hook-diagnostics.js +0 -136
- package/dist/commands-hook-diagnostics.js.map +0 -1
- package/dist/commands-hook-install.d.ts +0 -26
- package/dist/commands-hook-install.js +0 -784
- package/dist/commands-hook-install.js.map +0 -1
- package/dist/commands-hook-run.d.ts +0 -29
- package/dist/commands-hook-run.js +0 -933
- package/dist/commands-hook-run.js.map +0 -1
- package/dist/commands-hook-shape.d.ts +0 -19
- package/dist/commands-hook-shape.js +0 -48
- package/dist/commands-hook-shape.js.map +0 -1
- package/dist/commands-init.d.ts +0 -42
- package/dist/commands-init.js +0 -901
- package/dist/commands-init.js.map +0 -1
- package/dist/commands-io.d.ts +0 -2
- package/dist/commands-io.js +0 -117
- package/dist/commands-io.js.map +0 -1
- package/dist/commands-logs.d.ts +0 -30
- package/dist/commands-logs.js +0 -201
- package/dist/commands-logs.js.map +0 -1
- package/dist/commands-native-acknowledge.d.ts +0 -8
- package/dist/commands-native-acknowledge.js +0 -114
- package/dist/commands-native-acknowledge.js.map +0 -1
- package/dist/commands-project.d.ts +0 -4
- package/dist/commands-project.js +0 -41
- package/dist/commands-project.js.map +0 -1
- package/dist/commands-receive.d.ts +0 -3
- package/dist/commands-receive.js +0 -59
- package/dist/commands-receive.js.map +0 -1
- package/dist/commands-send-support.d.ts +0 -59
- package/dist/commands-send-support.js +0 -258
- package/dist/commands-send-support.js.map +0 -1
- package/dist/commands-send.d.ts +0 -34
- package/dist/commands-send.js +0 -900
- package/dist/commands-send.js.map +0 -1
- package/dist/commands-setup-proof.d.ts +0 -43
- package/dist/commands-setup-proof.js +0 -134
- package/dist/commands-setup-proof.js.map +0 -1
- package/dist/commands-skill.d.ts +0 -21
- package/dist/commands-skill.js +0 -199
- package/dist/commands-skill.js.map +0 -1
- package/dist/commands-sounds.d.ts +0 -12
- package/dist/commands-sounds.js +0 -35
- package/dist/commands-sounds.js.map +0 -1
- package/dist/commands-update-check.d.ts +0 -6
- package/dist/commands-update-check.js +0 -102
- package/dist/commands-update-check.js.map +0 -1
- package/dist/commands-update-resume.d.ts +0 -7
- package/dist/commands-update-resume.js +0 -143
- package/dist/commands-update-resume.js.map +0 -1
- package/dist/commands-update-skill.d.ts +0 -5
- package/dist/commands-update-skill.js +0 -36
- package/dist/commands-update-skill.js.map +0 -1
- package/dist/commands-update.d.ts +0 -11
- package/dist/commands-update.js +0 -286
- package/dist/commands-update.js.map +0 -1
- package/dist/commands.d.ts +0 -26
- package/dist/commands.js +0 -27
- package/dist/commands.js.map +0 -1
- package/dist/config-schema.d.ts +0 -62
- package/dist/config-schema.js +0 -302
- package/dist/config-schema.js.map +0 -1
- package/dist/config.d.ts +0 -181
- package/dist/config.js +0 -357
- package/dist/config.js.map +0 -1
- package/dist/credentials.d.ts +0 -96
- package/dist/credentials.js +0 -342
- package/dist/credentials.js.map +0 -1
- package/dist/file-lock.d.ts +0 -34
- package/dist/file-lock.js +0 -367
- package/dist/file-lock.js.map +0 -1
- package/dist/generated-session-label.d.ts +0 -2
- package/dist/generated-session-label.js +0 -152
- package/dist/generated-session-label.js.map +0 -1
- package/dist/guidance-content.d.ts +0 -53
- package/dist/guidance-content.js +0 -248
- package/dist/guidance-content.js.map +0 -1
- package/dist/guidance-render.d.ts +0 -25
- package/dist/guidance-render.js +0 -47
- package/dist/guidance-render.js.map +0 -1
- package/dist/guidance.d.ts +0 -69
- package/dist/guidance.js +0 -173
- package/dist/guidance.js.map +0 -1
- package/dist/harness-session-title.d.ts +0 -22
- package/dist/harness-session-title.js +0 -74
- package/dist/harness-session-title.js.map +0 -1
- package/dist/harnesses.d.ts +0 -65
- package/dist/harnesses.js +0 -159
- package/dist/harnesses.js.map +0 -1
- package/dist/hermes-attendant.d.ts +0 -38
- package/dist/hermes-attendant.js +0 -337
- package/dist/hermes-attendant.js.map +0 -1
- package/dist/hermes-plugin.d.ts +0 -11
- package/dist/hermes-plugin.js +0 -299
- package/dist/hermes-plugin.js.map +0 -1
- package/dist/hook-acknowledgements.d.ts +0 -70
- package/dist/hook-acknowledgements.js +0 -490
- package/dist/hook-acknowledgements.js.map +0 -1
- package/dist/hook-adapter.d.ts +0 -48
- package/dist/hook-adapter.js +0 -419
- package/dist/hook-adapter.js.map +0 -1
- package/dist/hook-events.d.ts +0 -105
- package/dist/hook-events.js +0 -130
- package/dist/hook-events.js.map +0 -1
- package/dist/hook-gates.d.ts +0 -7
- package/dist/hook-gates.js +0 -20
- package/dist/hook-gates.js.map +0 -1
- package/dist/hook-input.d.ts +0 -10
- package/dist/hook-input.js +0 -64
- package/dist/hook-input.js.map +0 -1
- package/dist/hook-lifecycle.d.ts +0 -112
- package/dist/hook-lifecycle.js +0 -2031
- package/dist/hook-lifecycle.js.map +0 -1
- package/dist/hook-project-sessions.d.ts +0 -28
- package/dist/hook-project-sessions.js +0 -141
- package/dist/hook-project-sessions.js.map +0 -1
- package/dist/hook-question-lock.d.ts +0 -29
- package/dist/hook-question-lock.js +0 -217
- package/dist/hook-question-lock.js.map +0 -1
- package/dist/hook-question-retirement.d.ts +0 -74
- package/dist/hook-question-retirement.js +0 -314
- package/dist/hook-question-retirement.js.map +0 -1
- package/dist/hook-question-state.d.ts +0 -58
- package/dist/hook-question-state.js +0 -239
- package/dist/hook-question-state.js.map +0 -1
- package/dist/hook-session-state.d.ts +0 -127
- package/dist/hook-session-state.js +0 -488
- package/dist/hook-session-state.js.map +0 -1
- package/dist/hook-types.d.ts +0 -476
- package/dist/hook-types.js +0 -4
- package/dist/hook-types.js.map +0 -1
- package/dist/injection-render.d.ts +0 -36
- package/dist/injection-render.js +0 -61
- package/dist/injection-render.js.map +0 -1
- package/dist/install-hooks.d.ts +0 -470
- package/dist/install-hooks.js +0 -1622
- package/dist/install-hooks.js.map +0 -1
- package/dist/integration-health.d.ts +0 -26
- package/dist/integration-health.js +0 -133
- package/dist/integration-health.js.map +0 -1
- package/dist/interactive.d.ts +0 -22
- package/dist/interactive.js +0 -644
- package/dist/interactive.js.map +0 -1
- package/dist/invocation-context.d.ts +0 -47
- package/dist/invocation-context.js +0 -164
- package/dist/invocation-context.js.map +0 -1
- package/dist/local-path.d.ts +0 -7
- package/dist/local-path.js +0 -27
- package/dist/local-path.js.map +0 -1
- package/dist/logging.d.ts +0 -157
- package/dist/logging.js +0 -599
- package/dist/logging.js.map +0 -1
- package/dist/main-run.d.ts +0 -1
- package/dist/main-run.js +0 -42
- package/dist/main-run.js.map +0 -1
- package/dist/main.d.ts +0 -2
- package/dist/main.js +0 -12
- package/dist/main.js.map +0 -1
- package/dist/native-answer-operation.d.ts +0 -70
- package/dist/native-answer-operation.js +0 -247
- package/dist/native-answer-operation.js.map +0 -1
- package/dist/native-skills.d.ts +0 -64
- package/dist/native-skills.js +0 -167
- package/dist/native-skills.js.map +0 -1
- package/dist/node-floor.d.ts +0 -19
- package/dist/node-floor.js +0 -32
- package/dist/node-floor.js.map +0 -1
- package/dist/npm-invocation.d.ts +0 -34
- package/dist/npm-invocation.js +0 -53
- package/dist/npm-invocation.js.map +0 -1
- package/dist/openclaw-continuation-bridge.d.ts +0 -7
- package/dist/openclaw-continuation-bridge.js +0 -64
- package/dist/openclaw-continuation-bridge.js.map +0 -1
- package/dist/openclaw-gateway-readiness.d.ts +0 -1
- package/dist/openclaw-gateway-readiness.js +0 -24
- package/dist/openclaw-gateway-readiness.js.map +0 -1
- package/dist/openclaw-generation.d.ts +0 -21
- package/dist/openclaw-generation.js +0 -115
- package/dist/openclaw-generation.js.map +0 -1
- package/dist/openclaw-message-bridge.d.ts +0 -8
- package/dist/openclaw-message-bridge.js +0 -93
- package/dist/openclaw-message-bridge.js.map +0 -1
- package/dist/openclaw-pending.d.ts +0 -8
- package/dist/openclaw-pending.js +0 -47
- package/dist/openclaw-pending.js.map +0 -1
- package/dist/openclaw-plugin.d.ts +0 -75
- package/dist/openclaw-plugin.js +0 -1353
- package/dist/openclaw-plugin.js.map +0 -1
- package/dist/openclaw-session-access.d.ts +0 -12
- package/dist/openclaw-session-access.js +0 -79
- package/dist/openclaw-session-access.js.map +0 -1
- package/dist/opencode-plugin.d.ts +0 -88
- package/dist/opencode-plugin.js +0 -286
- package/dist/opencode-plugin.js.map +0 -1
- package/dist/orca-session-title.d.ts +0 -11
- package/dist/orca-session-title.js +0 -111
- package/dist/orca-session-title.js.map +0 -1
- package/dist/pack-feedback-slice.d.ts +0 -35
- package/dist/pack-feedback-slice.js +0 -126
- package/dist/pack-feedback-slice.js.map +0 -1
- package/dist/pairing-qr.d.ts +0 -5
- package/dist/pairing-qr.js +0 -24
- package/dist/pairing-qr.js.map +0 -1
- package/dist/pending-pairing.d.ts +0 -38
- package/dist/pending-pairing.js +0 -70
- package/dist/pending-pairing.js.map +0 -1
- package/dist/platform.d.ts +0 -67
- package/dist/platform.js +0 -140
- package/dist/platform.js.map +0 -1
- package/dist/process-identity.d.ts +0 -26
- package/dist/process-identity.js +0 -82
- package/dist/process-identity.js.map +0 -1
- package/dist/program.d.ts +0 -59
- package/dist/program.js +0 -691
- package/dist/program.js.map +0 -1
- package/dist/project-enablement.d.ts +0 -21
- package/dist/project-enablement.js +0 -71
- package/dist/project-enablement.js.map +0 -1
- package/dist/question-settlement-process.d.ts +0 -17
- package/dist/question-settlement-process.js +0 -35
- package/dist/question-settlement-process.js.map +0 -1
- package/dist/question-timing.d.ts +0 -46
- package/dist/question-timing.js +0 -48
- package/dist/question-timing.js.map +0 -1
- package/dist/readiness.d.ts +0 -153
- package/dist/readiness.js +0 -149
- package/dist/readiness.js.map +0 -1
- package/dist/release.d.ts +0 -31
- package/dist/release.js +0 -52
- package/dist/release.js.map +0 -1
- package/dist/send-attempts.d.ts +0 -26
- package/dist/send-attempts.js +0 -139
- package/dist/send-attempts.js.map +0 -1
- package/dist/send.d.ts +0 -115
- package/dist/send.js +0 -421
- package/dist/send.js.map +0 -1
- package/dist/session-activation.d.ts +0 -13
- package/dist/session-activation.js +0 -64
- package/dist/session-activation.js.map +0 -1
- package/dist/session-attendant-probe.d.ts +0 -85
- package/dist/session-attendant-probe.js +0 -173
- package/dist/session-attendant-probe.js.map +0 -1
- package/dist/session-attendant-state.d.ts +0 -70
- package/dist/session-attendant-state.js +0 -256
- package/dist/session-attendant-state.js.map +0 -1
- package/dist/session-attendant.d.ts +0 -143
- package/dist/session-attendant.js +0 -520
- package/dist/session-attendant.js.map +0 -1
- package/dist/session-delivery.d.ts +0 -249
- package/dist/session-delivery.js +0 -576
- package/dist/session-delivery.js.map +0 -1
- package/dist/session-handoff.d.ts +0 -98
- package/dist/session-handoff.js +0 -118
- package/dist/session-handoff.js.map +0 -1
- package/dist/session-input-wakes.d.ts +0 -78
- package/dist/session-input-wakes.js +0 -239
- package/dist/session-input-wakes.js.map +0 -1
- package/dist/session-inputs.d.ts +0 -57
- package/dist/session-inputs.js +0 -434
- package/dist/session-inputs.js.map +0 -1
- package/dist/session-labels.d.ts +0 -56
- package/dist/session-labels.js +0 -396
- package/dist/session-labels.js.map +0 -1
- package/dist/session-message-handoff.d.ts +0 -39
- package/dist/session-message-handoff.js +0 -107
- package/dist/session-message-handoff.js.map +0 -1
- package/dist/setup-destinations.d.ts +0 -23
- package/dist/setup-destinations.js +0 -44
- package/dist/setup-destinations.js.map +0 -1
- package/dist/skill-integrity.d.ts +0 -54
- package/dist/skill-integrity.js +0 -164
- package/dist/skill-integrity.js.map +0 -1
- package/dist/skill-source/manifest.json +0 -40
- package/dist/skill-source/notifai/SKILL.md +0 -350
- package/dist/skill-source/notifai/references/diagnostics.md +0 -36
- package/dist/skill-source/notifai/references/harness-setup.md +0 -384
- package/dist/skill-source/notifai/references/native-questions.md +0 -51
- package/dist/skill-source/notifai/references/notes-and-edits.md +0 -54
- package/dist/skill-source/notifai/references/send-details.md +0 -14
- package/dist/skill-source/notifai/references/updates.md +0 -116
- package/dist/skill-source/notifai/references/writing-examples.md +0 -56
- package/dist/sound-ref.d.ts +0 -23
- package/dist/sound-ref.js +0 -28
- package/dist/sound-ref.js.map +0 -1
- package/dist/ui/banner.d.ts +0 -22
- package/dist/ui/banner.js +0 -87
- package/dist/ui/banner.js.map +0 -1
- package/dist/ui/config-view.d.ts +0 -30
- package/dist/ui/config-view.js +0 -108
- package/dist/ui/config-view.js.map +0 -1
- package/dist/ui/help.d.ts +0 -44
- package/dist/ui/help.js +0 -107
- package/dist/ui/help.js.map +0 -1
- package/dist/ui/theme.d.ts +0 -90
- package/dist/ui/theme.js +0 -244
- package/dist/ui/theme.js.map +0 -1
- package/dist/update-handoff.d.ts +0 -29
- package/dist/update-handoff.js +0 -69
- package/dist/update-handoff.js.map +0 -1
- package/dist/url-policy.d.ts +0 -85
- package/dist/url-policy.js +0 -369
- package/dist/url-policy.js.map +0 -1
- package/dist/version.d.ts +0 -40
- package/dist/version.js +0 -132
- package/dist/version.js.map +0 -1
- package/dist/wake-support.d.ts +0 -40
- package/dist/wake-support.js +0 -95
- package/dist/wake-support.js.map +0 -1
|
@@ -1,933 +0,0 @@
|
|
|
1
|
-
/** Fail-open CLI adapter from harness input to hook lifecycle handlers. */
|
|
2
|
-
import { agentUpdateNotice } from './agent-update-notice.js';
|
|
3
|
-
import { integrationFaultNotice } from './integration-health.js';
|
|
4
|
-
import { claudeWakeRoute } from './claude-wake.js';
|
|
5
|
-
import { ApiCallError } from './client.js';
|
|
6
|
-
import { codexWakeRoute } from './codex-wake.js';
|
|
7
|
-
import { deliverCodexToolMessage } from './codex-tool-messages.js';
|
|
8
|
-
import { EXIT, SETUP_COMMAND, diagnoseIgnoredOriginOverride, log, makeClient, rejectedPaths, updateCliCommand, } from './commands-core.js';
|
|
9
|
-
import { claudeSessionPid } from './commands-harness-context.js';
|
|
10
|
-
import { attendHook, recordCodexTurnStart, reportCodexSessionEnded } from './commands-hook-attend.js';
|
|
11
|
-
import { waitForReply } from './commands-send-support.js';
|
|
12
|
-
import { loadConfig } from './config.js';
|
|
13
|
-
import { withFileLock } from './file-lock.js';
|
|
14
|
-
import { questionRoutingCapability } from './harnesses.js';
|
|
15
|
-
import { HOOK_EVENTS } from './hook-events.js';
|
|
16
|
-
import { handleSessionEnd, handleStop, handleUserPromptSubmit, parseHookInput, submitSessionQuestions, } from './hook-lifecycle.js';
|
|
17
|
-
import { claimCursorStopActivation, confirmCursorStopActivation, lifecycleStamp, pruneAbandonedSessions, readSessionIncarnation, readSessionState, updateSessionState, recordSessionStart, resetCursorStopActivation, sessionHasEnded, sessionStatePath, } from './hook-session-state.js';
|
|
18
|
-
import {} from './hook-types.js';
|
|
19
|
-
import { codexStopDefinitionFingerprint, findInstallations } from './install-hooks.js';
|
|
20
|
-
import { logConfigResolved, logSettingsFrom } from './logging.js';
|
|
21
|
-
import { observeOpenclawPrompt, observeOpenclawStart, openclawGenerationLockPath, readOpenclawGeneration, writeOpenclawGeneration, } from './openclaw-generation.js';
|
|
22
|
-
import { projectBinding, projectEnabled } from './project-enablement.js';
|
|
23
|
-
import { spawnQuestionSettlement } from './question-settlement-process.js';
|
|
24
|
-
import { QUESTION_WAITER_CEILING_SECONDS } from './question-timing.js';
|
|
25
|
-
import { cursorStopActivationOutput, sessionActivationOutput, userPromptContextOutput } from './session-activation.js';
|
|
26
|
-
import { currentProcessIdentity } from './process-identity.js';
|
|
27
|
-
import { attendantSupport } from './session-attendant-probe.js';
|
|
28
|
-
import { readAttendantEndingLease, readAttendantLease } from './session-attendant-state.js';
|
|
29
|
-
import { openclawContinuationRoute } from './openclaw-continuation-bridge.js';
|
|
30
|
-
import { listPendingOpenclawSessions } from './openclaw-pending.js';
|
|
31
|
-
import { readDeliveryJournal } from './session-delivery.js';
|
|
32
|
-
import { sessionInputRoute, stageSessionAnswers, observeSessionInputWake, sessionInputWake } from './session-inputs.js';
|
|
33
|
-
import { receiveSessionInputs } from './commands-receive.js';
|
|
34
|
-
const INTERNAL_HOOK_EVENTS = [
|
|
35
|
-
'question-submission', 'question-settlement', 'openclaw-lifecycle', 'openclaw-generation',
|
|
36
|
-
'openclaw-turn-start', 'openclaw-turn-end', 'openclaw-list-pending',
|
|
37
|
-
'openclaw-attendance-ready', 'openclaw-settlement',
|
|
38
|
-
'openclaw-verify-prepared',
|
|
39
|
-
];
|
|
40
|
-
/** Keep diagnostic context in the same document as an answer or Session Note. */
|
|
41
|
-
function appendIntegrationContext(output, notice, harness, event) {
|
|
42
|
-
if (notice === undefined)
|
|
43
|
-
return output ?? '';
|
|
44
|
-
if (harness === 'opencode' || harness === 'openclaw')
|
|
45
|
-
return [output, notice].filter(Boolean).join('\n\n');
|
|
46
|
-
if (output === undefined) {
|
|
47
|
-
return event === 'UserPromptSubmit' ? userPromptContextOutput(harness, notice) ?? ''
|
|
48
|
-
: JSON.stringify({ hookSpecificOutput: { hookEventName: event, additionalContext: notice } });
|
|
49
|
-
}
|
|
50
|
-
try {
|
|
51
|
-
const value = JSON.parse(output);
|
|
52
|
-
if (harness === 'cursor')
|
|
53
|
-
value.additional_context = [value.additional_context, notice].filter(Boolean).join('\n\n');
|
|
54
|
-
else {
|
|
55
|
-
value.hookSpecificOutput ??= { hookEventName: event };
|
|
56
|
-
value.hookSpecificOutput.additionalContext = [value.hookSpecificOutput.additionalContext, notice].filter(Boolean).join('\n\n');
|
|
57
|
-
}
|
|
58
|
-
return JSON.stringify(value);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return output;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
/** SessionEnd cleanup must precede every diagnostic that can wait on a file lock. */
|
|
65
|
-
export function hookDefersDiagnosticsUntilAfterCleanup(event) {
|
|
66
|
-
return event === 'session-end';
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Runs one harness hook. Contract with every harness: hook JSON arrives on
|
|
70
|
-
* stdin, harness output (if any) goes to stdout, diagnostics go to stderr, and
|
|
71
|
-
* exit 0 with no stdout means "no decision or added context — carry on as normal".
|
|
72
|
-
*
|
|
73
|
-
* Every failure path in here must reach that no-decision state. A hook that
|
|
74
|
-
* throws, or that blocks past the harness's timeout, degrades the agent for a
|
|
75
|
-
* feature the user only asked to make it more convenient.
|
|
76
|
-
*/
|
|
77
|
-
export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
78
|
-
if (!HOOK_EVENTS.includes(event) &&
|
|
79
|
-
!INTERNAL_HOOK_EVENTS.includes(event)) {
|
|
80
|
-
deps.io.err(`Unknown hook event "${event}". Valid: ${HOOK_EVENTS.join(', ')}`);
|
|
81
|
-
return EXIT.usage;
|
|
82
|
-
}
|
|
83
|
-
// One clock owns the complete Stop invocation, including stdin, config,
|
|
84
|
-
// credentials, and client construction. Starting this inside `handleStop`
|
|
85
|
-
// would grant slow setup a second budget and let the harness kill us before
|
|
86
|
-
// an accepted answer is journaled or written to stdout.
|
|
87
|
-
const now = deps.now ?? Date.now;
|
|
88
|
-
// Taken before stdin is read: an end the harness recorded before this
|
|
89
|
-
// invocation began belongs to an earlier incarnation of the session.
|
|
90
|
-
const invokedAt = lifecycleStamp(now());
|
|
91
|
-
// One owner lifetime covers startup and the longest answer window. Native
|
|
92
|
-
// observers run detached; held Stop routes keep stdout. The delivery mechanism does not
|
|
93
|
-
// change how long the exact Agent Session remains reachable.
|
|
94
|
-
const processDeadlineAt = now() + QUESTION_WAITER_CEILING_SECONDS * 1000;
|
|
95
|
-
const logger = log(deps);
|
|
96
|
-
logger.bind({ cmd: `hook ${event}` });
|
|
97
|
-
let started = false;
|
|
98
|
-
const start = (data = {}) => {
|
|
99
|
-
if (started)
|
|
100
|
-
return;
|
|
101
|
-
started = true;
|
|
102
|
-
logger.info('hook.start', { hook: event, harness: harness ?? 'unknown', ...data });
|
|
103
|
-
};
|
|
104
|
-
const failureData = (err) => err instanceof ApiCallError
|
|
105
|
-
? { status: err.status, code: err.code, message: err.message, details: err.details }
|
|
106
|
-
: { message: err instanceof Error ? err.message : String(err) };
|
|
107
|
-
// Cursor may load ~/.claude/settings.json in addition to its own native
|
|
108
|
-
// hooks. Cursor guarantees CURSOR_PROJECT_DIR to hook processes, so a Claude
|
|
109
|
-
// compatibility copy becomes a no-op and the native Cursor definition is
|
|
110
|
-
// the single owner. Real Claude hooks do not receive that hook-only marker.
|
|
111
|
-
if (harness === 'claude-code' && deps.env['CURSOR_PROJECT_DIR']) {
|
|
112
|
-
start({ outcome: 'cursor-compatibility-copy-skipped' });
|
|
113
|
-
logger.info('hook.end', {
|
|
114
|
-
hook: event,
|
|
115
|
-
outcome: 'ignored',
|
|
116
|
-
reason: 'cursor-native-handler-owns-event',
|
|
117
|
-
decided: false,
|
|
118
|
-
});
|
|
119
|
-
return EXIT.ok;
|
|
120
|
-
}
|
|
121
|
-
// Grok loads Claude settings by default. Its native Notifai definition is
|
|
122
|
-
// the sole owner; the inherited Claude copy must stop before reading stdin.
|
|
123
|
-
if (harness === 'claude-code' && (deps.env['GROK_HOOK_EVENT'] ?? '') !== '') {
|
|
124
|
-
start({ outcome: 'grok-compatibility-copy-skipped' });
|
|
125
|
-
logger.info('hook.end', {
|
|
126
|
-
hook: event,
|
|
127
|
-
outcome: 'ignored',
|
|
128
|
-
reason: 'grok-native-handler-owns-event',
|
|
129
|
-
decided: false,
|
|
130
|
-
});
|
|
131
|
-
return EXIT.ok;
|
|
132
|
-
}
|
|
133
|
-
let raw;
|
|
134
|
-
try {
|
|
135
|
-
raw = await readStdin();
|
|
136
|
-
}
|
|
137
|
-
catch (err) {
|
|
138
|
-
start({ input: 'unavailable' });
|
|
139
|
-
logger.error('hook.end', {
|
|
140
|
-
hook: event,
|
|
141
|
-
outcome: 'ignored',
|
|
142
|
-
reason: 'input-read-failed',
|
|
143
|
-
...failureData(err),
|
|
144
|
-
});
|
|
145
|
-
return EXIT.ok;
|
|
146
|
-
}
|
|
147
|
-
if (raw.trim() !== '') {
|
|
148
|
-
try {
|
|
149
|
-
const parsed = JSON.parse(raw);
|
|
150
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
151
|
-
throw new Error('not an object');
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
start({ input: 'malformed' });
|
|
155
|
-
logger.error('hook.end', { hook: event, outcome: 'ignored', reason: 'malformed-input' });
|
|
156
|
-
deps.io.err('notifai: ignored malformed or truncated hook input; no routing action was taken');
|
|
157
|
-
return EXIT.ok;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
let envelope = parseHookInput(raw);
|
|
161
|
-
// Codex subagents share the root session_id and harness process, while
|
|
162
|
-
// turn_id belongs to the child. They must never own the root's lifecycle
|
|
163
|
-
// or consume its input. SubagentStart only teaches worker ownership and
|
|
164
|
-
// deliberately does not activate or update the parent session.
|
|
165
|
-
if (harness === 'codex' && event !== 'subagent-start' &&
|
|
166
|
-
(envelope.agent_id !== undefined || envelope.agent_type !== undefined)) {
|
|
167
|
-
start({ outcome: 'codex-child-callback-skipped' });
|
|
168
|
-
logger.info('hook.end', {
|
|
169
|
-
hook: event, outcome: 'ignored', reason: 'codex-subagent-owns-event', decided: false,
|
|
170
|
-
});
|
|
171
|
-
return EXIT.ok;
|
|
172
|
-
}
|
|
173
|
-
if (harness === 'cursor') {
|
|
174
|
-
const sessionId = envelope.session_id ?? envelope.conversation_id;
|
|
175
|
-
const cwd = envelope.cwd ?? envelope.workspace_roots?.[0];
|
|
176
|
-
envelope = {
|
|
177
|
-
...envelope,
|
|
178
|
-
...(sessionId === undefined ? {} : { session_id: sessionId }),
|
|
179
|
-
...(cwd === undefined ? {} : { cwd }),
|
|
180
|
-
stop_hook_active: envelope.stop_hook_active ??
|
|
181
|
-
(typeof envelope.loop_count === 'number' && envelope.loop_count > 0),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
if (harness === 'grok') {
|
|
185
|
-
envelope = {
|
|
186
|
-
...envelope,
|
|
187
|
-
...(envelope.stop_hook_active === undefined && envelope.stopHookActive !== undefined
|
|
188
|
-
? { stop_hook_active: envelope.stopHookActive }
|
|
189
|
-
: {}),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
const cwd = envelope.cwd ?? deps.cwd;
|
|
193
|
-
if (event === 'openclaw-lifecycle') {
|
|
194
|
-
logger.bind({ session: envelope.session_id ?? null });
|
|
195
|
-
start({ cwd, event: envelope.hook_event_name ?? null });
|
|
196
|
-
if (harness !== 'openclaw' || envelope.session_id === undefined)
|
|
197
|
-
return EXIT.ok;
|
|
198
|
-
try {
|
|
199
|
-
const sessionKey = envelope.session_id;
|
|
200
|
-
const outcome = withFileLock(openclawGenerationLockPath(sessionKey, deps.env), () => {
|
|
201
|
-
const current = readOpenclawGeneration(sessionKey, deps.env);
|
|
202
|
-
if (envelope.hook_event_name === 'SessionStart') {
|
|
203
|
-
const next = observeOpenclawStart(current, envelope.openclaw_session_id, envelope.openclaw_resumed_from);
|
|
204
|
-
if (envelope.openclaw_session_id !== undefined &&
|
|
205
|
-
next.sessionId !== envelope.openclaw_session_id)
|
|
206
|
-
return 'stale-start-ignored';
|
|
207
|
-
writeOpenclawGeneration(sessionKey, deps.env, next);
|
|
208
|
-
return 'start-observed';
|
|
209
|
-
}
|
|
210
|
-
if (envelope.hook_event_name === 'BeforeReset' && current !== null && !current.ended) {
|
|
211
|
-
// OpenClaw dispatches before_reset after committing its new entry and
|
|
212
|
-
// does not await the hook. A late old reset must not end a generation
|
|
213
|
-
// already observed from the replacement entry's native revision.
|
|
214
|
-
if (current.lifecycleRevision !== undefined &&
|
|
215
|
-
(typeof envelope.openclaw_lifecycle_revision !== 'string' ||
|
|
216
|
-
envelope.openclaw_lifecycle_revision === current.lifecycleRevision ||
|
|
217
|
-
current.supersededLifecycleRevisions?.includes(envelope.openclaw_lifecycle_revision))) {
|
|
218
|
-
return 'stale-reset-ignored';
|
|
219
|
-
}
|
|
220
|
-
if (envelope.openclaw_session_id !== undefined && current.sessionId !== undefined &&
|
|
221
|
-
envelope.openclaw_session_id !== current.sessionId)
|
|
222
|
-
return 'stale-reset-ignored';
|
|
223
|
-
if (current.activated)
|
|
224
|
-
handleSessionEnd(deps.env, envelope, now());
|
|
225
|
-
writeOpenclawGeneration(sessionKey, deps.env, {
|
|
226
|
-
...current, ended: true, resetPending: true,
|
|
227
|
-
});
|
|
228
|
-
return 'reset-observed';
|
|
229
|
-
}
|
|
230
|
-
return 'ignored';
|
|
231
|
-
});
|
|
232
|
-
logger.info('hook.end', { hook: event, outcome, decided: false });
|
|
233
|
-
}
|
|
234
|
-
catch (err) {
|
|
235
|
-
logger.error('hook.end', { hook: event, outcome: 'failed', ...failureData(err) });
|
|
236
|
-
}
|
|
237
|
-
return EXIT.ok;
|
|
238
|
-
}
|
|
239
|
-
if (event === 'attend') {
|
|
240
|
-
logger.bind({ session: envelope.session_id ?? null });
|
|
241
|
-
start({ cwd, event: envelope.hook_event_name ?? null, source: envelope.source ?? null });
|
|
242
|
-
try {
|
|
243
|
-
return await attendHook(deps, { envelope, harness, cwd, invokedAt, logger });
|
|
244
|
-
}
|
|
245
|
-
catch (err) {
|
|
246
|
-
// A resident process that throws must still hand the harness exit 0.
|
|
247
|
-
logger.error('hook.end', { hook: event, outcome: 'failed', ...failureData(err) });
|
|
248
|
-
return EXIT.ok;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
const launchSettlement = () => {
|
|
252
|
-
const sessionId = envelope.session_id;
|
|
253
|
-
if (sessionId === undefined || harness === undefined)
|
|
254
|
-
return {};
|
|
255
|
-
try {
|
|
256
|
-
const launched = withFileLock(`${sessionStatePath(sessionId, deps.env)}.lock`, () => {
|
|
257
|
-
if (sessionHasEnded(sessionId, deps.env))
|
|
258
|
-
return false;
|
|
259
|
-
const current = readSessionState(sessionId, deps.env);
|
|
260
|
-
if ((current.pending?.length ?? 0) === 0 && current.accepted === undefined)
|
|
261
|
-
return false;
|
|
262
|
-
const spawnSettlement = deps.spawnQuestionSettlement ?? spawnQuestionSettlement;
|
|
263
|
-
spawnSettlement({
|
|
264
|
-
envelope: { session_id: sessionId, cwd }, harness,
|
|
265
|
-
});
|
|
266
|
-
return true;
|
|
267
|
-
});
|
|
268
|
-
return { settlement: launched ? 'launched' : 'cancelled' };
|
|
269
|
-
}
|
|
270
|
-
catch (err) {
|
|
271
|
-
return { settlement: 'launch-failed', settlement_error: failureData(err) };
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
const sessionEnd = hookDefersDiagnosticsUntilAfterCleanup(event);
|
|
275
|
-
// Codex may kill the attendant before this hook starts. Capture its saved
|
|
276
|
-
// fencing identity before cleanup or another start can replace local state.
|
|
277
|
-
const codexEndingLease = sessionEnd && harness === 'codex' && envelope.session_id !== undefined
|
|
278
|
-
? readAttendantEndingLease(envelope.session_id, deps.env)
|
|
279
|
-
: null;
|
|
280
|
-
logger.bind({ session: envelope.session_id ?? null });
|
|
281
|
-
const lifecycleEnabled = () => {
|
|
282
|
-
try {
|
|
283
|
-
const activationConfig = loadConfig({ cwd, env: deps.env, sessionId: envelope.session_id });
|
|
284
|
-
logger.bind({ project: activationConfig.project.value });
|
|
285
|
-
return projectEnabled(projectBinding(cwd, deps.env, activationConfig.project.value));
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
logger.error('hook.end', { hook: event, outcome: 'enablement-unavailable', ...failureData(err) });
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
const currentOpenclawOwner = () => {
|
|
293
|
-
if (harness !== 'openclaw' || envelope.session_id === undefined || !lifecycleEnabled())
|
|
294
|
-
return null;
|
|
295
|
-
const current = readOpenclawGeneration(envelope.session_id, deps.env);
|
|
296
|
-
const incarnation = readSessionIncarnation(envelope.session_id, deps.env);
|
|
297
|
-
return current !== null && current.activated && !current.ended &&
|
|
298
|
-
incarnation?.openclaw_generation === current.id &&
|
|
299
|
-
!sessionHasEnded(envelope.session_id, deps.env) &&
|
|
300
|
-
(envelope.openclaw_lifecycle_revision === undefined ||
|
|
301
|
-
current.lifecycleRevision === envelope.openclaw_lifecycle_revision) &&
|
|
302
|
-
(envelope.openclaw_session_id === undefined || current.sessionId === envelope.openclaw_session_id)
|
|
303
|
-
? current.id : null;
|
|
304
|
-
};
|
|
305
|
-
if (event === 'post-tool-use') {
|
|
306
|
-
start({ cwd });
|
|
307
|
-
try {
|
|
308
|
-
if (harness === 'codex' && lifecycleEnabled()) {
|
|
309
|
-
const notice = integrationFaultNotice({ ...deps, cwd }, harness);
|
|
310
|
-
let wrote = false;
|
|
311
|
-
const io = { ...deps.io, out: (line) => {
|
|
312
|
-
wrote = true;
|
|
313
|
-
deps.io.out(appendIntegrationContext(line, notice, harness, 'PostToolUse'));
|
|
314
|
-
} };
|
|
315
|
-
await deliverCodexToolMessage({ ...deps, io }, envelope, logger);
|
|
316
|
-
if (!wrote && notice !== undefined)
|
|
317
|
-
deps.io.out(appendIntegrationContext(undefined, notice, harness, 'PostToolUse'));
|
|
318
|
-
}
|
|
319
|
-
logger.info('hook.end', { hook: event, outcome: 'checked', decided: false });
|
|
320
|
-
}
|
|
321
|
-
catch (err) {
|
|
322
|
-
logger.error('hook.end', { hook: event, outcome: 'ignored', ...failureData(err) });
|
|
323
|
-
}
|
|
324
|
-
return EXIT.ok;
|
|
325
|
-
}
|
|
326
|
-
if (event === 'openclaw-generation') {
|
|
327
|
-
start({ cwd });
|
|
328
|
-
const generation = currentOpenclawOwner();
|
|
329
|
-
if (generation !== null)
|
|
330
|
-
deps.io.out(generation);
|
|
331
|
-
return EXIT.ok;
|
|
332
|
-
}
|
|
333
|
-
if (event === 'openclaw-turn-start') {
|
|
334
|
-
start({ cwd });
|
|
335
|
-
const generation = currentOpenclawOwner();
|
|
336
|
-
if (generation !== null && envelope.session_id !== undefined) {
|
|
337
|
-
updateSessionState(envelope.session_id, deps.env, (state) => ({
|
|
338
|
-
...state, last_prompt_at: now(),
|
|
339
|
-
}));
|
|
340
|
-
}
|
|
341
|
-
return EXIT.ok;
|
|
342
|
-
}
|
|
343
|
-
if (event === 'openclaw-turn-end') {
|
|
344
|
-
start({ cwd });
|
|
345
|
-
const generation = currentOpenclawOwner();
|
|
346
|
-
if (generation !== null && envelope.session_id !== undefined) {
|
|
347
|
-
updateSessionState(envelope.session_id, deps.env, (state) => ({
|
|
348
|
-
...state, last_stop_at: now(),
|
|
349
|
-
}));
|
|
350
|
-
}
|
|
351
|
-
return EXIT.ok;
|
|
352
|
-
}
|
|
353
|
-
if (event === 'openclaw-list-pending') {
|
|
354
|
-
start({ cwd });
|
|
355
|
-
if (harness === 'openclaw')
|
|
356
|
-
deps.io.out(JSON.stringify(listPendingOpenclawSessions(deps.env)));
|
|
357
|
-
return EXIT.ok;
|
|
358
|
-
}
|
|
359
|
-
if (event === 'openclaw-attendance-ready') {
|
|
360
|
-
start({ cwd });
|
|
361
|
-
if (currentOpenclawOwner() !== null && envelope.session_id !== undefined &&
|
|
362
|
-
readAttendantLease(envelope.session_id, deps.env) !== null &&
|
|
363
|
-
(() => {
|
|
364
|
-
try {
|
|
365
|
-
const config = loadConfig({ cwd, env: deps.env, sessionId: envelope.session_id });
|
|
366
|
-
return projectEnabled(projectBinding(cwd, deps.env, config.project.value));
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
return false;
|
|
370
|
-
}
|
|
371
|
-
})())
|
|
372
|
-
deps.io.out('ready');
|
|
373
|
-
return EXIT.ok;
|
|
374
|
-
}
|
|
375
|
-
if (event === 'openclaw-verify-prepared') {
|
|
376
|
-
start({ cwd });
|
|
377
|
-
const ids = envelope.openclaw_request_ids;
|
|
378
|
-
if (currentOpenclawOwner() !== null && envelope.session_id !== undefined &&
|
|
379
|
-
Array.isArray(ids) && ids.length > 0 &&
|
|
380
|
-
ids.every((id) => typeof id === 'string' && /^req_[A-Za-z0-9_-]+$/.test(id))) {
|
|
381
|
-
const entries = readDeliveryJournal(envelope.session_id, deps.env);
|
|
382
|
-
if (ids.every((id) => entries.some((entry) => entry.subject.type === 'answer' && entry.subject.request_id === id &&
|
|
383
|
-
(entry.stage === 'writing' || entry.stage === 'written' || entry.stage === 'failed')))) {
|
|
384
|
-
deps.io.out('committed');
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return EXIT.ok;
|
|
388
|
-
}
|
|
389
|
-
if (event === 'openclaw-settlement' && currentOpenclawOwner() === null)
|
|
390
|
-
return EXIT.ok;
|
|
391
|
-
// Installation only makes lifecycle hooks available. Model-visible
|
|
392
|
-
// activation is a separate User-owned Project decision, checked anew on
|
|
393
|
-
// every run so disabling takes effect without reinstalling anything.
|
|
394
|
-
if (event === 'session-start' || event === 'subagent-start') {
|
|
395
|
-
start({ cwd, source: envelope.source ?? null });
|
|
396
|
-
if (!lifecycleEnabled()) {
|
|
397
|
-
logger.info('hook.end', {
|
|
398
|
-
hook: event,
|
|
399
|
-
outcome: 'ignored',
|
|
400
|
-
reason: 'project-disabled',
|
|
401
|
-
decided: false,
|
|
402
|
-
});
|
|
403
|
-
return EXIT.ok;
|
|
404
|
-
}
|
|
405
|
-
if (event === 'session-start' && harness === 'cursor' && envelope.session_id !== undefined) {
|
|
406
|
-
try {
|
|
407
|
-
resetCursorStopActivation(envelope.session_id, deps.env);
|
|
408
|
-
}
|
|
409
|
-
catch (err) {
|
|
410
|
-
logger.error('hook.end', {
|
|
411
|
-
hook: event,
|
|
412
|
-
outcome: 'reset-failed',
|
|
413
|
-
...failureData(err),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
const updateNotice = event === 'session-start' &&
|
|
418
|
-
['claude-code', 'codex', 'opencode', 'openclaw'].includes(harness ?? '')
|
|
419
|
-
? await agentUpdateNotice({ env: deps.env, now: now(), updateCommand: updateCliCommand(deps),
|
|
420
|
-
...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }) })
|
|
421
|
-
: undefined;
|
|
422
|
-
const faultNotice = event === 'session-start' && harness !== undefined && harness !== 'grok'
|
|
423
|
-
? integrationFaultNotice({ ...deps, cwd }, harness) : undefined;
|
|
424
|
-
const notice = [updateNotice, faultNotice].filter(part => part !== undefined).join('\n\n') || undefined;
|
|
425
|
-
if (harness === 'openclaw' && envelope.session_id !== undefined) {
|
|
426
|
-
try {
|
|
427
|
-
const sessionKey = envelope.session_id;
|
|
428
|
-
const outcome = withFileLock(openclawGenerationLockPath(sessionKey, deps.env), () => {
|
|
429
|
-
const current = readOpenclawGeneration(sessionKey, deps.env);
|
|
430
|
-
if (envelope.openclaw_lifecycle_revision !== undefined &&
|
|
431
|
-
current?.supersededLifecycleRevisions?.includes(envelope.openclaw_lifecycle_revision)) {
|
|
432
|
-
return 'stale-prompt-ignored';
|
|
433
|
-
}
|
|
434
|
-
const generation = observeOpenclawPrompt(current, envelope.openclaw_session_id, envelope.openclaw_lifecycle_revision);
|
|
435
|
-
if (envelope.openclaw_session_id !== undefined &&
|
|
436
|
-
generation.sessionId !== envelope.openclaw_session_id)
|
|
437
|
-
return 'stale-prompt-ignored';
|
|
438
|
-
writeOpenclawGeneration(sessionKey, deps.env, generation);
|
|
439
|
-
if (generation.activated)
|
|
440
|
-
return 'already-activated';
|
|
441
|
-
const previous = readSessionIncarnation(sessionKey, deps.env);
|
|
442
|
-
if (previous !== null && previous.openclaw_generation !== generation.id) {
|
|
443
|
-
// A new sessionId or resumed generation is authoritative even if
|
|
444
|
-
// its old session_end was dropped or delayed.
|
|
445
|
-
handleSessionEnd(deps.env, envelope, now());
|
|
446
|
-
}
|
|
447
|
-
const output = sessionActivationOutput(harness, event === 'session-start' ? 'SessionStart' : 'SubagentStart', cwd, deps.env, notice);
|
|
448
|
-
if (output === undefined || output.trim().length === 0)
|
|
449
|
-
return 'activation-unavailable';
|
|
450
|
-
recordSessionStart(sessionKey, deps.env, harness, cwd, undefined, lifecycleStamp(now()), generation.id);
|
|
451
|
-
writeOpenclawGeneration(sessionKey, deps.env, { ...generation, activated: true });
|
|
452
|
-
deps.io.out(output);
|
|
453
|
-
return 'context-added';
|
|
454
|
-
});
|
|
455
|
-
logger.info('hook.end', { hook: event, outcome, decided: false });
|
|
456
|
-
}
|
|
457
|
-
catch (err) {
|
|
458
|
-
logger.error('hook.end', {
|
|
459
|
-
hook: event, outcome: 'record-failed', reason: 'session-state-failed', ...failureData(err),
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
return EXIT.ok;
|
|
463
|
-
}
|
|
464
|
-
const stdout = sessionActivationOutput(harness, event === 'session-start' ? 'SessionStart' : 'SubagentStart', cwd, deps.env, notice);
|
|
465
|
-
if (stdout !== undefined)
|
|
466
|
-
deps.io.out(stdout);
|
|
467
|
-
let settlementRecovery = {};
|
|
468
|
-
if (event === 'session-start' && envelope.session_id !== undefined) {
|
|
469
|
-
try {
|
|
470
|
-
const stopFingerprint = harness === 'codex'
|
|
471
|
-
? codexStopDefinitionFingerprint(findInstallations(deps.env, deps.hookAdapterHome, deps.hookPlatform))
|
|
472
|
-
: undefined;
|
|
473
|
-
recordSessionStart(envelope.session_id, deps.env, harness, cwd, stopFingerprint, invokedAt);
|
|
474
|
-
if (harness === 'codex') {
|
|
475
|
-
// Pending state is the durable handoff debt if the prior process
|
|
476
|
-
// died after queue commit but before starting its successor.
|
|
477
|
-
settlementRecovery = launchSettlement();
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
catch (err) {
|
|
481
|
-
logger.error('hook.end', {
|
|
482
|
-
hook: event,
|
|
483
|
-
outcome: 'record-failed',
|
|
484
|
-
reason: 'session-state-failed',
|
|
485
|
-
...failureData(err),
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
logger.info('hook.end', {
|
|
490
|
-
hook: event,
|
|
491
|
-
outcome: stdout === undefined
|
|
492
|
-
? harness === 'grok' ? 'activation-context-unsupported' : 'unsupported-harness'
|
|
493
|
-
: 'context-added',
|
|
494
|
-
decided: false,
|
|
495
|
-
...settlementRecovery,
|
|
496
|
-
});
|
|
497
|
-
return EXIT.ok;
|
|
498
|
-
}
|
|
499
|
-
// A Codex turn starts here. This prompt hook is synchronous, so Codex runs it
|
|
500
|
-
// before the turn and one turn at a time: the Session Attendant reads the
|
|
501
|
-
// thread's activity from starts recorded in that order.
|
|
502
|
-
if (event === 'user-prompt-submit' && harness === 'codex')
|
|
503
|
-
recordCodexTurnStart(envelope, deps.env);
|
|
504
|
-
// Cursor has a confirmed host bug in which sessionStart.additional_context
|
|
505
|
-
// is accepted but never reaches the model. A native Stop follow-up is the
|
|
506
|
-
// host's guaranteed model-visible channel. Claim it once per conversation,
|
|
507
|
-
// before config/auth/network, and leave the ordinary Stop handler separate
|
|
508
|
-
// so question delivery is never displaced by activation.
|
|
509
|
-
if (event === 'activation-stop') {
|
|
510
|
-
start({ cwd, stop_hook_active: envelope.stop_hook_active ?? null });
|
|
511
|
-
if (harness !== 'cursor') {
|
|
512
|
-
logger.info('hook.end', { hook: event, outcome: 'unsupported-harness', decided: false });
|
|
513
|
-
return EXIT.ok;
|
|
514
|
-
}
|
|
515
|
-
if (!lifecycleEnabled()) {
|
|
516
|
-
logger.info('hook.end', {
|
|
517
|
-
hook: event,
|
|
518
|
-
outcome: 'ignored',
|
|
519
|
-
reason: 'project-disabled',
|
|
520
|
-
decided: false,
|
|
521
|
-
});
|
|
522
|
-
return EXIT.ok;
|
|
523
|
-
}
|
|
524
|
-
if (envelope.status === 'aborted') {
|
|
525
|
-
logger.info('hook.end', {
|
|
526
|
-
hook: event,
|
|
527
|
-
outcome: 'ignored',
|
|
528
|
-
reason: `turn-${envelope.status}`,
|
|
529
|
-
decided: false,
|
|
530
|
-
});
|
|
531
|
-
return EXIT.ok;
|
|
532
|
-
}
|
|
533
|
-
if (envelope.session_id === undefined) {
|
|
534
|
-
logger.info('hook.end', {
|
|
535
|
-
hook: event,
|
|
536
|
-
outcome: 'ignored',
|
|
537
|
-
reason: 'missing-conversation-id',
|
|
538
|
-
decided: false,
|
|
539
|
-
});
|
|
540
|
-
return EXIT.ok;
|
|
541
|
-
}
|
|
542
|
-
if (envelope.session_id !== undefined &&
|
|
543
|
-
typeof envelope.loop_count === 'number' &&
|
|
544
|
-
envelope.loop_count > 0) {
|
|
545
|
-
let activationOwned = false;
|
|
546
|
-
try {
|
|
547
|
-
activationOwned = confirmCursorStopActivation(envelope.session_id, deps.env, now());
|
|
548
|
-
}
|
|
549
|
-
catch (err) {
|
|
550
|
-
logger.error('hook.end', {
|
|
551
|
-
hook: event,
|
|
552
|
-
outcome: 'confirm-failed',
|
|
553
|
-
...failureData(err),
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
if (activationOwned) {
|
|
557
|
-
logger.info('hook.end', { hook: event, outcome: 'confirmed', decided: false });
|
|
558
|
-
return EXIT.ok;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
const cursorState = readSessionState(envelope.session_id, deps.env);
|
|
562
|
-
if ((cursorState.pending?.length ?? 0) > 0 ||
|
|
563
|
-
cursorState.accepted !== undefined ||
|
|
564
|
-
(cursorState.acknowledgement_due?.length ?? 0) > 0) {
|
|
565
|
-
logger.info('hook.end', {
|
|
566
|
-
hook: event,
|
|
567
|
-
outcome: 'deferred',
|
|
568
|
-
reason: 'question-continuation-owns-stop',
|
|
569
|
-
decided: false,
|
|
570
|
-
});
|
|
571
|
-
return EXIT.ok;
|
|
572
|
-
}
|
|
573
|
-
let claimed = false;
|
|
574
|
-
try {
|
|
575
|
-
claimed = claimCursorStopActivation(envelope.session_id, deps.env, now());
|
|
576
|
-
}
|
|
577
|
-
catch (err) {
|
|
578
|
-
logger.error('hook.end', {
|
|
579
|
-
hook: event,
|
|
580
|
-
outcome: 'claim-failed',
|
|
581
|
-
...failureData(err),
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
logger.info('hook.end', {
|
|
585
|
-
hook: event,
|
|
586
|
-
outcome: claimed ? 'followup-added' : 'already-activated',
|
|
587
|
-
decided: claimed,
|
|
588
|
-
});
|
|
589
|
-
if (claimed) {
|
|
590
|
-
const updateNotice = await agentUpdateNotice({ env: deps.env, now: now(), updateCommand: updateCliCommand(deps),
|
|
591
|
-
...(deps.fetchImpl === undefined ? {} : { fetchImpl: deps.fetchImpl }) });
|
|
592
|
-
const notice = [updateNotice, integrationFaultNotice({ ...deps, cwd }, 'cursor')].filter(Boolean).join('\n\n') || undefined;
|
|
593
|
-
deps.io.out(cursorStopActivationOutput(cwd, deps.env, notice));
|
|
594
|
-
}
|
|
595
|
-
return EXIT.ok;
|
|
596
|
-
}
|
|
597
|
-
let config = null;
|
|
598
|
-
let configFailure;
|
|
599
|
-
try {
|
|
600
|
-
config = loadConfig({ cwd, env: deps.env, sessionId: envelope.session_id });
|
|
601
|
-
// The hook's project is the session's, not this process's, and the log
|
|
602
|
-
// settings that apply are that project's too. Keeping a mutable bootstrap
|
|
603
|
-
// logger lets this more-specific layer turn logging back on.
|
|
604
|
-
logger.adopt(logSettingsFrom(config));
|
|
605
|
-
logger.bind({ project: config.project.value });
|
|
606
|
-
if (!sessionEnd) {
|
|
607
|
-
start({ cwd, stop_hook_active: envelope.stop_hook_active ?? null });
|
|
608
|
-
logConfigResolved(logger, config);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
catch (err) {
|
|
612
|
-
configFailure = err;
|
|
613
|
-
if (!sessionEnd) {
|
|
614
|
-
start({ cwd, stop_hook_active: envelope.stop_hook_active ?? null });
|
|
615
|
-
logger.error('hook.end', {
|
|
616
|
-
hook: event,
|
|
617
|
-
outcome: 'failed',
|
|
618
|
-
reason: 'config-failed',
|
|
619
|
-
...failureData(err),
|
|
620
|
-
});
|
|
621
|
-
for (const line of describeHookFailure(err))
|
|
622
|
-
deps.io.err(`notifai: ${line}`);
|
|
623
|
-
return EXIT.ok;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
// Everything below is inside one fail-open boundary. Credential loading,
|
|
627
|
-
// client construction and hook handling can all throw, and a hook that exits
|
|
628
|
-
// non-zero makes the harness report a failure — strictly worse than skipping.
|
|
629
|
-
try {
|
|
630
|
-
if (sessionEnd) {
|
|
631
|
-
// Codex gives SessionEnd one second total. Do every durable cleanup write
|
|
632
|
-
// before lifecycle diagnostics: the log lock is deliberately allowed to
|
|
633
|
-
// wait that long, and a busy log must never preserve ended-session state
|
|
634
|
-
// or its inherited configuration. The resolved config above is retained
|
|
635
|
-
// in memory so logging still uses the ending session's settings afterwards.
|
|
636
|
-
const outcome = harness === 'openclaw' && envelope.session_id !== undefined
|
|
637
|
-
? withFileLock(openclawGenerationLockPath(envelope.session_id, deps.env), () => {
|
|
638
|
-
const sessionKey = envelope.session_id;
|
|
639
|
-
const current = readOpenclawGeneration(sessionKey, deps.env);
|
|
640
|
-
const reason = envelope.openclaw_reason;
|
|
641
|
-
if (reason === 'new' || reason === 'reset') {
|
|
642
|
-
// These can arrive after a same-ID replacement. before_reset
|
|
643
|
-
// owns cleanup; an end event alone cannot identify the old ID.
|
|
644
|
-
return { notes: [], log: { outcome: 'ignored', reason: 'reset-end' } };
|
|
645
|
-
}
|
|
646
|
-
if (current === null || current.ended ||
|
|
647
|
-
(envelope.openclaw_session_id !== undefined && current.sessionId !== undefined &&
|
|
648
|
-
envelope.openclaw_session_id !== current.sessionId)) {
|
|
649
|
-
return { notes: [], log: { outcome: 'ignored', reason: 'stale-openclaw-generation' } };
|
|
650
|
-
}
|
|
651
|
-
const ended = current.activated
|
|
652
|
-
? handleSessionEnd(deps.env, envelope, now())
|
|
653
|
-
: { notes: [], log: { outcome: 'ignored', reason: 'inactive-openclaw-generation' } };
|
|
654
|
-
writeOpenclawGeneration(sessionKey, deps.env, { ...current, ended: true });
|
|
655
|
-
return ended;
|
|
656
|
-
})
|
|
657
|
-
: handleSessionEnd(deps.env, envelope, now());
|
|
658
|
-
start({ cwd, stop_hook_active: envelope.stop_hook_active ?? null });
|
|
659
|
-
if (config !== null)
|
|
660
|
-
logConfigResolved(logger, config);
|
|
661
|
-
const data = { hook: event, decided: false, ...outcome.log };
|
|
662
|
-
if (configFailure === undefined)
|
|
663
|
-
logger.info('hook.end', data);
|
|
664
|
-
else {
|
|
665
|
-
logger.error('hook.end', {
|
|
666
|
-
...data,
|
|
667
|
-
reason: 'config-failed',
|
|
668
|
-
config_error: failureData(configFailure),
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
for (const note of outcome.notes)
|
|
672
|
-
deps.io.err(`notifai: ${note}`);
|
|
673
|
-
if (harness === 'codex' && envelope.session_id !== undefined) {
|
|
674
|
-
// Codex kills the attendant as soon as this hook returns.
|
|
675
|
-
const reported = await reportCodexSessionEnded(deps, envelope.session_id, codexEndingLease);
|
|
676
|
-
logger.info('attendant.lease', { event: 'ended-by-session-end', outcome: reported });
|
|
677
|
-
}
|
|
678
|
-
return EXIT.ok;
|
|
679
|
-
}
|
|
680
|
-
// Non-SessionEnd hooks cannot reach here without resolved configuration.
|
|
681
|
-
const resolved = config;
|
|
682
|
-
const credential = deps.store.load();
|
|
683
|
-
if (!credential) {
|
|
684
|
-
logger.error('hook.end', { hook: event, outcome: 'not-paired' });
|
|
685
|
-
deps.io.err(`notifai: hook skipped: this machine is not paired; run \`${SETUP_COMMAND}\``);
|
|
686
|
-
return EXIT.ok;
|
|
687
|
-
}
|
|
688
|
-
// Pin authenticated traffic to the origin the credential was issued for. A
|
|
689
|
-
// repository can commit `.notifai/config.toml`, and honouring a base_url
|
|
690
|
-
// from it would hand this machine's bearer token to whatever host it names.
|
|
691
|
-
const baseUrl = credential.baseUrl;
|
|
692
|
-
diagnoseIgnoredOriginOverride(deps.io, resolved, credential);
|
|
693
|
-
// UserPromptSubmit runs in front of the user's own prompt under a 15s
|
|
694
|
-
// harness ceiling and can make two calls, so each gets a small slice of it;
|
|
695
|
-
// Stop is allowed to block and keeps the ordinary budget.
|
|
696
|
-
const client = makeClient(deps, baseUrl, `Bearer nfm_${credential.machineId}.${credential.secret}`, {
|
|
697
|
-
timeoutMs: event === 'user-prompt-submit' ? 4_000 : 20_000,
|
|
698
|
-
...(event === 'stop' || event === 'question-settlement'
|
|
699
|
-
? { deadlineAt: processDeadlineAt, now }
|
|
700
|
-
: {}),
|
|
701
|
-
});
|
|
702
|
-
const ctx = {
|
|
703
|
-
client,
|
|
704
|
-
service_identity: { base_url: credential.baseUrl, machine_id: credential.machineId },
|
|
705
|
-
config: resolved,
|
|
706
|
-
env: deps.env,
|
|
707
|
-
now,
|
|
708
|
-
sleep: deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
709
|
-
waitForFirstReply: async (requestId, timeoutSeconds) => {
|
|
710
|
-
const result = await waitForReply(client, requestId, {
|
|
711
|
-
timeoutSeconds,
|
|
712
|
-
afterSeq: 0,
|
|
713
|
-
now: deps.now,
|
|
714
|
-
sleep: deps.sleep,
|
|
715
|
-
});
|
|
716
|
-
return {
|
|
717
|
-
replies: result.response.replies,
|
|
718
|
-
timedOut: result.timedOut,
|
|
719
|
-
degraded: result.degraded,
|
|
720
|
-
};
|
|
721
|
-
},
|
|
722
|
-
log: logger,
|
|
723
|
-
...(harness === undefined ? {} : { harness }),
|
|
724
|
-
...answerClaimsFor(deps, harness, envelope.session_id, event),
|
|
725
|
-
};
|
|
726
|
-
if (event === 'question-submission') {
|
|
727
|
-
const notes = [];
|
|
728
|
-
const sessionId = envelope.session_id;
|
|
729
|
-
if (sessionId === undefined || harness === undefined ||
|
|
730
|
-
readSessionState(sessionId, deps.env).harness !== harness || !lifecycleEnabled())
|
|
731
|
-
return EXIT.ok;
|
|
732
|
-
await submitSessionQuestions(ctx, envelope, processDeadlineAt, notes);
|
|
733
|
-
// Native queues can observe answers while the asking turn keeps working.
|
|
734
|
-
// Held Stop and plugin routes retain their own genuine output owner.
|
|
735
|
-
const nativeObserver = harness === 'codex' ||
|
|
736
|
-
(harness === 'claude-code' && (deps.hookPlatform ?? process.platform) !== 'win32');
|
|
737
|
-
logger.info('hook.end', { hook: event, outcome: 'submission-checked', notes,
|
|
738
|
-
...(nativeObserver ? launchSettlement() : {}) });
|
|
739
|
-
return EXIT.ok;
|
|
740
|
-
}
|
|
741
|
-
// Real clock, deliberately, not `deps.now`. This compares against file
|
|
742
|
-
// mtimes, which are wall-clock facts — handing it a virtual or skewed clock
|
|
743
|
-
// would have it delete live session state as "abandoned".
|
|
744
|
-
// Daily state pruning is housekeeping, not part of the Stop delivery
|
|
745
|
-
// contract. Its directory scan has no useful bound, so keep it on the
|
|
746
|
-
// short prompt path and never spend the answer owner's finite budget on it.
|
|
747
|
-
if (event !== 'stop' && event !== 'question-settlement' && event !== 'openclaw-settlement') {
|
|
748
|
-
pruneAbandonedSessions(deps.env);
|
|
749
|
-
}
|
|
750
|
-
let outcome;
|
|
751
|
-
if (event === 'user-prompt-submit') {
|
|
752
|
-
if (envelope.session_id !== undefined)
|
|
753
|
-
observeSessionInputWake(envelope.session_id, deps.env, envelope.prompt);
|
|
754
|
-
const notice = lifecycleEnabled() && harness !== undefined && harness !== 'grok'
|
|
755
|
-
? integrationFaultNotice({ ...deps, cwd }, harness) : undefined;
|
|
756
|
-
outcome = envelope.session_id !== undefined && envelope.prompt === sessionInputWake()
|
|
757
|
-
? { notes: [], log: { stage: 'input-wake-observed' } }
|
|
758
|
-
: await handleUserPromptSubmit(ctx, envelope);
|
|
759
|
-
if (notice !== undefined)
|
|
760
|
-
outcome.stdout = appendIntegrationContext(outcome.stdout, notice, harness, 'UserPromptSubmit');
|
|
761
|
-
}
|
|
762
|
-
else {
|
|
763
|
-
outcome = await handleStop(ctx, envelope, processDeadlineAt, event === 'openclaw-settlement' && envelope.session_id !== undefined
|
|
764
|
-
? openclawContinuationRoute(envelope.session_id, currentOpenclawOwner())
|
|
765
|
-
: stopWakeRoute(deps, harness, envelope.session_id, cwd, event !== 'stop'),
|
|
766
|
-
// The Gateway settlement process is not an agent turn boundary. Only
|
|
767
|
-
// agent_end records Stop; a background poll must not spend the three
|
|
768
|
-
// acknowledgement reminders or abandon a Session Message debt.
|
|
769
|
-
event === 'stop');
|
|
770
|
-
}
|
|
771
|
-
if (outcome.settlementRequired === true && harness !== undefined && harness !== 'openclaw' &&
|
|
772
|
-
questionRoutingCapability(harness, deps.hookPlatform ?? process.platform)
|
|
773
|
-
.stopContinuation !== 'unsupported') {
|
|
774
|
-
// handleStop released its question-owner lease before returning here.
|
|
775
|
-
outcome.log = { ...outcome.log, ...launchSettlement() };
|
|
776
|
-
}
|
|
777
|
-
// Answer diagnostics are already persisted once as hook.answer. Keep every
|
|
778
|
-
// other note in the lifecycle record without duplicating the user's text.
|
|
779
|
-
const notes = outcome.notes.filter((note) => !/^(?:late )?answer from /.test(note));
|
|
780
|
-
logger.info('hook.end', {
|
|
781
|
-
hook: event,
|
|
782
|
-
decided: outcome.decided ?? outcome.stdout !== undefined,
|
|
783
|
-
...(notes.length === 0 ? {} : { notes }),
|
|
784
|
-
...outcome.log,
|
|
785
|
-
});
|
|
786
|
-
for (const note of outcome.notes)
|
|
787
|
-
deps.io.err(`notifai: ${note}`);
|
|
788
|
-
let inputWritten = false;
|
|
789
|
-
if (event === 'user-prompt-submit' && (harness === 'codex' || harness === 'claude-code') &&
|
|
790
|
-
envelope.session_id !== undefined && outcome.commitStdout === undefined &&
|
|
791
|
-
readSessionIncarnation(envelope.session_id, deps.env)?.harness_process?.pid === declaredHookSourcePid(deps)) {
|
|
792
|
-
inputWritten = await receiveSessionInputs(deps, envelope.session_id, (text) => {
|
|
793
|
-
deps.io.out(appendIntegrationContext(outcome.stdout, text, harness, 'UserPromptSubmit'));
|
|
794
|
-
});
|
|
795
|
-
if (inputWritten)
|
|
796
|
-
await outcome.afterOutput?.();
|
|
797
|
-
}
|
|
798
|
-
if (!inputWritten && outcome.stdout !== undefined) {
|
|
799
|
-
// No work, await, or diagnostic may sit between this cross-process
|
|
800
|
-
// SessionEnd fence and the irreversible harness stdout write.
|
|
801
|
-
if (outcome.commitStdout === undefined || outcome.commitStdout()) {
|
|
802
|
-
deps.io.out(outcome.stdout);
|
|
803
|
-
try {
|
|
804
|
-
await outcome.afterOutput?.();
|
|
805
|
-
}
|
|
806
|
-
catch {
|
|
807
|
-
// Recording what the hand-off proved never fails the hook.
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
deps.io.err('notifai: the Agent Session ended before answer delivery; no continuation was written');
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
return EXIT.ok;
|
|
815
|
-
}
|
|
816
|
-
catch (err) {
|
|
817
|
-
// SessionEnd defers its start record until after cleanup; if cleanup itself
|
|
818
|
-
// fails, begin the after-the-fact lifecycle here before recording why.
|
|
819
|
-
if (sessionEnd)
|
|
820
|
-
start({ cwd, stop_hook_active: envelope.stop_hook_active ?? null });
|
|
821
|
-
// The hook still exits 0 — handing the terminal back is always right. What
|
|
822
|
-
// this adds is that the reason survives, including the server's own words.
|
|
823
|
-
logger.error('hook.end', {
|
|
824
|
-
hook: event,
|
|
825
|
-
outcome: 'failed',
|
|
826
|
-
reason: 'execution-failed',
|
|
827
|
-
...failureData(err),
|
|
828
|
-
});
|
|
829
|
-
for (const line of describeHookFailure(err))
|
|
830
|
-
deps.io.err(`notifai: ${line}`);
|
|
831
|
-
return EXIT.ok;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
/**
|
|
835
|
-
* The last meter for an answer this Stop hook accepted, chosen by harness.
|
|
836
|
-
*
|
|
837
|
-
* Claude's adapter needs the harness process that invoked this hook, to prove
|
|
838
|
-
* exact own-child session ownership before it posts to the inbox socket. Codex's
|
|
839
|
-
* needs only the thread id: queueing is a write to that thread's own durable
|
|
840
|
-
* inbox, which no other process can be confused for. Without an exact session id
|
|
841
|
-
* neither can prove anything, so the waiter falls back to the plain Stop
|
|
842
|
-
* continuation.
|
|
843
|
-
*/
|
|
844
|
-
function stopWakeRoute(deps, harness, sessionId, cwd, background) {
|
|
845
|
-
if (harness === 'grok')
|
|
846
|
-
return undefined;
|
|
847
|
-
if (sessionId === undefined)
|
|
848
|
-
return undefined;
|
|
849
|
-
const declaredSourcePid = declaredHookSourcePid(deps);
|
|
850
|
-
if (harness === 'claude-code') {
|
|
851
|
-
if ((deps.hookPlatform ?? process.platform) === 'win32')
|
|
852
|
-
return undefined;
|
|
853
|
-
const route = sessionInputRoute(sessionId, deps.env, claudeWakeRoute({
|
|
854
|
-
sessionId,
|
|
855
|
-
cwd,
|
|
856
|
-
sourcePid: deps.claudeSourcePid ?? declaredSourcePid ?? claudeSessionPid(deps.env),
|
|
857
|
-
...(deps.claudeWake === undefined ? {} : { adapters: deps.claudeWake }),
|
|
858
|
-
}), log(deps));
|
|
859
|
-
// A detached subprocess can be reparented after ask exits. Only the
|
|
860
|
-
// resident Session Attendant retains Claude's required own-child ancestry.
|
|
861
|
-
// Its ordinary attendance exchange wakes staged inputs, even with no notes.
|
|
862
|
-
return background ? { ...route, defer: async (accepted) => {
|
|
863
|
-
stageSessionAnswers(sessionId, deps.env, accepted);
|
|
864
|
-
} } : route;
|
|
865
|
-
}
|
|
866
|
-
if (harness === 'codex') {
|
|
867
|
-
return sessionInputRoute(sessionId, deps.env, codexWakeRoute({
|
|
868
|
-
threadId: sessionId,
|
|
869
|
-
cwd,
|
|
870
|
-
env: deps.env,
|
|
871
|
-
...(deps.codexWake === undefined ? {} : { adapters: deps.codexWake }),
|
|
872
|
-
}), log(deps), 'attendant');
|
|
873
|
-
}
|
|
874
|
-
return undefined;
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* The Stop waiter claims fenced answers only where a Session Attendant can
|
|
878
|
-
* hold the session's lease and the answer is written in place: Claude Code on
|
|
879
|
-
* macOS and Linux. Everywhere else it closes and writes exactly as before.
|
|
880
|
-
*/
|
|
881
|
-
function answerClaimsFor(deps, harness, sessionId, event) {
|
|
882
|
-
if (sessionId === undefined ||
|
|
883
|
-
(event !== 'stop' && event !== 'question-settlement' && event !== 'openclaw-settlement'))
|
|
884
|
-
return {};
|
|
885
|
-
if (!attendantSupport(harness, deps.hookPlatform ?? process.platform).supported)
|
|
886
|
-
return {};
|
|
887
|
-
const writer = deps.answerWriter === undefined ? currentProcessIdentity() : deps.answerWriter;
|
|
888
|
-
if (writer === null)
|
|
889
|
-
return {};
|
|
890
|
-
return {
|
|
891
|
-
answerClaims: {
|
|
892
|
-
lease: () => readAttendantLease(sessionId, deps.env),
|
|
893
|
-
writer,
|
|
894
|
-
monotonic: () => performance.now(),
|
|
895
|
-
},
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
/** Stable harness parent propagated by the managed adapter across child tools. */
|
|
899
|
-
function declaredHookSourcePid(deps) {
|
|
900
|
-
const value = Number(deps.env['NOTIFAI_HOOK_SOURCE_PID']);
|
|
901
|
-
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* What went wrong, in terms of what to do about it.
|
|
905
|
-
*
|
|
906
|
-
* On 2026-08-03 a contract change shipped without the server deploy that goes
|
|
907
|
-
* with it. The CLI stamped `lifecycle` on every question draft, the deployed
|
|
908
|
-
* server rejected the unknown field, and escalation stopped working entirely —
|
|
909
|
-
* announced as "hook failed, deferring to the terminal", which reads like a
|
|
910
|
-
* flaky network. The information needed to diagnose it in one second was
|
|
911
|
-
* already in hand: a 422 whose details name the offending path. It was being
|
|
912
|
-
* thrown away by `String(err)`.
|
|
913
|
-
*
|
|
914
|
-
* A hook still exits 0 whatever this says. Handing the terminal back is always
|
|
915
|
-
* right; the only question is whether the user is told anything they can use.
|
|
916
|
-
*/
|
|
917
|
-
export function describeHookFailure(err) {
|
|
918
|
-
if (!(err instanceof ApiCallError)) {
|
|
919
|
-
return [`hook failed, deferring to the terminal (${String(err)})`];
|
|
920
|
-
}
|
|
921
|
-
const lines = [`hook failed, deferring to the terminal (${err.code}: ${err.message})`];
|
|
922
|
-
const paths = rejectedPaths(err.details);
|
|
923
|
-
if (paths.length > 0)
|
|
924
|
-
lines.push(`the server rejected: ${paths.join(', ')}`);
|
|
925
|
-
// A 422 on a draft this CLI built is not a user error — this CLI's own
|
|
926
|
-
// contract produced it. Either the server is behind, or the two disagree.
|
|
927
|
-
if (err.status === 422) {
|
|
928
|
-
lines.push('this build sent a field the server did not accept, which usually means the server ' +
|
|
929
|
-
'is older than this CLI — check with `notifai doctor`');
|
|
930
|
-
}
|
|
931
|
-
return lines;
|
|
932
|
-
}
|
|
933
|
-
//# sourceMappingURL=commands-hook-run.js.map
|