@raidiant/notifai 9.0.0 → 9.2.0
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 +14 -0
- package/dist/command-session.d.ts +14 -0
- package/dist/command-session.js +30 -0
- package/dist/command-session.js.map +1 -0
- package/dist/commands-doctor.js +17 -15
- package/dist/commands-doctor.js.map +1 -1
- package/dist/commands-guidance.js +3 -29
- package/dist/commands-guidance.js.map +1 -1
- package/dist/commands-harness-context.js +21 -1
- package/dist/commands-harness-context.js.map +1 -1
- package/dist/commands-hooks.d.ts +11 -1
- package/dist/commands-hooks.js +98 -184
- package/dist/commands-hooks.js.map +1 -1
- package/dist/commands-init.d.ts +2 -0
- package/dist/commands-init.js +41 -21
- package/dist/commands-init.js.map +1 -1
- package/dist/commands-send.d.ts +1 -0
- package/dist/commands-send.js +62 -14
- package/dist/commands-send.js.map +1 -1
- package/dist/commands-setup-proof.d.ts +4 -3
- package/dist/commands-setup-proof.js +29 -14
- package/dist/commands-setup-proof.js.map +1 -1
- package/dist/commands.d.ts +1 -1
- package/dist/commands.js +1 -1
- package/dist/commands.js.map +1 -1
- package/dist/guidance-render.d.ts +20 -0
- package/dist/guidance-render.js +42 -0
- package/dist/guidance-render.js.map +1 -0
- package/dist/hooks.d.ts +16 -13
- package/dist/hooks.js +57 -32
- package/dist/hooks.js.map +1 -1
- package/dist/opencode-plugin.js +32 -5
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/program.js +21 -12
- package/dist/program.js.map +1 -1
- package/dist/readiness.d.ts +6 -0
- package/dist/readiness.js +37 -0
- package/dist/readiness.js.map +1 -1
- package/dist/send-attempts.d.ts +26 -0
- package/dist/send-attempts.js +139 -0
- package/dist/send-attempts.js.map +1 -0
- package/dist/session-activation.d.ts +4 -8
- package/dist/session-activation.js +25 -14
- package/dist/session-activation.js.map +1 -1
- package/package.json +1 -1
- package/src/command-session.ts +38 -0
- package/src/commands-doctor.ts +18 -28
- package/src/commands-guidance.ts +3 -38
- package/src/commands-harness-context.ts +20 -1
- package/src/commands-hooks.ts +219 -220
- package/src/commands-init.ts +46 -24
- package/src/commands-send.ts +70 -14
- package/src/commands-setup-proof.test.ts +72 -0
- package/src/commands-setup-proof.ts +33 -14
- package/src/commands.test.ts +209 -67
- package/src/commands.ts +1 -0
- package/src/guidance-render.ts +56 -0
- package/src/guidance.test.ts +28 -0
- package/src/hooks.test.ts +42 -137
- package/src/hooks.ts +67 -46
- package/src/install-hooks.test.ts +41 -16
- package/src/opencode-plugin.ts +35 -5
- package/src/program.ts +22 -12
- package/src/readiness.ts +44 -0
- package/src/send-attempts.test.ts +143 -0
- package/src/send-attempts.ts +188 -0
- package/src/session-activation.ts +35 -18
- package/src/skill.test.ts +22 -23
package/src/commands-hooks.ts
CHANGED
|
@@ -30,19 +30,16 @@ import {
|
|
|
30
30
|
} from './hook-adapter.js'
|
|
31
31
|
import {
|
|
32
32
|
clearAcknowledgementObligation,
|
|
33
|
-
claimPromptActivation,
|
|
34
33
|
claimCursorStopActivation,
|
|
35
34
|
confirmCursorStopActivation,
|
|
36
35
|
dropPendingQuestion,
|
|
37
36
|
handleSessionEnd,
|
|
38
37
|
handleStop,
|
|
39
38
|
handleUserPromptSubmit,
|
|
40
|
-
|
|
39
|
+
recordSessionStart,
|
|
41
40
|
parkForRetirement,
|
|
42
41
|
parseHookInput,
|
|
43
42
|
pruneAbandonedSessions,
|
|
44
|
-
readMatchingProjectSessionPointer,
|
|
45
|
-
readProjectSession,
|
|
46
43
|
readSessionState,
|
|
47
44
|
resetCursorStopActivation,
|
|
48
45
|
registerQuestion,
|
|
@@ -123,6 +120,7 @@ import {
|
|
|
123
120
|
uploadImage,
|
|
124
121
|
waitForReply,
|
|
125
122
|
} from './commands-send-support.js'
|
|
123
|
+
import { resolveCommandSession } from './command-session.js'
|
|
126
124
|
|
|
127
125
|
// ---------------------------------------------------------------------------
|
|
128
126
|
// hook / ask / close — harness integration
|
|
@@ -255,14 +253,6 @@ export async function hookRunCommand(
|
|
|
255
253
|
const cwd = envelope.cwd ?? deps.cwd
|
|
256
254
|
const sessionEnd = hookDefersDiagnosticsUntilAfterCleanup(event)
|
|
257
255
|
logger.bind({ session: envelope.session_id ?? null })
|
|
258
|
-
const declaredSourcePid = declaredHookSourcePid(deps)
|
|
259
|
-
const activationOwnerPid =
|
|
260
|
-
harness === 'claude-code'
|
|
261
|
-
? (deps.claudeSourcePid ?? declaredSourcePid ?? claudeSessionPid(deps.env))
|
|
262
|
-
: harness === 'codex'
|
|
263
|
-
? (deps.codexSourcePid ?? declaredSourcePid ?? process.ppid)
|
|
264
|
-
: undefined
|
|
265
|
-
|
|
266
256
|
// Session activation is local model context, not authenticated routing. It
|
|
267
257
|
// must survive the exact first-run states it exists to repair: no config,
|
|
268
258
|
// no credential, no Companion device, and no network.
|
|
@@ -282,24 +272,18 @@ export async function hookRunCommand(
|
|
|
282
272
|
const stdout = sessionActivationOutput(
|
|
283
273
|
harness,
|
|
284
274
|
event === 'session-start' ? 'SessionStart' : 'SubagentStart',
|
|
275
|
+
cwd,
|
|
276
|
+
deps.env,
|
|
285
277
|
)
|
|
286
278
|
if (stdout !== undefined) deps.io.out(stdout)
|
|
287
|
-
if (
|
|
288
|
-
stdout !== undefined &&
|
|
289
|
-
event === 'session-start' &&
|
|
290
|
-
activationOwnerPid !== undefined &&
|
|
291
|
-
envelope.session_id !== undefined
|
|
292
|
-
) {
|
|
279
|
+
if (event === 'session-start' && envelope.session_id !== undefined) {
|
|
293
280
|
try {
|
|
294
|
-
|
|
295
|
-
// later prompt may duplicate context, but it cannot suppress context
|
|
296
|
-
// that the host never received.
|
|
297
|
-
markSessionActivation(envelope.session_id, deps.env, now(), activationOwnerPid)
|
|
281
|
+
recordSessionStart(envelope.session_id, deps.env, harness, cwd)
|
|
298
282
|
} catch (err) {
|
|
299
283
|
logger.error('hook.end', {
|
|
300
284
|
hook: event,
|
|
301
|
-
outcome: '
|
|
302
|
-
reason: '
|
|
285
|
+
outcome: 'record-failed',
|
|
286
|
+
reason: 'session-state-failed',
|
|
303
287
|
...failureData(err),
|
|
304
288
|
})
|
|
305
289
|
}
|
|
@@ -312,33 +296,6 @@ export async function hookRunCommand(
|
|
|
312
296
|
return EXIT.ok
|
|
313
297
|
}
|
|
314
298
|
|
|
315
|
-
// SessionStart is the preferred model-visible activation seam. A few
|
|
316
|
-
// managed hosts regenerate older hook definitions that retain
|
|
317
|
-
// UserPromptSubmit but omit newer lifecycle events. Claim the first prompt
|
|
318
|
-
// once per exact session as a compatibility fallback, before config/auth/
|
|
319
|
-
// device/network work, and stay silent when SessionStart already ran.
|
|
320
|
-
let promptActivationStdout: string | undefined
|
|
321
|
-
if (
|
|
322
|
-
event === 'user-prompt-submit' &&
|
|
323
|
-
(harness === 'claude-code' || harness === 'codex') &&
|
|
324
|
-
activationOwnerPid !== undefined &&
|
|
325
|
-
envelope.session_id !== undefined
|
|
326
|
-
) {
|
|
327
|
-
try {
|
|
328
|
-
if (claimPromptActivation(envelope.session_id, deps.env, now(), activationOwnerPid)) {
|
|
329
|
-
promptActivationStdout = sessionActivationOutput(harness, 'UserPromptSubmit')
|
|
330
|
-
if (promptActivationStdout !== undefined) deps.io.out(promptActivationStdout)
|
|
331
|
-
}
|
|
332
|
-
} catch (err) {
|
|
333
|
-
logger.error('hook.end', {
|
|
334
|
-
hook: event,
|
|
335
|
-
outcome: 'claim-failed',
|
|
336
|
-
reason: 'activation-state-failed',
|
|
337
|
-
...failureData(err),
|
|
338
|
-
})
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
299
|
// Cursor has a confirmed host bug in which sessionStart.additional_context
|
|
343
300
|
// is accepted but never reaches the model. A native Stop follow-up is the
|
|
344
301
|
// host's guaranteed model-visible channel. Claim it once per conversation,
|
|
@@ -417,7 +374,7 @@ export async function hookRunCommand(
|
|
|
417
374
|
outcome: claimed ? 'followup-added' : 'already-activated',
|
|
418
375
|
decided: claimed,
|
|
419
376
|
})
|
|
420
|
-
if (claimed) deps.io.out(cursorStopActivationOutput())
|
|
377
|
+
if (claimed) deps.io.out(cursorStopActivationOutput(cwd, deps.env))
|
|
421
378
|
return EXIT.ok
|
|
422
379
|
}
|
|
423
380
|
|
|
@@ -442,7 +399,6 @@ export async function hookRunCommand(
|
|
|
442
399
|
hook: event,
|
|
443
400
|
outcome: 'failed',
|
|
444
401
|
reason: 'config-failed',
|
|
445
|
-
...(promptActivationStdout === undefined ? {} : { activation: 'context-added' }),
|
|
446
402
|
...failureData(err),
|
|
447
403
|
})
|
|
448
404
|
for (const line of describeHookFailure(err)) deps.io.err(`notifai: ${line}`)
|
|
@@ -546,10 +502,7 @@ export async function hookRunCommand(
|
|
|
546
502
|
const notes = outcome.notes.filter((note) => !/^(?:late )?answer from /.test(note))
|
|
547
503
|
logger.info('hook.end', {
|
|
548
504
|
hook: event,
|
|
549
|
-
// Stop stdout takes over the turn. UserPromptSubmit stdout may instead
|
|
550
|
-
// add context to the user's own turn, which is explicitly non-decisive.
|
|
551
505
|
decided: outcome.decided ?? outcome.stdout !== undefined,
|
|
552
|
-
...(promptActivationStdout === undefined ? {} : { activation: 'context-added' }),
|
|
553
506
|
...(notes.length === 0 ? {} : { notes }),
|
|
554
507
|
...outcome.log,
|
|
555
508
|
})
|
|
@@ -666,10 +619,52 @@ export interface AskFlags {
|
|
|
666
619
|
image?: string[]
|
|
667
620
|
imageAlt?: string[]
|
|
668
621
|
project?: string
|
|
669
|
-
sessionId?: string
|
|
670
622
|
sessionLabel?: string
|
|
671
623
|
}
|
|
672
624
|
|
|
625
|
+
export interface AskFailure {
|
|
626
|
+
ok: false
|
|
627
|
+
registered: false
|
|
628
|
+
code: string
|
|
629
|
+
check_id: string
|
|
630
|
+
exit_code: number
|
|
631
|
+
remedy: string
|
|
632
|
+
message: string
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/** One stable failure contract for every machine-readable ask refusal. */
|
|
636
|
+
export function reportAskFailure(
|
|
637
|
+
deps: CommandDeps,
|
|
638
|
+
flags: Pick<AskFlags, 'json'>,
|
|
639
|
+
failure: Omit<AskFailure, 'ok' | 'registered'>,
|
|
640
|
+
): number {
|
|
641
|
+
if (flags.json === true) {
|
|
642
|
+
deps.io.out(JSON.stringify({ ok: false, registered: false, ...failure }, null, 2))
|
|
643
|
+
} else {
|
|
644
|
+
deps.io.err(failure.message)
|
|
645
|
+
deps.io.err(`next: ${failure.remedy}`)
|
|
646
|
+
}
|
|
647
|
+
return failure.exit_code
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function askFailure(
|
|
651
|
+
deps: CommandDeps,
|
|
652
|
+
flags: AskFlags,
|
|
653
|
+
code: string,
|
|
654
|
+
checkId: string,
|
|
655
|
+
message: string,
|
|
656
|
+
remedy: string,
|
|
657
|
+
exitCode: number = EXIT.usage,
|
|
658
|
+
): number {
|
|
659
|
+
return reportAskFailure(deps, flags, {
|
|
660
|
+
code,
|
|
661
|
+
check_id: checkId,
|
|
662
|
+
exit_code: exitCode,
|
|
663
|
+
remedy,
|
|
664
|
+
message,
|
|
665
|
+
})
|
|
666
|
+
}
|
|
667
|
+
|
|
673
668
|
/** The `--form` document: what an agent writes to ask several things at once. */
|
|
674
669
|
interface AskFormQuestion {
|
|
675
670
|
text: string
|
|
@@ -863,8 +858,15 @@ function recordRegisteredQuestion(
|
|
|
863
858
|
)
|
|
864
859
|
} catch (err) {
|
|
865
860
|
log(deps).error('ask.registered', { ok: false, session: sessionId, message: String(err) })
|
|
866
|
-
|
|
867
|
-
|
|
861
|
+
return askFailure(
|
|
862
|
+
deps,
|
|
863
|
+
{ json },
|
|
864
|
+
'registration_failed',
|
|
865
|
+
'question_registration',
|
|
866
|
+
`Could not register the question: ${err instanceof Error ? err.message : String(err)}`,
|
|
867
|
+
'retire an older pending question if the limit was reached, then retry the same ask',
|
|
868
|
+
EXIT.failed,
|
|
869
|
+
)
|
|
868
870
|
}
|
|
869
871
|
log(deps).info('ask.registered', {
|
|
870
872
|
ok: true,
|
|
@@ -962,7 +964,17 @@ async function uploadAskMedia(
|
|
|
962
964
|
invocation: DraftInvocation,
|
|
963
965
|
): Promise<number> {
|
|
964
966
|
const authed = authedClient(deps, config)
|
|
965
|
-
if (!authed)
|
|
967
|
+
if (!authed) {
|
|
968
|
+
return askFailure(
|
|
969
|
+
deps,
|
|
970
|
+
flags,
|
|
971
|
+
'auth_required',
|
|
972
|
+
'credential',
|
|
973
|
+
'Question routing is not paired on this machine.',
|
|
974
|
+
'run `notifai init --json`',
|
|
975
|
+
EXIT.auth,
|
|
976
|
+
)
|
|
977
|
+
}
|
|
966
978
|
const mediaIds: string[] = []
|
|
967
979
|
for (const image of flags.image ?? []) {
|
|
968
980
|
if (image.startsWith('med_')) {
|
|
@@ -972,14 +984,21 @@ async function uploadAskMedia(
|
|
|
972
984
|
const uploaded = await uploadImage(deps, authed.client, image, config.media_origins.value)
|
|
973
985
|
if (!uploaded.ok) {
|
|
974
986
|
if (uploaded.error !== null) deps.io.err(uploaded.error)
|
|
975
|
-
return
|
|
987
|
+
return askFailure(
|
|
988
|
+
deps,
|
|
989
|
+
flags,
|
|
990
|
+
'media_upload_failed',
|
|
991
|
+
'media',
|
|
992
|
+
uploaded.error ?? 'The image upload failed.',
|
|
993
|
+
'fix the reported image or network problem, then retry the same ask',
|
|
994
|
+
uploaded.exit,
|
|
995
|
+
)
|
|
976
996
|
}
|
|
977
997
|
mediaIds.push(uploaded.mediaId)
|
|
978
998
|
}
|
|
979
999
|
const ready = buildAskDraft(config, built, flags, invocation, mediaIds)
|
|
980
1000
|
if (!ready.ok) {
|
|
981
|
-
deps
|
|
982
|
-
return EXIT.usage
|
|
1001
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', ready.error, 'fix the reported field and retry')
|
|
983
1002
|
}
|
|
984
1003
|
return recordRegisteredQuestion(deps, sessionId, built, ready.draft, flags.json === true)
|
|
985
1004
|
}
|
|
@@ -999,31 +1018,26 @@ export function askCommand(
|
|
|
999
1018
|
// happens to exist on this machine.
|
|
1000
1019
|
const escapedBody = rejectAccidentalEscapedNewlines(flags.body, flags.literalBackslashN)
|
|
1001
1020
|
if (escapedBody !== null) {
|
|
1002
|
-
deps
|
|
1003
|
-
return EXIT.usage
|
|
1021
|
+
return askFailure(deps, flags, 'invalid_input', 'body', escapedBody, 'fix the body and retry')
|
|
1004
1022
|
}
|
|
1005
1023
|
const built = buildQuestions(flags, question)
|
|
1006
1024
|
if (!built.ok) {
|
|
1007
|
-
deps
|
|
1008
|
-
return EXIT.usage
|
|
1025
|
+
return askFailure(deps, flags, 'invalid_input', 'questions', built.error, 'fix the question input and retry')
|
|
1009
1026
|
}
|
|
1010
1027
|
const mediaInputError = validateMediaInputs(flags.image, flags.imageAlt)
|
|
1011
1028
|
if (mediaInputError !== null) {
|
|
1012
|
-
deps
|
|
1013
|
-
return EXIT.usage
|
|
1014
|
-
}
|
|
1015
|
-
const routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env })
|
|
1016
|
-
if (!routingConfig.ask_notifications.value) {
|
|
1017
|
-
deps.io.err(
|
|
1018
|
-
'Question routing is disabled by ask_notifications=false; enable it or use a blocking `notifai send --reply` question.',
|
|
1019
|
-
)
|
|
1020
|
-
return EXIT.usage
|
|
1029
|
+
return askFailure(deps, flags, 'invalid_input', 'media', mediaInputError, 'fix the media flags and retry')
|
|
1021
1030
|
}
|
|
1022
1031
|
if (deps.store.load() === null) {
|
|
1023
|
-
|
|
1024
|
-
|
|
1032
|
+
return askFailure(
|
|
1033
|
+
deps,
|
|
1034
|
+
flags,
|
|
1035
|
+
'auth_required',
|
|
1036
|
+
'credential',
|
|
1037
|
+
'Question routing is not paired on this machine.',
|
|
1038
|
+
'run `notifai init --json`',
|
|
1039
|
+
EXIT.auth,
|
|
1025
1040
|
)
|
|
1026
|
-
return EXIT.usage
|
|
1027
1041
|
}
|
|
1028
1042
|
// An agent calling this gets no hook payload. Harness-native environment
|
|
1029
1043
|
// markers identify the active owner, while UserPromptSubmit adds the hook's
|
|
@@ -1032,78 +1046,127 @@ export function askCommand(
|
|
|
1032
1046
|
const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, now)
|
|
1033
1047
|
let sessionId: string | undefined
|
|
1034
1048
|
if (active !== null) {
|
|
1035
|
-
|
|
1049
|
+
if (contested.length > 1) {
|
|
1050
|
+
return askFailure(
|
|
1051
|
+
deps,
|
|
1052
|
+
flags,
|
|
1053
|
+
'session_identity_ambiguous',
|
|
1054
|
+
'exact_session',
|
|
1055
|
+
`Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}).`,
|
|
1056
|
+
'run the ask from a shell with one exact active harness session',
|
|
1057
|
+
)
|
|
1058
|
+
}
|
|
1059
|
+
const exactState = active.sessionId === undefined
|
|
1060
|
+
? null
|
|
1061
|
+
: readSessionState(active.sessionId, deps.env)
|
|
1062
|
+
const installationCwd = exactState?.activation_cwd ?? deps.cwd
|
|
1063
|
+
const installationDeps = installationCwd === deps.cwd ? deps : { ...deps, cwd: installationCwd }
|
|
1064
|
+
const installations = findInstallations(installationCwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
1036
1065
|
const activeInstalled = installations.some(
|
|
1037
1066
|
(installation) => installation.harness === active.harness,
|
|
1038
1067
|
)
|
|
1039
1068
|
if (!activeInstalled) {
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
return EXIT.usage
|
|
1069
|
+
return askFailure(
|
|
1070
|
+
deps,
|
|
1071
|
+
flags,
|
|
1072
|
+
'hooks_not_installed',
|
|
1073
|
+
'hook_installation',
|
|
1074
|
+
`Notifai ${active.label} hooks are not installed for this project.`,
|
|
1075
|
+
`run \`notifai init --json\` from this ${active.label} session`,
|
|
1076
|
+
)
|
|
1049
1077
|
}
|
|
1050
1078
|
if (active.sessionId === undefined) {
|
|
1051
|
-
|
|
1052
|
-
deps
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
now,
|
|
1060
|
-
active.sessionId,
|
|
1061
|
-
active.harness,
|
|
1062
|
-
)
|
|
1063
|
-
if (projectPointer === null) {
|
|
1064
|
-
for (const line of diagnoseActiveHarnessSession(
|
|
1065
|
-
active,
|
|
1066
|
-
'not-fired',
|
|
1067
|
-
installations,
|
|
1068
|
-
contested,
|
|
1069
|
-
)) {
|
|
1070
|
-
deps.io.err(line)
|
|
1071
|
-
}
|
|
1072
|
-
return EXIT.usage
|
|
1079
|
+
return askFailure(
|
|
1080
|
+
deps,
|
|
1081
|
+
flags,
|
|
1082
|
+
'session_identity_missing',
|
|
1083
|
+
'exact_session',
|
|
1084
|
+
`The active ${active.label} shell does not expose an exact session id.`,
|
|
1085
|
+
'use a blocking `notifai send --reply` question',
|
|
1086
|
+
)
|
|
1073
1087
|
}
|
|
1074
|
-
const routeProblems = activeQuestionRouteProblems(
|
|
1088
|
+
const routeProblems = activeQuestionRouteProblems(installationDeps, active, installations)
|
|
1075
1089
|
if (routeProblems.length > 0) {
|
|
1076
|
-
|
|
1077
|
-
|
|
1090
|
+
return askFailure(
|
|
1091
|
+
deps,
|
|
1092
|
+
flags,
|
|
1093
|
+
'question_routing_unavailable',
|
|
1094
|
+
'hook_contract',
|
|
1095
|
+
`Question routing is not ready: ${routeProblems.join('; ')}`,
|
|
1096
|
+
'run `notifai init --json` and follow its question-routing remedy',
|
|
1097
|
+
)
|
|
1078
1098
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1099
|
+
const state = exactState ?? readSessionState(active.sessionId, deps.env)
|
|
1100
|
+
if (state.harness !== active.harness || state.last_prompt_at === undefined) {
|
|
1101
|
+
return askFailure(
|
|
1102
|
+
deps,
|
|
1103
|
+
flags,
|
|
1104
|
+
'session_not_activated',
|
|
1105
|
+
'user_prompt_submit',
|
|
1106
|
+
`This exact ${active.label} session has not fired UserPromptSubmit.`,
|
|
1107
|
+
`send one prompt in this ${active.label} session, then retry \`notifai ask --json\``,
|
|
1108
|
+
)
|
|
1109
|
+
}
|
|
1110
|
+
if (state.last_stop_at === undefined) {
|
|
1111
|
+
return askFailure(
|
|
1112
|
+
deps,
|
|
1113
|
+
flags,
|
|
1114
|
+
'stop_not_observed',
|
|
1115
|
+
'stop_hook',
|
|
1116
|
+
`This exact ${active.label} session has fired UserPromptSubmit, but its Stop hook has not been observed.`,
|
|
1117
|
+
`end one harmless turn, send a new prompt, then retry \`notifai ask --json\``,
|
|
1082
1118
|
)
|
|
1083
|
-
return EXIT.usage
|
|
1084
1119
|
}
|
|
1085
|
-
sessionId =
|
|
1120
|
+
sessionId = active.sessionId
|
|
1086
1121
|
} else {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1122
|
+
return askFailure(
|
|
1123
|
+
deps,
|
|
1124
|
+
flags,
|
|
1125
|
+
'session_identity_missing',
|
|
1126
|
+
'exact_session',
|
|
1127
|
+
'Could not prove which live harness session owns this command.',
|
|
1128
|
+
'run it from a supported harness with exact session identity, or use a blocking `notifai send --reply` question',
|
|
1090
1129
|
)
|
|
1091
|
-
return EXIT.usage
|
|
1092
1130
|
}
|
|
1093
1131
|
if (!sessionId) {
|
|
1094
|
-
|
|
1095
|
-
|
|
1132
|
+
return askFailure(deps, flags, 'session_identity_missing', 'exact_session', 'No exact active session is available.', 'use a blocking `notifai send --reply` question')
|
|
1133
|
+
}
|
|
1134
|
+
let routingConfig: CliConfig
|
|
1135
|
+
try {
|
|
1136
|
+
routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env, sessionId })
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
return askFailure(
|
|
1139
|
+
deps,
|
|
1140
|
+
flags,
|
|
1141
|
+
'config_invalid',
|
|
1142
|
+
'configuration',
|
|
1143
|
+
`Question routing configuration is invalid: ${String(err)}`,
|
|
1144
|
+
'fix the reported Notifai configuration and retry',
|
|
1145
|
+
)
|
|
1146
|
+
}
|
|
1147
|
+
if (!routingConfig.ask_notifications.value) {
|
|
1148
|
+
return askFailure(
|
|
1149
|
+
deps,
|
|
1150
|
+
flags,
|
|
1151
|
+
'question_routing_disabled',
|
|
1152
|
+
'ask_notifications',
|
|
1153
|
+
'Question routing is disabled by ask_notifications=false.',
|
|
1154
|
+
'enable question routing or use a blocking `notifai send --reply` question',
|
|
1155
|
+
)
|
|
1096
1156
|
}
|
|
1097
1157
|
const source = resolveDraftInvocation(deps, flags, active)
|
|
1098
1158
|
if (!source.ok) {
|
|
1099
|
-
deps
|
|
1100
|
-
return EXIT.usage
|
|
1159
|
+
return askFailure(deps, flags, 'invalid_source', 'source_context', source.error, 'remove the conflicting source override and retry')
|
|
1101
1160
|
}
|
|
1102
1161
|
if (source.invocation.source?.session_id !== sessionId) {
|
|
1103
|
-
|
|
1104
|
-
|
|
1162
|
+
return askFailure(
|
|
1163
|
+
deps,
|
|
1164
|
+
flags,
|
|
1165
|
+
'session_identity_mismatch',
|
|
1166
|
+
'exact_session',
|
|
1167
|
+
`The inferred session does not match the exact active ${active.label} session.`,
|
|
1168
|
+
'remove NOTIFAI_SESSION_ID and retry from the exact active session',
|
|
1105
1169
|
)
|
|
1106
|
-
return EXIT.usage
|
|
1107
1170
|
}
|
|
1108
1171
|
|
|
1109
1172
|
// Placeholders let every body, source, project, media, and payload limit fail
|
|
@@ -1111,8 +1174,7 @@ export function askCommand(
|
|
|
1111
1174
|
const placeholders = (flags.image ?? []).map((_, index) => `med_pending_${index + 1}`)
|
|
1112
1175
|
const preflight = buildAskDraft(routingConfig, built, flags, source.invocation, placeholders)
|
|
1113
1176
|
if (!preflight.ok) {
|
|
1114
|
-
deps
|
|
1115
|
-
return EXIT.usage
|
|
1177
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', preflight.error, 'fix the reported field and retry')
|
|
1116
1178
|
}
|
|
1117
1179
|
if ((flags.image?.length ?? 0) > 0) {
|
|
1118
1180
|
return uploadAskMedia(
|
|
@@ -1178,79 +1240,6 @@ export function activeQuestionRouteProblems(
|
|
|
1178
1240
|
return [...new Set(problems)]
|
|
1179
1241
|
}
|
|
1180
1242
|
|
|
1181
|
-
type ActiveHarnessProblem = 'not-installed' | 'not-fired' | 'different-session'
|
|
1182
|
-
|
|
1183
|
-
function diagnoseActiveHarnessSession(
|
|
1184
|
-
active: ActiveHarnessSession,
|
|
1185
|
-
problem: ActiveHarnessProblem,
|
|
1186
|
-
installations: Installation[],
|
|
1187
|
-
contested: ActiveHarnessSession[] = [],
|
|
1188
|
-
): string[] {
|
|
1189
|
-
// Naming one harness confidently is wrong when the environment carries the
|
|
1190
|
-
// markers of several and none of them has fired here: whichever one the
|
|
1191
|
-
// agent is told to prompt may not be the one it is running in.
|
|
1192
|
-
const ambiguity =
|
|
1193
|
-
contested.length > 1
|
|
1194
|
-
? [
|
|
1195
|
-
`Several harness markers are present here (${contested.map((candidate) => candidate.label).join(', ')}) and none names a session that has fired in this directory, so ${active.label} is a guess from the environment. Send the prompt in the harness you are actually running.`,
|
|
1196
|
-
]
|
|
1197
|
-
: []
|
|
1198
|
-
if (problem === 'not-installed') {
|
|
1199
|
-
const others = installations.map((installation) => installation.harness)
|
|
1200
|
-
return [
|
|
1201
|
-
`Could not register the question for the active ${active.label} session: Notifai ${active.label} hooks are not installed for this project.`,
|
|
1202
|
-
...ambiguity,
|
|
1203
|
-
...(others.length === 0
|
|
1204
|
-
? []
|
|
1205
|
-
: [
|
|
1206
|
-
`Hooks installed for ${[...new Set(others)].join(', ')} do not route an active ${active.label} session.`,
|
|
1207
|
-
]),
|
|
1208
|
-
`Run \`notifai hooks install --harness ${active.harness}\`, then send one ${active.label} prompt and run \`notifai doctor\`.`,
|
|
1209
|
-
`Retry \`notifai ask\` only after doctor reports that the ${active.label} hooks fired.`,
|
|
1210
|
-
]
|
|
1211
|
-
}
|
|
1212
|
-
if (problem === 'different-session') {
|
|
1213
|
-
return [
|
|
1214
|
-
`Could not register the question for the active ${active.label} session: the project pointer belongs to another ${active.label} session or harness.`,
|
|
1215
|
-
...ambiguity,
|
|
1216
|
-
`Refusing to guess or cross-wire the question. Send one prompt in this ${active.label} session, then run \`notifai doctor\`.`,
|
|
1217
|
-
`Retry \`notifai ask\` only after doctor reports that this active ${active.label} session fired the hooks.`,
|
|
1218
|
-
]
|
|
1219
|
-
}
|
|
1220
|
-
return [
|
|
1221
|
-
`Could not register the question for the active ${active.label} session: Notifai hooks are installed, but this session has not published its pointer.`,
|
|
1222
|
-
...ambiguity,
|
|
1223
|
-
`Send one ${active.label} prompt, then run \`notifai doctor\`.`,
|
|
1224
|
-
`Retry \`notifai ask\` only after doctor reports that the active ${active.label} session fired the hooks.`,
|
|
1225
|
-
]
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
/**
|
|
1229
|
-
* Why `ask` cannot see a session, in terms of what to do about it.
|
|
1230
|
-
*
|
|
1231
|
-
* Only a UserPromptSubmit hook firing produces the pointer this reads, and the
|
|
1232
|
-
* old message answered every cause with "run `notifai hooks install` and send
|
|
1233
|
-
* one prompt". The useful next action depends on the harness: some reload
|
|
1234
|
-
* project hook files, OpenCode loads its plugin at startup, and Codex should be
|
|
1235
|
-
* checked after a prompt before assuming that a new session is required.
|
|
1236
|
-
*/
|
|
1237
|
-
function diagnoseMissingSession(deps: CommandDeps): string[] {
|
|
1238
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform)
|
|
1239
|
-
if (installations.length === 0) {
|
|
1240
|
-
return [
|
|
1241
|
-
'Could not tell which harness session this is: no Notifai hooks are installed for this project.',
|
|
1242
|
-
'Run `notifai hooks install`, then follow the activation instruction it prints.',
|
|
1243
|
-
]
|
|
1244
|
-
}
|
|
1245
|
-
const where = installations.map((i) => `${i.harness} in ${i.file}`).join(', ')
|
|
1246
|
-
return [
|
|
1247
|
-
`Could not tell which harness session this is. Notifai hooks are installed (${where}),`,
|
|
1248
|
-
'but no usable session pointer from the last 24 hours exists here.',
|
|
1249
|
-
hookActivationAdvice(installations),
|
|
1250
|
-
'Do not bypass this with a guessed session id; run from the exact active harness session or use a blocking `notifai send --reply` question.',
|
|
1251
|
-
]
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
1243
|
/** The least disruptive verified way to make each installed adapter run once. */
|
|
1255
1244
|
export function hookActivationAdvice(installations: Installation[]): string {
|
|
1256
1245
|
const harnesses = new Set(installations.map((installation) => installation.harness))
|
|
@@ -1324,7 +1313,12 @@ export async function acknowledgeCommand(
|
|
|
1324
1313
|
}
|
|
1325
1314
|
|
|
1326
1315
|
const logger = log(deps)
|
|
1327
|
-
const
|
|
1316
|
+
const lifecycleSession = resolveCommandSession(deps, requestId)
|
|
1317
|
+
const config = loadLoggedConfig(deps, {
|
|
1318
|
+
cwd: deps.cwd,
|
|
1319
|
+
env: deps.env,
|
|
1320
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
1321
|
+
})
|
|
1328
1322
|
logger.info('acknowledgement.attempted', {
|
|
1329
1323
|
request_id: requestId,
|
|
1330
1324
|
characters: text.length,
|
|
@@ -1349,9 +1343,8 @@ export async function acknowledgeCommand(
|
|
|
1349
1343
|
created_at: result.agent_acknowledgement.created_at,
|
|
1350
1344
|
agent_acknowledgement_required: true,
|
|
1351
1345
|
})
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
clearAcknowledgementObligation(sessionId, deps.env, requestId)
|
|
1346
|
+
if (lifecycleSession !== null) {
|
|
1347
|
+
clearAcknowledgementObligation(lifecycleSession.sessionId, deps.env, requestId)
|
|
1355
1348
|
}
|
|
1356
1349
|
if (flags.json) deps.io.out(JSON.stringify(output, null, 2))
|
|
1357
1350
|
else {
|
|
@@ -1387,7 +1380,12 @@ export async function closeCommand(
|
|
|
1387
1380
|
const local = await closeLocalQuestion(deps, requestId!, flags.json === true)
|
|
1388
1381
|
if (local !== null) return local
|
|
1389
1382
|
|
|
1390
|
-
const
|
|
1383
|
+
const lifecycleSession = resolveCommandSession(deps, requestId!)
|
|
1384
|
+
const config = loadLoggedConfig(deps, {
|
|
1385
|
+
cwd: deps.cwd,
|
|
1386
|
+
env: deps.env,
|
|
1387
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
1388
|
+
})
|
|
1391
1389
|
const authed = authedClient(deps, config)
|
|
1392
1390
|
if (!authed) return EXIT.auth
|
|
1393
1391
|
try {
|
|
@@ -1426,12 +1424,12 @@ export async function closeCommand(
|
|
|
1426
1424
|
* them is the whole retirement. Live ones still need their reply window closed.
|
|
1427
1425
|
*/
|
|
1428
1426
|
async function closePendingQuestions(deps: CommandDeps, json: boolean): Promise<number> {
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
if (sessionId === null) {
|
|
1427
|
+
const lifecycleSession = resolveCommandSession(deps)
|
|
1428
|
+
if (lifecycleSession === null) {
|
|
1432
1429
|
deps.io.err('No active session pointer is available in this directory.')
|
|
1433
1430
|
return EXIT.noReply
|
|
1434
1431
|
}
|
|
1432
|
+
const sessionId = lifecycleSession.sessionId
|
|
1435
1433
|
const pending = readSessionState(sessionId, deps.env).pending ?? []
|
|
1436
1434
|
if (pending.length === 0) {
|
|
1437
1435
|
if (json) {
|
|
@@ -1452,7 +1450,7 @@ async function closePendingQuestions(deps: CommandDeps, json: boolean): Promise<
|
|
|
1452
1450
|
|
|
1453
1451
|
const closed: string[] = []
|
|
1454
1452
|
if (live.length > 0) {
|
|
1455
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
1453
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId })
|
|
1456
1454
|
const authed = authedClient(deps, config)
|
|
1457
1455
|
if (!authed) return EXIT.auth
|
|
1458
1456
|
for (const entry of live) {
|
|
@@ -1502,9 +1500,9 @@ async function closeLocalQuestion(
|
|
|
1502
1500
|
id: string,
|
|
1503
1501
|
json: boolean,
|
|
1504
1502
|
): Promise<number | null> {
|
|
1505
|
-
const
|
|
1506
|
-
|
|
1507
|
-
|
|
1503
|
+
const lifecycleSession = resolveCommandSession(deps, id)
|
|
1504
|
+
if (lifecycleSession === null) return null
|
|
1505
|
+
const sessionId = lifecycleSession.sessionId
|
|
1508
1506
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find(
|
|
1509
1507
|
(candidate) => candidate.question_id === id || candidate.request_id === id,
|
|
1510
1508
|
)
|
|
@@ -1531,7 +1529,7 @@ async function closeLocalQuestion(
|
|
|
1531
1529
|
return EXIT.ok
|
|
1532
1530
|
}
|
|
1533
1531
|
|
|
1534
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env })
|
|
1532
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId })
|
|
1535
1533
|
const authed = authedClient(deps, config)
|
|
1536
1534
|
if (!authed) return EXIT.auth
|
|
1537
1535
|
try {
|
|
@@ -1567,8 +1565,9 @@ async function closeLocalQuestion(
|
|
|
1567
1565
|
}
|
|
1568
1566
|
|
|
1569
1567
|
function forgetClosedQuestion(deps: CommandDeps, requestId: string): void {
|
|
1570
|
-
const
|
|
1571
|
-
if (
|
|
1568
|
+
const lifecycleSession = resolveCommandSession(deps, requestId)
|
|
1569
|
+
if (lifecycleSession === null) return
|
|
1570
|
+
const sessionId = lifecycleSession.sessionId
|
|
1572
1571
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find(
|
|
1573
1572
|
(candidate) => candidate.request_id === requestId,
|
|
1574
1573
|
)
|