@raidiant/notifai 9.0.0 → 9.1.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 +7 -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 +97 -141
- 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 +17 -2
- package/dist/hooks.js +59 -7
- package/dist/hooks.js.map +1 -1
- package/dist/opencode-plugin.js +16 -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 +24 -1
- package/src/commands-hooks.ts +217 -170
- 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 +38 -7
- package/src/hooks.ts +69 -5
- package/src/install-hooks.test.ts +3 -3
- package/src/opencode-plugin.ts +16 -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 +34 -17
- package/src/skill.test.ts +12 -14
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, claimPromptActivation, claimCursorStopActivation, confirmCursorStopActivation, dropPendingQuestion, handleSessionEnd, handleStop, handleUserPromptSubmit, markSessionActivation, parkForRetirement, parseHookInput, pruneAbandonedSessions,
|
|
12
|
+
import { clearAcknowledgementObligation, claimPromptActivation, claimCursorStopActivation, confirmCursorStopActivation, dropPendingQuestion, handleSessionEnd, handleStop, handleUserPromptSubmit, markSessionActivation, 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
|
// ---------------------------------------------------------------------------
|
|
@@ -159,7 +160,7 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
159
160
|
});
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
|
-
const stdout = sessionActivationOutput(harness, event === 'session-start' ? 'SessionStart' : 'SubagentStart');
|
|
163
|
+
const stdout = sessionActivationOutput(harness, event === 'session-start' ? 'SessionStart' : 'SubagentStart', cwd, deps.env);
|
|
163
164
|
if (stdout !== undefined)
|
|
164
165
|
deps.io.out(stdout);
|
|
165
166
|
if (stdout !== undefined &&
|
|
@@ -170,7 +171,7 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
170
171
|
// Write stdout first. If the process dies between these operations a
|
|
171
172
|
// later prompt may duplicate context, but it cannot suppress context
|
|
172
173
|
// that the host never received.
|
|
173
|
-
markSessionActivation(envelope.session_id, deps.env, now(), activationOwnerPid);
|
|
174
|
+
markSessionActivation(envelope.session_id, deps.env, now(), activationOwnerPid, harness, cwd);
|
|
174
175
|
}
|
|
175
176
|
catch (err) {
|
|
176
177
|
logger.error('hook.end', {
|
|
@@ -199,8 +200,8 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
199
200
|
activationOwnerPid !== undefined &&
|
|
200
201
|
envelope.session_id !== undefined) {
|
|
201
202
|
try {
|
|
202
|
-
if (claimPromptActivation(envelope.session_id, deps.env, now(), activationOwnerPid)) {
|
|
203
|
-
promptActivationStdout = sessionActivationOutput(harness, 'UserPromptSubmit');
|
|
203
|
+
if (claimPromptActivation(envelope.session_id, deps.env, now(), activationOwnerPid, harness, cwd)) {
|
|
204
|
+
promptActivationStdout = sessionActivationOutput(harness, 'UserPromptSubmit', cwd, deps.env);
|
|
204
205
|
if (promptActivationStdout !== undefined)
|
|
205
206
|
deps.io.out(promptActivationStdout);
|
|
206
207
|
}
|
|
@@ -291,7 +292,7 @@ export async function hookRunCommand(deps, event, readStdin, harness) {
|
|
|
291
292
|
decided: claimed,
|
|
292
293
|
});
|
|
293
294
|
if (claimed)
|
|
294
|
-
deps.io.out(cursorStopActivationOutput());
|
|
295
|
+
deps.io.out(cursorStopActivationOutput(cwd, deps.env));
|
|
295
296
|
return EXIT.ok;
|
|
296
297
|
}
|
|
297
298
|
let config = null;
|
|
@@ -510,6 +511,26 @@ export function describeHookFailure(err) {
|
|
|
510
511
|
}
|
|
511
512
|
return lines;
|
|
512
513
|
}
|
|
514
|
+
/** One stable failure contract for every machine-readable ask refusal. */
|
|
515
|
+
export function reportAskFailure(deps, flags, failure) {
|
|
516
|
+
if (flags.json === true) {
|
|
517
|
+
deps.io.out(JSON.stringify({ ok: false, registered: false, ...failure }, null, 2));
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
deps.io.err(failure.message);
|
|
521
|
+
deps.io.err(`next: ${failure.remedy}`);
|
|
522
|
+
}
|
|
523
|
+
return failure.exit_code;
|
|
524
|
+
}
|
|
525
|
+
function askFailure(deps, flags, code, checkId, message, remedy, exitCode = EXIT.usage) {
|
|
526
|
+
return reportAskFailure(deps, flags, {
|
|
527
|
+
code,
|
|
528
|
+
check_id: checkId,
|
|
529
|
+
exit_code: exitCode,
|
|
530
|
+
remedy,
|
|
531
|
+
message,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
513
534
|
/**
|
|
514
535
|
* Turn ask input into questions plus their canonical body. Everything is
|
|
515
536
|
* validated at registration because a later hook failure is easy to miss.
|
|
@@ -657,8 +678,7 @@ function recordRegisteredQuestion(deps, sessionId, built, draft, json = false) {
|
|
|
657
678
|
}
|
|
658
679
|
catch (err) {
|
|
659
680
|
log(deps).error('ask.registered', { ok: false, session: sessionId, message: String(err) });
|
|
660
|
-
deps
|
|
661
|
-
return EXIT.failed;
|
|
681
|
+
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
682
|
}
|
|
663
683
|
log(deps).info('ask.registered', {
|
|
664
684
|
ok: true,
|
|
@@ -727,8 +747,9 @@ function recordRegisteredQuestion(deps, sessionId, built, draft, json = false) {
|
|
|
727
747
|
}
|
|
728
748
|
async function uploadAskMedia(deps, config, sessionId, built, flags, invocation) {
|
|
729
749
|
const authed = authedClient(deps, config);
|
|
730
|
-
if (!authed)
|
|
731
|
-
return EXIT.auth;
|
|
750
|
+
if (!authed) {
|
|
751
|
+
return askFailure(deps, flags, 'auth_required', 'credential', 'Question routing is not paired on this machine.', 'run `notifai init --json`', EXIT.auth);
|
|
752
|
+
}
|
|
732
753
|
const mediaIds = [];
|
|
733
754
|
for (const image of flags.image ?? []) {
|
|
734
755
|
if (image.startsWith('med_')) {
|
|
@@ -739,14 +760,13 @@ async function uploadAskMedia(deps, config, sessionId, built, flags, invocation)
|
|
|
739
760
|
if (!uploaded.ok) {
|
|
740
761
|
if (uploaded.error !== null)
|
|
741
762
|
deps.io.err(uploaded.error);
|
|
742
|
-
return uploaded.exit;
|
|
763
|
+
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
764
|
}
|
|
744
765
|
mediaIds.push(uploaded.mediaId);
|
|
745
766
|
}
|
|
746
767
|
const ready = buildAskDraft(config, built, flags, invocation, mediaIds);
|
|
747
768
|
if (!ready.ok) {
|
|
748
|
-
deps
|
|
749
|
-
return EXIT.usage;
|
|
769
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', ready.error, 'fix the reported field and retry');
|
|
750
770
|
}
|
|
751
771
|
return recordRegisteredQuestion(deps, sessionId, built, ready.draft, flags.json === true);
|
|
752
772
|
}
|
|
@@ -761,27 +781,18 @@ export function askCommand(deps, question, flags) {
|
|
|
761
781
|
// happens to exist on this machine.
|
|
762
782
|
const escapedBody = rejectAccidentalEscapedNewlines(flags.body, flags.literalBackslashN);
|
|
763
783
|
if (escapedBody !== null) {
|
|
764
|
-
deps
|
|
765
|
-
return EXIT.usage;
|
|
784
|
+
return askFailure(deps, flags, 'invalid_input', 'body', escapedBody, 'fix the body and retry');
|
|
766
785
|
}
|
|
767
786
|
const built = buildQuestions(flags, question);
|
|
768
787
|
if (!built.ok) {
|
|
769
|
-
deps
|
|
770
|
-
return EXIT.usage;
|
|
788
|
+
return askFailure(deps, flags, 'invalid_input', 'questions', built.error, 'fix the question input and retry');
|
|
771
789
|
}
|
|
772
790
|
const mediaInputError = validateMediaInputs(flags.image, flags.imageAlt);
|
|
773
791
|
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;
|
|
792
|
+
return askFailure(deps, flags, 'invalid_input', 'media', mediaInputError, 'fix the media flags and retry');
|
|
781
793
|
}
|
|
782
794
|
if (deps.store.load() === null) {
|
|
783
|
-
deps
|
|
784
|
-
return EXIT.usage;
|
|
795
|
+
return askFailure(deps, flags, 'auth_required', 'credential', 'Question routing is not paired on this machine.', 'run `notifai init --json`', EXIT.auth);
|
|
785
796
|
}
|
|
786
797
|
// An agent calling this gets no hook payload. Harness-native environment
|
|
787
798
|
// markers identify the active owner, while UserPromptSubmit adds the hook's
|
|
@@ -790,66 +801,64 @@ export function askCommand(deps, question, flags) {
|
|
|
790
801
|
const { active, contested } = resolveActiveHarness(deps.env, deps.cwd, now);
|
|
791
802
|
let sessionId;
|
|
792
803
|
if (active !== null) {
|
|
793
|
-
|
|
804
|
+
if (contested.length > 1) {
|
|
805
|
+
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');
|
|
806
|
+
}
|
|
807
|
+
const exactState = active.sessionId === undefined
|
|
808
|
+
? null
|
|
809
|
+
: readSessionState(active.sessionId, deps.env);
|
|
810
|
+
const installationCwd = exactState?.activation_cwd ?? deps.cwd;
|
|
811
|
+
const installationDeps = installationCwd === deps.cwd ? deps : { ...deps, cwd: installationCwd };
|
|
812
|
+
const installations = findInstallations(installationCwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
|
|
794
813
|
const activeInstalled = installations.some((installation) => installation.harness === active.harness);
|
|
795
814
|
if (!activeInstalled) {
|
|
796
|
-
|
|
797
|
-
deps.io.err(line);
|
|
798
|
-
}
|
|
799
|
-
return EXIT.usage;
|
|
815
|
+
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
816
|
}
|
|
801
817
|
if (active.sessionId === undefined) {
|
|
802
|
-
|
|
803
|
-
deps.io.err(`Question routing is not ready: ${problem}`);
|
|
804
|
-
}
|
|
805
|
-
return EXIT.usage;
|
|
806
|
-
}
|
|
807
|
-
const projectPointer = readMatchingProjectSessionPointer(deps.cwd, deps.env, now, active.sessionId, active.harness);
|
|
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;
|
|
818
|
+
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');
|
|
813
819
|
}
|
|
814
|
-
const routeProblems = activeQuestionRouteProblems(
|
|
820
|
+
const routeProblems = activeQuestionRouteProblems(installationDeps, active, installations);
|
|
815
821
|
if (routeProblems.length > 0) {
|
|
816
|
-
|
|
817
|
-
deps.io.err(`Question routing is not ready: ${problem}`);
|
|
818
|
-
return EXIT.usage;
|
|
822
|
+
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
823
|
}
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return
|
|
824
|
+
const state = exactState ?? readSessionState(active.sessionId, deps.env);
|
|
825
|
+
if (state.harness !== active.harness || state.last_prompt_at === undefined) {
|
|
826
|
+
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\``);
|
|
823
827
|
}
|
|
824
|
-
|
|
828
|
+
if (state.last_stop_at === undefined) {
|
|
829
|
+
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\``);
|
|
830
|
+
}
|
|
831
|
+
sessionId = active.sessionId;
|
|
825
832
|
}
|
|
826
833
|
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;
|
|
834
|
+
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
835
|
}
|
|
832
836
|
if (!sessionId) {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
837
|
+
return askFailure(deps, flags, 'session_identity_missing', 'exact_session', 'No exact active session is available.', 'use a blocking `notifai send --reply` question');
|
|
838
|
+
}
|
|
839
|
+
let routingConfig;
|
|
840
|
+
try {
|
|
841
|
+
routingConfig = loadConfig({ cwd: deps.cwd, env: deps.env, sessionId });
|
|
842
|
+
}
|
|
843
|
+
catch (err) {
|
|
844
|
+
return askFailure(deps, flags, 'config_invalid', 'configuration', `Question routing configuration is invalid: ${String(err)}`, 'fix the reported Notifai configuration and retry');
|
|
845
|
+
}
|
|
846
|
+
if (!routingConfig.ask_notifications.value) {
|
|
847
|
+
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
848
|
}
|
|
837
849
|
const source = resolveDraftInvocation(deps, flags, active);
|
|
838
850
|
if (!source.ok) {
|
|
839
|
-
deps
|
|
840
|
-
return EXIT.usage;
|
|
851
|
+
return askFailure(deps, flags, 'invalid_source', 'source_context', source.error, 'remove the conflicting source override and retry');
|
|
841
852
|
}
|
|
842
853
|
if (source.invocation.source?.session_id !== sessionId) {
|
|
843
|
-
deps
|
|
844
|
-
return EXIT.usage;
|
|
854
|
+
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
855
|
}
|
|
846
856
|
// Placeholders let every body, source, project, media, and payload limit fail
|
|
847
857
|
// before an upload starts. The real ids replace them only after this passes.
|
|
848
858
|
const placeholders = (flags.image ?? []).map((_, index) => `med_pending_${index + 1}`);
|
|
849
859
|
const preflight = buildAskDraft(routingConfig, built, flags, source.invocation, placeholders);
|
|
850
860
|
if (!preflight.ok) {
|
|
851
|
-
deps
|
|
852
|
-
return EXIT.usage;
|
|
861
|
+
return askFailure(deps, flags, 'invalid_draft', 'draft', preflight.error, 'fix the reported field and retry');
|
|
853
862
|
}
|
|
854
863
|
if ((flags.image?.length ?? 0) > 0) {
|
|
855
864
|
return uploadAskMedia(deps, routingConfig, sessionId, built, flags, source.invocation);
|
|
@@ -894,69 +903,6 @@ export function activeQuestionRouteProblems(deps, active, installations) {
|
|
|
894
903
|
problems.push(...codexTrustProblems(matching, deps.env));
|
|
895
904
|
return [...new Set(problems)];
|
|
896
905
|
}
|
|
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
906
|
/** The least disruptive verified way to make each installed adapter run once. */
|
|
961
907
|
export function hookActivationAdvice(installations) {
|
|
962
908
|
const harnesses = new Set(installations.map((installation) => installation.harness));
|
|
@@ -1000,7 +946,12 @@ export async function acknowledgeCommand(deps, requestId, flags) {
|
|
|
1000
946
|
return EXIT.usage;
|
|
1001
947
|
}
|
|
1002
948
|
const logger = log(deps);
|
|
1003
|
-
const
|
|
949
|
+
const lifecycleSession = resolveCommandSession(deps, requestId);
|
|
950
|
+
const config = loadLoggedConfig(deps, {
|
|
951
|
+
cwd: deps.cwd,
|
|
952
|
+
env: deps.env,
|
|
953
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
954
|
+
});
|
|
1004
955
|
logger.info('acknowledgement.attempted', {
|
|
1005
956
|
request_id: requestId,
|
|
1006
957
|
characters: text.length,
|
|
@@ -1023,9 +974,8 @@ export async function acknowledgeCommand(deps, requestId, flags) {
|
|
|
1023
974
|
created_at: result.agent_acknowledgement.created_at,
|
|
1024
975
|
agent_acknowledgement_required: true,
|
|
1025
976
|
});
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
clearAcknowledgementObligation(sessionId, deps.env, requestId);
|
|
977
|
+
if (lifecycleSession !== null) {
|
|
978
|
+
clearAcknowledgementObligation(lifecycleSession.sessionId, deps.env, requestId);
|
|
1029
979
|
}
|
|
1030
980
|
if (flags.json)
|
|
1031
981
|
deps.io.out(JSON.stringify(output, null, 2));
|
|
@@ -1054,7 +1004,12 @@ export async function closeCommand(deps, requestId, flags = {}) {
|
|
|
1054
1004
|
const local = await closeLocalQuestion(deps, requestId, flags.json === true);
|
|
1055
1005
|
if (local !== null)
|
|
1056
1006
|
return local;
|
|
1057
|
-
const
|
|
1007
|
+
const lifecycleSession = resolveCommandSession(deps, requestId);
|
|
1008
|
+
const config = loadLoggedConfig(deps, {
|
|
1009
|
+
cwd: deps.cwd,
|
|
1010
|
+
env: deps.env,
|
|
1011
|
+
...(lifecycleSession === null ? {} : { sessionId: lifecycleSession.sessionId }),
|
|
1012
|
+
});
|
|
1058
1013
|
const authed = authedClient(deps, config);
|
|
1059
1014
|
if (!authed)
|
|
1060
1015
|
return EXIT.auth;
|
|
@@ -1084,12 +1039,12 @@ export async function closeCommand(deps, requestId, flags = {}) {
|
|
|
1084
1039
|
* them is the whole retirement. Live ones still need their reply window closed.
|
|
1085
1040
|
*/
|
|
1086
1041
|
async function closePendingQuestions(deps, json) {
|
|
1087
|
-
const
|
|
1088
|
-
|
|
1089
|
-
if (sessionId === null) {
|
|
1042
|
+
const lifecycleSession = resolveCommandSession(deps);
|
|
1043
|
+
if (lifecycleSession === null) {
|
|
1090
1044
|
deps.io.err('No active session pointer is available in this directory.');
|
|
1091
1045
|
return EXIT.noReply;
|
|
1092
1046
|
}
|
|
1047
|
+
const sessionId = lifecycleSession.sessionId;
|
|
1093
1048
|
const pending = readSessionState(sessionId, deps.env).pending ?? [];
|
|
1094
1049
|
if (pending.length === 0) {
|
|
1095
1050
|
if (json) {
|
|
@@ -1104,7 +1059,7 @@ async function closePendingQuestions(deps, json) {
|
|
|
1104
1059
|
const live = (readSessionState(sessionId, deps.env).pending ?? []).filter((entry) => entry.request_id !== undefined);
|
|
1105
1060
|
const closed = [];
|
|
1106
1061
|
if (live.length > 0) {
|
|
1107
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
|
|
1062
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId });
|
|
1108
1063
|
const authed = authedClient(deps, config);
|
|
1109
1064
|
if (!authed)
|
|
1110
1065
|
return EXIT.auth;
|
|
@@ -1149,10 +1104,10 @@ async function closePendingQuestions(deps, json) {
|
|
|
1149
1104
|
* the caller can still close a live request id against the service.
|
|
1150
1105
|
*/
|
|
1151
1106
|
async function closeLocalQuestion(deps, id, json) {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
if (sessionId === null)
|
|
1107
|
+
const lifecycleSession = resolveCommandSession(deps, id);
|
|
1108
|
+
if (lifecycleSession === null)
|
|
1155
1109
|
return null;
|
|
1110
|
+
const sessionId = lifecycleSession.sessionId;
|
|
1156
1111
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find((candidate) => candidate.question_id === id || candidate.request_id === id);
|
|
1157
1112
|
if (entry === undefined)
|
|
1158
1113
|
return null;
|
|
@@ -1175,7 +1130,7 @@ async function closeLocalQuestion(deps, id, json) {
|
|
|
1175
1130
|
}
|
|
1176
1131
|
return EXIT.ok;
|
|
1177
1132
|
}
|
|
1178
|
-
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
|
|
1133
|
+
const config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env, sessionId });
|
|
1179
1134
|
const authed = authedClient(deps, config);
|
|
1180
1135
|
if (!authed)
|
|
1181
1136
|
return EXIT.auth;
|
|
@@ -1202,9 +1157,10 @@ async function closeLocalQuestion(deps, id, json) {
|
|
|
1202
1157
|
}
|
|
1203
1158
|
}
|
|
1204
1159
|
function forgetClosedQuestion(deps, requestId) {
|
|
1205
|
-
const
|
|
1206
|
-
if (
|
|
1160
|
+
const lifecycleSession = resolveCommandSession(deps, requestId);
|
|
1161
|
+
if (lifecycleSession === null)
|
|
1207
1162
|
return;
|
|
1163
|
+
const sessionId = lifecycleSession.sessionId;
|
|
1208
1164
|
const entry = (readSessionState(sessionId, deps.env).pending ?? []).find((candidate) => candidate.request_id === requestId);
|
|
1209
1165
|
if (entry === undefined)
|
|
1210
1166
|
return;
|