@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/dist/commands-hooks.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadConfig } from './config.js';
|
|
|
9
9
|
import { withTargetFileLock } from './file-lock.js';
|
|
10
10
|
import { HARNESS_CAPABILITIES, HARNESS_LABELS } from './harnesses.js';
|
|
11
11
|
import { inspectHookAdapter, installHookAdapter, isNpxAdapterTarget, } from './hook-adapter.js';
|
|
12
|
-
import { clearAcknowledgementObligation,
|
|
12
|
+
import { clearAcknowledgementObligation, claimCursorStopActivation, confirmCursorStopActivation, dropPendingQuestion, handleSessionEnd, handleStop, handleUserPromptSubmit, recordSessionStart, parkForRetirement, parseHookInput, pruneAbandonedSessions, readSessionState, resetCursorStopActivation, registerQuestion, waiterCeilingSeconds, withdrawUnpushedQuestions, } from './hooks.js';
|
|
13
13
|
import { BLOCKING_STOP_TIMEOUT_SECONDS, HARNESSES, applyPlan, buildCursorHookConfig, buildHookConfig, cleanupEmptiedCodexLayer, codexCoexistenceNotes, codexHomeNote, codexLayerDir, codexLayerPaths, codexRepresentationProblems, codexTrustProblems, detectHarness, detectedHarnesses, findInstallations, handlerEvent, hookDefinitionFiles, loadCursorSettings, loadSettings, mergeCursorHooks, mergeHooks, removeCursorHooks, removeHooks, settingsFile, stopHandlerIsDetached, withCodexLayerTransaction, } from './install-hooks.js';
|
|
14
14
|
import { logConfigResolved, logSettingsFrom } from './logging.js';
|
|
15
15
|
import { isOurOpencodePlugin, opencodePluginSource } from './opencode-plugin.js';
|
|
@@ -21,6 +21,7 @@ import { EXIT, authedClient, diagnoseIgnoredOriginOverride, loadLoggedConfig, lo
|
|
|
21
21
|
import { claudeSessionPid, resolveActiveHarness, } from './commands-harness-context.js';
|
|
22
22
|
import { stopShapeProblems } from './commands-hook-shape.js';
|
|
23
23
|
import { acknowledgementCommand, printAcknowledgementStatus, resolveDraftInvocation, uploadImage, waitForReply, } from './commands-send-support.js';
|
|
24
|
+
import { resolveCommandSession } from './command-session.js';
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// hook / ask / close — harness integration
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
@@ -136,12 +137,6 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
136
137
|
const cwd = envelope.cwd ?? deps.cwd;
|
|
137
138
|
const sessionEnd = hookDefersDiagnosticsUntilAfterCleanup(event);
|
|
138
139
|
logger.bind({ session: envelope.session_id ?? null });
|
|
139
|
-
const declaredSourcePid = declaredHookSourcePid(deps);
|
|
140
|
-
const activationOwnerPid = harness === 'claude-code'
|
|
141
|
-
? (deps.claudeSourcePid ?? declaredSourcePid ?? claudeSessionPid(deps.env))
|
|
142
|
-
: harness === 'codex'
|
|
143
|
-
? (deps.codexSourcePid ?? declaredSourcePid ?? process.ppid)
|
|
144
|
-
: undefined;
|
|
145
140
|
// Session activation is local model context, not authenticated routing. It
|
|
146
141
|
// must survive the exact first-run states it exists to repair: no config,
|
|
147
142
|
// no credential, no Companion device, and no network.
|
|
@@ -159,24 +154,18 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
159
154
|
});
|
|
160
155
|
}
|
|
161
156
|
}
|
|
162
|
-
const stdout = sessionActivationOutput(harness, event === 'session-start' ? 'SessionStart' : 'SubagentStart');
|
|
157
|
+
const stdout = sessionActivationOutput(harness, event === 'session-start' ? 'SessionStart' : 'SubagentStart', cwd, deps.env);
|
|
163
158
|
if (stdout !== undefined)
|
|
164
159
|
deps.io.out(stdout);
|
|
165
|
-
if (
|
|
166
|
-
event === 'session-start' &&
|
|
167
|
-
activationOwnerPid !== undefined &&
|
|
168
|
-
envelope.session_id !== undefined) {
|
|
160
|
+
if (event === 'session-start' && envelope.session_id !== undefined) {
|
|
169
161
|
try {
|
|
170
|
-
|
|
171
|
-
// later prompt may duplicate context, but it cannot suppress context
|
|
172
|
-
// that the host never received.
|
|
173
|
-
markSessionActivation(envelope.session_id, deps.env, now(), activationOwnerPid);
|
|
162
|
+
recordSessionStart(envelope.session_id, deps.env, harness, cwd);
|
|
174
163
|
}
|
|
175
164
|
catch (err) {
|
|
176
165
|
logger.error('hook.end', {
|
|
177
166
|
hook: event,
|
|
178
|
-
outcome: '
|
|
179
|
-
reason: '
|
|
167
|
+
outcome: 'record-failed',
|
|
168
|
+
reason: 'session-state-failed',
|
|
180
169
|
...failureData(err),
|
|
181
170
|
});
|
|
182
171
|
}
|
|
@@ -188,32 +177,6 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
188
177
|
});
|
|
189
178
|
return EXIT.ok;
|
|
190
179
|
}
|
|
191
|
-
// SessionStart is the preferred model-visible activation seam. A few
|
|
192
|
-
// managed hosts regenerate older hook definitions that retain
|
|
193
|
-
// UserPromptSubmit but omit newer lifecycle events. Claim the first prompt
|
|
194
|
-
// once per exact session as a compatibility fallback, before config/auth/
|
|
195
|
-
// device/network work, and stay silent when SessionStart already ran.
|
|
196
|
-
let promptActivationStdout;
|
|
197
|
-
if (event === 'user-prompt-submit' &&
|
|
198
|
-
(harness === 'claude-code' || harness === 'codex') &&
|
|
199
|
-
activationOwnerPid !== undefined &&
|
|
200
|
-
envelope.session_id !== undefined) {
|
|
201
|
-
try {
|
|
202
|
-
if (claimPromptActivation(envelope.session_id, deps.env, now(), activationOwnerPid)) {
|
|
203
|
-
promptActivationStdout = sessionActivationOutput(harness, 'UserPromptSubmit');
|
|
204
|
-
if (promptActivationStdout !== undefined)
|
|
205
|
-
deps.io.out(promptActivationStdout);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
catch (err) {
|
|
209
|
-
logger.error('hook.end', {
|
|
210
|
-
hook: event,
|
|
211
|
-
outcome: 'claim-failed',
|
|
212
|
-
reason: 'activation-state-failed',
|
|
213
|
-
...failureData(err),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
180
|
// Cursor has a confirmed host bug in which sessionStart.additional_context
|
|
218
181
|
// is accepted but never reaches the model. A native Stop follow-up is the
|
|
219
182
|
// host's guaranteed model-visible channel. Claim it once per conversation,
|
|
@@ -291,7 +254,7 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
291
254
|
decided: claimed,
|
|
292
255
|
});
|
|
293
256
|
if (claimed)
|
|
294
|
-
deps.io.out(cursorStopActivationOutput());
|
|
257
|
+
deps.io.out(cursorStopActivationOutput(cwd, deps.env));
|
|
295
258
|
return EXIT.ok;
|
|
296
259
|
}
|
|
297
260
|
let config = null;
|
|
@@ -316,7 +279,6 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
316
279
|
hook: event,
|
|
317
280
|
outcome: 'failed',
|
|
318
281
|
reason: 'config-failed',
|
|
319
|
-
...(promptActivationStdout === undefined ? {} : { activation: 'context-added' }),
|
|
320
282
|
...failureData(err),
|
|
321
283
|
});
|
|
322
284
|
for (const line of describeHookFailure(err))
|
|
@@ -410,10 +372,7 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
410
372
|
const notes = outcome.notes.filter((note) => !/^(?:late )?answer from /.test(note));
|
|
411
373
|
logger.info('hook.end', {
|
|
412
374
|
hook: event,
|
|
413
|
-
// Stop stdout takes over the turn. UserPromptSubmit stdout may instead
|
|
414
|
-
// add context to the user's own turn, which is explicitly non-decisive.
|
|
415
375
|
decided: outcome.decided ?? outcome.stdout !== undefined,
|
|
416
|
-
...(promptActivationStdout === undefined ? {} : { activation: 'context-added' }),
|
|
417
376
|
...(notes.length === 0 ? {} : { notes }),
|
|
418
377
|
...outcome.log,
|
|
419
378
|
});
|
|
@@ -510,6 +469,26 @@ export function describeHookFailure(err) {
|
|
|
510
469
|
}
|
|
511
470
|
return lines;
|
|
512
471
|
}
|
|
472
|
+
/** One stable failure contract for every machine-readable ask refusal. */
|
|
473
|
+
export function reportAskFailure(deps, flags, failure) {
|
|
474
|
+
if (flags.json === true) {
|
|
475
|
+
deps.io.out(JSON.stringify({ ok: false, registered: false, ...failure }, null, 2));
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
deps.io.err(failure.message);
|
|
479
|
+
deps.io.err(`next: ${failure.remedy}`);
|
|
480
|
+
}
|
|
481
|
+
return failure.exit_code;
|
|
482
|
+
}
|
|
483
|
+
function askFailure(deps, flags, code, checkId, message, remedy, exitCode = EXIT.usage) {
|
|
484
|
+
return reportAskFailure(deps, flags, {
|
|
485
|
+
code,
|
|
486
|
+
check_id: checkId,
|
|
487
|
+
exit_code: exitCode,
|
|
488
|
+
remedy,
|
|
489
|
+
message,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
513
492
|
/**
|
|
514
493
|
* Turn ask input into questions plus their canonical body. Everything is
|
|
515
494
|
* validated at registration because a later hook failure is easy to miss.
|
|
@@ -657,8 +636,7 @@ function recordRegisteredQuestion(deps, sessionId, built, draft, json = false) {
|
|
|
657
636
|
}
|
|
658
637
|
catch (err) {
|
|
659
638
|
log(deps).error('ask.registered', { ok: false, session: sessionId, message: String(err) });
|
|
660
|
-
deps
|
|
661
|
-
return EXIT.failed;
|
|
639
|
+
return askFailure(deps, { json }, 'registration_failed', 'question_registration', `Could not register the question: ${err instanceof Error ? err.message : String(err)}`, 'retire an older pending question if the limit was reached, then retry the same ask', EXIT.failed);
|
|
662
640
|
}
|
|
663
641
|
log(deps).info('ask.registered', {
|
|
664
642
|
ok: true,
|
|
@@ -727,8 +705,9 @@ function recordRegisteredQuestion(deps, sessionId, built, draft, json = false) {
|
|
|
727
705
|
}
|
|
728
706
|
async function uploadAskMedia(deps, config, sessionId, built, flags, invocation) {
|
|
729
707
|
const authed = authedClient(deps, config);
|
|
730
|
-
if (!authed)
|
|
731
|
-
return EXIT.auth;
|
|
708
|
+
if (!authed) {
|
|
709
|
+
return askFailure(deps, flags, 'auth_required', 'credential', 'Question routing is not paired on this machine.', 'run `notifai init --json`', EXIT.auth);
|
|
710
|
+
}
|
|
732
711
|
const mediaIds = [];
|
|
733
712
|
for (const image of flags.image ?? []) {
|
|
734
713
|
if (image.startsWith('med_')) {
|
|
@@ -739,14 +718,13 @@ async function uploadAskMedia(deps, config, sessionId, built, flags, invocation)
|
|
|
739
718
|
if (!uploaded.ok) {
|
|
740
719
|
if (uploaded.error !== null)
|
|
741
720
|
deps.io.err(uploaded.error);
|
|
742
|
-
return uploaded.exit;
|
|
721
|
+
return askFailure(deps, flags, 'media_upload_failed', 'media', uploaded.error ?? 'The image upload failed.', 'fix the reported image or network problem, then retry the same ask', uploaded.exit);
|
|
743
722
|
}
|
|
744
723
|
mediaIds.push(uploaded.mediaId);
|
|
745
724
|
}
|
|
746
725
|
const ready = buildAskDraft(config, built, flags, invocation, mediaIds);
|
|
747
726
|
if (!ready.ok) {
|
|
748
|
-
deps
|
|
749
|
-
return EXIT.usage;
|
|
727
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', ready.error, 'fix the reported field and retry');
|
|
750
728
|
}
|
|
751
729
|
return recordRegisteredQuestion(deps, sessionId, built, ready.draft, flags.json === true);
|
|
752
730
|
}
|
|
@@ -761,27 +739,18 @@ export function askCommand(deps, question, flags) {
|
|
|
761
739
|
// happens to exist on this machine.
|
|
762
740
|
const escapedBody = rejectAccidentalEscapedNewlines(flags.body, flags.literalBackslashN);
|
|
763
741
|
if (escapedBody !== null) {
|
|
764
|
-
deps
|
|
765
|
-
return EXIT.usage;
|
|
742
|
+
return askFailure(deps, flags, 'invalid_input', 'body', escapedBody, 'fix the body and retry');
|
|
766
743
|
}
|
|
767
744
|
const built = buildQuestions(flags, question);
|
|
768
745
|
if (!built.ok) {
|
|
769
|
-
deps
|
|
770
|
-
return EXIT.usage;
|
|
746
|
+
return askFailure(deps, flags, 'invalid_input', 'questions', built.error, 'fix the question input and retry');
|
|
771
747
|
}
|
|
772
748
|
const mediaInputError = validateMediaInputs(flags.image, flags.imageAlt);
|
|
773
749
|
if (mediaInputError !== null) {
|
|
774
|
-
deps
|
|
775
|
-
return EXIT.usage;
|
|
776
|
-
}
|
|
777
|
-
const routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env });
|
|
778
|
-
if (!routingConfig.ask_notifications.value) {
|
|
779
|
-
deps.io.err('Question routing is disabled by ask_notifications=false; enable it or use a blocking `notifai send --reply` question.');
|
|
780
|
-
return EXIT.usage;
|
|
750
|
+
return askFailure(deps, flags, 'invalid_input', 'media', mediaInputError, 'fix the media flags and retry');
|
|
781
751
|
}
|
|
782
752
|
if (deps.store.load() === null) {
|
|
783
|
-
deps
|
|
784
|
-
return EXIT.usage;
|
|
753
|
+
return askFailure(deps, flags, 'auth_required', 'credential', 'Question routing is not paired on this machine.', 'run `notifai init --json`', EXIT.auth);
|
|
785
754
|
}
|
|
786
755
|
// An agent calling this gets no hook payload. Harness-native environment
|
|
787
756
|
// markers identify the active owner, while UserPromptSubmit adds the hook's
|
|
@@ -790,66 +759,64 @@ export function askCommand(deps, question, flags) {
|
|
|
790
759
|
const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, now);
|
|
791
760
|
let sessionId;
|
|
792
761
|
if (active !== null) {
|
|
793
|
-
|
|
762
|
+
if (contested.length > 1) {
|
|
763
|
+
return askFailure(deps, flags, 'session_identity_ambiguous', 'exact_session', `Several harness sessions could own this shell (${contested.map((candidate) => candidate.label).join(', ')}).`, 'run the ask from a shell with one exact active harness session');
|
|
764
|
+
}
|
|
765
|
+
const exactState = active.sessionId === undefined
|
|
766
|
+
? null
|
|
767
|
+
: readSessionState(active.sessionId, deps.env);
|
|
768
|
+
const installationCwd = exactState?.activation_cwd ?? deps.cwd;
|
|
769
|
+
const installationDeps = installationCwd === deps.cwd ? deps : { ...deps, cwd: installationCwd };
|
|
770
|
+
const installations = findInstallations(installationCwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
|
|
794
771
|
const activeInstalled = installations.some((installation) => installation.harness === active.harness);
|
|
795
772
|
if (!activeInstalled) {
|
|
796
|
-
|
|
797
|
-
deps.io.err(line);
|
|
798
|
-
}
|
|
799
|
-
return EXIT.usage;
|
|
773
|
+
return askFailure(deps, flags, 'hooks_not_installed', 'hook_installation', `Notifai ${active.label} hooks are not installed for this project.`, `run \`notifai init --json\` from this ${active.label} session`);
|
|
800
774
|
}
|
|
801
775
|
if (active.sessionId === undefined) {
|
|
802
|
-
|
|
803
|
-
deps.io.err(`Question routing is not ready: ${problem}`);
|
|
804
|
-
}
|
|
805
|
-
return EXIT.usage;
|
|
776
|
+
return askFailure(deps, flags, 'session_identity_missing', 'exact_session', `The active ${active.label} shell does not expose an exact session id.`, 'use a blocking `notifai send --reply` question');
|
|
806
777
|
}
|
|
807
|
-
const
|
|
808
|
-
if (projectPointer === null) {
|
|
809
|
-
for (const line of diagnoseActiveHarnessSession(active, 'not-fired', installations, contested)) {
|
|
810
|
-
deps.io.err(line);
|
|
811
|
-
}
|
|
812
|
-
return EXIT.usage;
|
|
813
|
-
}
|
|
814
|
-
const routeProblems = activeQuestionRouteProblems(deps, active, installations);
|
|
778
|
+
const routeProblems = activeQuestionRouteProblems(installationDeps, active, installations);
|
|
815
779
|
if (routeProblems.length > 0) {
|
|
816
|
-
|
|
817
|
-
deps.io.err(`Question routing is not ready: ${problem}`);
|
|
818
|
-
return EXIT.usage;
|
|
780
|
+
return askFailure(deps, flags, 'question_routing_unavailable', 'hook_contract', `Question routing is not ready: ${routeProblems.join('; ')}`, 'run `notifai init --json` and follow its question-routing remedy');
|
|
819
781
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return
|
|
782
|
+
const state = exactState ?? readSessionState(active.sessionId, deps.env);
|
|
783
|
+
if (state.harness !== active.harness || state.last_prompt_at === undefined) {
|
|
784
|
+
return askFailure(deps, flags, 'session_not_activated', 'user_prompt_submit', `This exact ${active.label} session has not fired UserPromptSubmit.`, `send one prompt in this ${active.label} session, then retry \`notifai ask --json\``);
|
|
785
|
+
}
|
|
786
|
+
if (state.last_stop_at === undefined) {
|
|
787
|
+
return askFailure(deps, flags, 'stop_not_observed', 'stop_hook', `This exact ${active.label} session has fired UserPromptSubmit, but its Stop hook has not been observed.`, `end one harmless turn, send a new prompt, then retry \`notifai ask --json\``);
|
|
823
788
|
}
|
|
824
|
-
sessionId =
|
|
789
|
+
sessionId = active.sessionId;
|
|
825
790
|
}
|
|
826
791
|
else {
|
|
827
|
-
|
|
828
|
-
deps.io.err(line);
|
|
829
|
-
deps.io.err('Could not prove which live harness session owns this command, so Notifai will not register a question that could be delivered to the wrong or already-ended agent. Run it from a supported harness with exact session identity, or use a blocking `notifai send --reply` question.');
|
|
830
|
-
return EXIT.usage;
|
|
792
|
+
return askFailure(deps, flags, 'session_identity_missing', 'exact_session', 'Could not prove which live harness session owns this command.', 'run it from a supported harness with exact session identity, or use a blocking `notifai send --reply` question');
|
|
831
793
|
}
|
|
832
794
|
if (!sessionId) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
795
|
+
return askFailure(deps, flags, 'session_identity_missing', 'exact_session', 'No exact active session is available.', 'use a blocking `notifai send --reply` question');
|
|
796
|
+
}
|
|
797
|
+
let routingConfig;
|
|
798
|
+
try {
|
|
799
|
+
routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env, sessionId });
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
return askFailure(deps, flags, 'config_invalid', 'configuration', `Question routing configuration is invalid: ${String(err)}`, 'fix the reported Notifai configuration and retry');
|
|
803
|
+
}
|
|
804
|
+
if (!routingConfig.ask_notifications.value) {
|
|
805
|
+
return askFailure(deps, flags, 'question_routing_disabled', 'ask_notifications', 'Question routing is disabled by ask_notifications=false.', 'enable question routing or use a blocking `notifai send --reply` question');
|
|
836
806
|
}
|
|
837
807
|
const source = resolveDraftInvocation(deps, flags, active);
|
|
838
808
|
if (!source.ok) {
|
|
839
|
-
deps
|
|
840
|
-
return EXIT.usage;
|
|
809
|
+
return askFailure(deps, flags, 'invalid_source', 'source_context', source.error, 'remove the conflicting source override and retry');
|
|
841
810
|
}
|
|
842
811
|
if (source.invocation.source?.session_id !== sessionId) {
|
|
843
|
-
deps
|
|
844
|
-
return EXIT.usage;
|
|
812
|
+
return askFailure(deps, flags, 'session_identity_mismatch', 'exact_session', `The inferred session does not match the exact active ${active.label} session.`, 'remove NOTIFAI_SESSION_ID and retry from the exact active session');
|
|
845
813
|
}
|
|
846
814
|
// Placeholders let every body, source, project, media, and payload limit fail
|
|
847
815
|
// before an upload starts. The real ids replace them only after this passes.
|
|
848
816
|
const placeholders = (flags.image ?? []).map((_, index) => `med_pending_${index + 1}`);
|
|
849
817
|
const preflight = buildAskDraft(routingConfig, built, flags, source.invocation, placeholders);
|
|
850
818
|
if (!preflight.ok) {
|
|
851
|
-
deps
|
|
852
|
-
return EXIT.usage;
|
|
819
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', preflight.error, 'fix the reported field and retry');
|
|
853
820
|
}
|
|
854
821
|
if ((flags.image?.length ?? 0) > 0) {
|
|
855
822
|
return uploadAskMedia(deps, routingConfig, sessionId, built, flags, source.invocation);
|
|
@@ -894,69 +861,6 @@ export function activeQuestionRouteProblems(deps, active, installations) {
|
|
|
894
861
|
problems.push(...codexTrustProblems(matching, deps.env));
|
|
895
862
|
return [...new Set(problems)];
|
|
896
863
|
}
|
|
897
|
-
function diagnoseActiveHarnessSession(active, problem, installations, contested = []) {
|
|
898
|
-
// Naming one harness confidently is wrong when the environment carries the
|
|
899
|
-
// markers of several and none of them has fired here: whichever one the
|
|
900
|
-
// agent is told to prompt may not be the one it is running in.
|
|
901
|
-
const ambiguity = contested.length > 1
|
|
902
|
-
? [
|
|
903
|
-
`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.`,
|
|
904
|
-
]
|
|
905
|
-
: [];
|
|
906
|
-
if (problem === 'not-installed') {
|
|
907
|
-
const others = installations.map((installation) => installation.harness);
|
|
908
|
-
return [
|
|
909
|
-
`Could not register the question for the active ${active.label} session: Notifai ${active.label} hooks are not installed for this project.`,
|
|
910
|
-
...ambiguity,
|
|
911
|
-
...(others.length === 0
|
|
912
|
-
? []
|
|
913
|
-
: [
|
|
914
|
-
`Hooks installed for ${[...new Set(others)].join(', ')} do not route an active ${active.label} session.`,
|
|
915
|
-
]),
|
|
916
|
-
`Run \`notifai hooks install --harness ${active.harness}\`, then send one ${active.label} prompt and run \`notifai doctor\`.`,
|
|
917
|
-
`Retry \`notifai ask\` only after doctor reports that the ${active.label} hooks fired.`,
|
|
918
|
-
];
|
|
919
|
-
}
|
|
920
|
-
if (problem === 'different-session') {
|
|
921
|
-
return [
|
|
922
|
-
`Could not register the question for the active ${active.label} session: the project pointer belongs to another ${active.label} session or harness.`,
|
|
923
|
-
...ambiguity,
|
|
924
|
-
`Refusing to guess or cross-wire the question. Send one prompt in this ${active.label} session, then run \`notifai doctor\`.`,
|
|
925
|
-
`Retry \`notifai ask\` only after doctor reports that this active ${active.label} session fired the hooks.`,
|
|
926
|
-
];
|
|
927
|
-
}
|
|
928
|
-
return [
|
|
929
|
-
`Could not register the question for the active ${active.label} session: Notifai hooks are installed, but this session has not published its pointer.`,
|
|
930
|
-
...ambiguity,
|
|
931
|
-
`Send one ${active.label} prompt, then run \`notifai doctor\`.`,
|
|
932
|
-
`Retry \`notifai ask\` only after doctor reports that the active ${active.label} session fired the hooks.`,
|
|
933
|
-
];
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Why `ask` cannot see a session, in terms of what to do about it.
|
|
937
|
-
*
|
|
938
|
-
* Only a UserPromptSubmit hook firing produces the pointer this reads, and the
|
|
939
|
-
* old message answered every cause with "run `notifai hooks install` and send
|
|
940
|
-
* one prompt". The useful next action depends on the harness: some reload
|
|
941
|
-
* project hook files, OpenCode loads its plugin at startup, and Codex should be
|
|
942
|
-
* checked after a prompt before assuming that a new session is required.
|
|
943
|
-
*/
|
|
944
|
-
function diagnoseMissingSession(deps) {
|
|
945
|
-
const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
|
|
946
|
-
if (installations.length === 0) {
|
|
947
|
-
return [
|
|
948
|
-
'Could not tell which harness session this is: no Notifai hooks are installed for this project.',
|
|
949
|
-
'Run `notifai hooks install`, then follow the activation instruction it prints.',
|
|
950
|
-
];
|
|
951
|
-
}
|
|
952
|
-
const where = installations.map((i) => `${i.harness} in ${i.file}`).join(', ');
|
|
953
|
-
return [
|
|
954
|
-
`Could not tell which harness session this is. Notifai hooks are installed (${where}),`,
|
|
955
|
-
'but no usable session pointer from the last 24 hours exists here.',
|
|
956
|
-
hookActivationAdvice(installations),
|
|
957
|
-
'Do not bypass this with a guessed session id; run from the exact active harness session or use a blocking `notifai send --reply` question.',
|
|
958
|
-
];
|
|
959
|
-
}
|
|
960
864
|
/** The least disruptive verified way to make each installed adapter run once. */
|
|
961
865
|
export function hookActivationAdvice(installations) {
|
|
962
866
|
const harnesses = new Set(installations.map((installation) => installation.harness));
|
|
@@ -1000,7 +904,12 @@ export async function acknowledgeCommand(deps, requestId, flags) {
|
|
|
1000
904
|
return EXIT.usage;
|
|
1001
905
|
}
|
|
1002
906
|
const logger = log(deps);
|
|
1003
|
-
const
|
|
907
|
+
const lifecycleSession = resolveCommandSession(deps, requestId);
|
|
908
|
+
const config = loadLoggedConfig(deps, {
|
|
909
|
+
cwd: deps.cwd,
|
|
910
|
+
env: deps.env,
|
|
911
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
912
|
+
});
|
|
1004
913
|
logger.info('acknowledgement.attempted', {
|
|
1005
914
|
request_id: requestId,
|
|
1006
915
|
characters: text.length,
|
|
@@ -1023,9 +932,8 @@ export async function acknowledgeCommand(deps, requestId, flags) {
|
|
|
1023
932
|
created_at: result.agent_acknowledgement.created_at,
|
|
1024
933
|
agent_acknowledgement_required: true,
|
|
1025
934
|
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
clearAcknowledgementObligation(sessionId, deps.env, requestId);
|
|
935
|
+
if (lifecycleSession !== null) {
|
|
936
|
+
clearAcknowledgementObligation(lifecycleSession.sessionId, deps.env, requestId);
|
|
1029
937
|
}
|
|
1030
938
|
if (flags.json)
|
|
1031
939
|
deps.io.out(JSON.stringify(output, null, 2));
|
|
@@ -1054,7 +962,12 @@ export async function closeCommand(deps, requestId, flags = {}) {
|
|
|
1054
962
|
const local = await closeLocalQuestion(deps, requestId, flags.json === true);
|
|
1055
963
|
if (local !== null)
|
|
1056
964
|
return local;
|
|
1057
|
-
const
|
|
965
|
+
const lifecycleSession = resolveCommandSession(deps, requestId);
|
|
966
|
+
const config = loadLoggedConfig(deps, {
|
|
967
|
+
cwd: deps.cwd,
|
|
968
|
+
env: deps.env,
|
|
969
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
970
|
+
});
|
|
1058
971
|
const authed = authedClient(deps, config);
|
|
1059
972
|
if (!authed)
|
|
1060
973
|
return EXIT.auth;
|
|
@@ -1084,12 +997,12 @@ export async function closeCommand(deps, requestId, flags = {}) {
|
|
|
1084
997
|
* them is the whole retirement. Live ones still need their reply window closed.
|
|
1085
998
|
*/
|
|
1086
999
|
async function closePendingQuestions(deps, json) {
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
if (sessionId === null) {
|
|
1000
|
+
const lifecycleSession = resolveCommandSession(deps);
|
|
1001
|
+
if (lifecycleSession === null) {
|
|
1090
1002
|
deps.io.err('No active session pointer is available in this directory.');
|
|
1091
1003
|
return EXIT.noReply;
|
|
1092
1004
|
}
|
|
1005
|
+
const sessionId = lifecycleSession.sessionId;
|
|
1093
1006
|
const pending = readSessionState(sessionId, deps.env).pending ?? [];
|
|
1094
1007
|
if (pending.length === 0) {
|
|
1095
1008
|
if (json) {
|
|
@@ -1104,7 +1017,7 @@ async function closePendingQuestions(deps, json) {
|
|
|
1104
1017
|
const live = (readSessionState(sessionId, deps.env).pending ?? []).filter((entry) => entry.request_id !== undefined);
|
|
1105
1018
|
const closed = [];
|
|
1106
1019
|
if (live.length > 0) {
|
|
1107
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
|
|
1020
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId });
|
|
1108
1021
|
const authed = authedClient(deps, config);
|
|
1109
1022
|
if (!authed)
|
|
1110
1023
|
return EXIT.auth;
|
|
@@ -1149,10 +1062,10 @@ async function closePendingQuestions(deps, json) {
|
|
|
1149
1062
|
* the caller can still close a live request id against the service.
|
|
1150
1063
|
*/
|
|
1151
1064
|
async function closeLocalQuestion(deps, id, json) {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
if (sessionId === null)
|
|
1065
|
+
const lifecycleSession = resolveCommandSession(deps, id);
|
|
1066
|
+
if (lifecycleSession === null)
|
|
1155
1067
|
return null;
|
|
1068
|
+
const sessionId = lifecycleSession.sessionId;
|
|
1156
1069
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find((candidate) => candidate.question_id === id || candidate.request_id === id);
|
|
1157
1070
|
if (entry === undefined)
|
|
1158
1071
|
return null;
|
|
@@ -1175,7 +1088,7 @@ async function closeLocalQuestion(deps, id, json) {
|
|
|
1175
1088
|
}
|
|
1176
1089
|
return EXIT.ok;
|
|
1177
1090
|
}
|
|
1178
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
|
|
1091
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId });
|
|
1179
1092
|
const authed = authedClient(deps, config);
|
|
1180
1093
|
if (!authed)
|
|
1181
1094
|
return EXIT.auth;
|
|
@@ -1202,9 +1115,10 @@ async function closeLocalQuestion(deps, id, json) {
|
|
|
1202
1115
|
}
|
|
1203
1116
|
}
|
|
1204
1117
|
function forgetClosedQuestion(deps, requestId) {
|
|
1205
|
-
const
|
|
1206
|
-
if (
|
|
1118
|
+
const lifecycleSession = resolveCommandSession(deps, requestId);
|
|
1119
|
+
if (lifecycleSession === null)
|
|
1207
1120
|
return;
|
|
1121
|
+
const sessionId = lifecycleSession.sessionId;
|
|
1208
1122
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find((candidate) => candidate.request_id === requestId);
|
|
1209
1123
|
if (entry === undefined)
|
|
1210
1124
|
return;
|