@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/command-session.d.ts +14 -0
  3. package/dist/command-session.js +30 -0
  4. package/dist/command-session.js.map +1 -0
  5. package/dist/commands-doctor.js +17 -15
  6. package/dist/commands-doctor.js.map +1 -1
  7. package/dist/commands-guidance.js +3 -29
  8. package/dist/commands-guidance.js.map +1 -1
  9. package/dist/commands-harness-context.js +21 -1
  10. package/dist/commands-harness-context.js.map +1 -1
  11. package/dist/commands-hooks.d.ts +11 -1
  12. package/dist/commands-hooks.js +97 -141
  13. package/dist/commands-hooks.js.map +1 -1
  14. package/dist/commands-init.d.ts +2 -0
  15. package/dist/commands-init.js +41 -21
  16. package/dist/commands-init.js.map +1 -1
  17. package/dist/commands-send.d.ts +1 -0
  18. package/dist/commands-send.js +62 -14
  19. package/dist/commands-send.js.map +1 -1
  20. package/dist/commands-setup-proof.d.ts +4 -3
  21. package/dist/commands-setup-proof.js +29 -14
  22. package/dist/commands-setup-proof.js.map +1 -1
  23. package/dist/commands.d.ts +1 -1
  24. package/dist/commands.js +1 -1
  25. package/dist/commands.js.map +1 -1
  26. package/dist/guidance-render.d.ts +20 -0
  27. package/dist/guidance-render.js +42 -0
  28. package/dist/guidance-render.js.map +1 -0
  29. package/dist/hooks.d.ts +17 -2
  30. package/dist/hooks.js +59 -7
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/opencode-plugin.js +16 -5
  33. package/dist/opencode-plugin.js.map +1 -1
  34. package/dist/program.js +21 -12
  35. package/dist/program.js.map +1 -1
  36. package/dist/readiness.d.ts +6 -0
  37. package/dist/readiness.js +37 -0
  38. package/dist/readiness.js.map +1 -1
  39. package/dist/send-attempts.d.ts +26 -0
  40. package/dist/send-attempts.js +139 -0
  41. package/dist/send-attempts.js.map +1 -0
  42. package/dist/session-activation.d.ts +4 -8
  43. package/dist/session-activation.js +25 -14
  44. package/dist/session-activation.js.map +1 -1
  45. package/package.json +1 -1
  46. package/src/command-session.ts +38 -0
  47. package/src/commands-doctor.ts +18 -28
  48. package/src/commands-guidance.ts +3 -38
  49. package/src/commands-harness-context.ts +24 -1
  50. package/src/commands-hooks.ts +217 -170
  51. package/src/commands-init.ts +46 -24
  52. package/src/commands-send.ts +70 -14
  53. package/src/commands-setup-proof.test.ts +72 -0
  54. package/src/commands-setup-proof.ts +33 -14
  55. package/src/commands.test.ts +209 -67
  56. package/src/commands.ts +1 -0
  57. package/src/guidance-render.ts +56 -0
  58. package/src/guidance.test.ts +28 -0
  59. package/src/hooks.test.ts +38 -7
  60. package/src/hooks.ts +69 -5
  61. package/src/install-hooks.test.ts +3 -3
  62. package/src/opencode-plugin.ts +16 -5
  63. package/src/program.ts +22 -12
  64. package/src/readiness.ts +44 -0
  65. package/src/send-attempts.test.ts +143 -0
  66. package/src/send-attempts.ts +188 -0
  67. package/src/session-activation.ts +34 -17
  68. package/src/skill.test.ts +12 -14
@@ -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, readMatchingProjectSessionPointer, readProjectSession, readSessionState, resetCursorStopActivation, registerQuestion, waiterCeilingSeconds, withdrawUnpushedQuestions, } from './hooks.js';
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.io.err(`Could not register the question: ${err instanceof Error ? err.message : String(err)}`);
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.io.err(ready.error);
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.io.err(escapedBody);
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.io.err(built.error);
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.io.err(mediaInputError);
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.io.err('Question routing is not paired on this machine; run `notifai login` before registering an asynchronous question.');
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
- const installations = findInstallations(deps.cwd, deps.env, deps.hookAdapterHome, deps.hookPlatform);
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
- for (const line of diagnoseActiveHarnessSession(active, 'not-installed', installations, contested)) {
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
- for (const problem of activeQuestionRouteProblems(deps, active, installations)) {
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(deps, active, installations);
820
+ const routeProblems = activeQuestionRouteProblems(installationDeps, active, installations);
815
821
  if (routeProblems.length > 0) {
816
- for (const problem of routeProblems)
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
- if (readSessionState(projectPointer.sessionId, deps.env).last_stop_at === undefined) {
821
- deps.io.err(`Question routing is not ready: this ${active.label} session has fired UserPromptSubmit, but its Stop hook has not been observed. End one harmless turn, send a new prompt, then run \`notifai doctor\`.`);
822
- return EXIT.usage;
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
- sessionId = projectPointer.sessionId;
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
- for (const line of diagnoseMissingSession(deps))
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
- for (const line of diagnoseMissingSession(deps))
834
- deps.io.err(line);
835
- return EXIT.usage;
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.io.err(source.error);
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.io.err(`Question routing is not ready: --session-id or NOTIFAI_SESSION_ID does not match the exact active ${active.label} session; refusing cross-session routing.`);
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.io.err(preflight.error);
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 config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
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
- const sessionId = readProjectSession(deps.cwd, deps.env, (deps.now ?? Date.now)());
1027
- if (sessionId !== null) {
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 config = loadLoggedConfig(deps, { cwd: deps.cwd, env: deps.env });
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 now = (deps.now ?? Date.now)();
1088
- const sessionId = readProjectSession(deps.cwd, deps.env, now);
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 now = (deps.now ?? Date.now)();
1153
- const sessionId = readProjectSession(deps.cwd, deps.env, now);
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 sessionId = readProjectSession(deps.cwd, deps.env, (deps.now ?? Date.now)());
1206
- if (sessionId === null)
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;