@raidiant/notifai 10.1.0 → 10.1.2

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 (204) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +29 -11
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +9 -2
  19. package/dist/commands-core.js +11 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +93 -49
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-hooks.js +47 -10
  27. package/dist/commands-hooks.js.map +1 -1
  28. package/dist/commands-init.d.ts +16 -0
  29. package/dist/commands-init.js +254 -93
  30. package/dist/commands-init.js.map +1 -1
  31. package/dist/commands-send-support.js +3 -0
  32. package/dist/commands-send-support.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-skill.js +72 -54
  37. package/dist/commands-skill.js.map +1 -1
  38. package/dist/commands-update.d.ts +10 -0
  39. package/dist/commands-update.js +141 -0
  40. package/dist/commands-update.js.map +1 -0
  41. package/dist/commands.d.ts +2 -1
  42. package/dist/commands.js +2 -1
  43. package/dist/commands.js.map +1 -1
  44. package/dist/hook-adapter.d.ts +2 -0
  45. package/dist/hook-adapter.js +12 -1
  46. package/dist/hook-adapter.js.map +1 -1
  47. package/dist/hooks.d.ts +43 -2
  48. package/dist/hooks.js +163 -21
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/invocation-context.d.ts +1 -0
  51. package/dist/invocation-context.js +9 -1
  52. package/dist/invocation-context.js.map +1 -1
  53. package/dist/main-run.d.ts +1 -0
  54. package/dist/main-run.js +42 -0
  55. package/dist/main-run.js.map +1 -0
  56. package/dist/main.js +10 -31
  57. package/dist/main.js.map +1 -1
  58. package/dist/native-skills.d.ts +13 -3
  59. package/dist/native-skills.js +51 -12
  60. package/dist/native-skills.js.map +1 -1
  61. package/dist/node-floor.d.ts +19 -0
  62. package/dist/node-floor.js +39 -0
  63. package/dist/node-floor.js.map +1 -0
  64. package/dist/orca-session-title.d.ts +5 -4
  65. package/dist/orca-session-title.js +32 -19
  66. package/dist/orca-session-title.js.map +1 -1
  67. package/dist/platform.d.ts +11 -17
  68. package/dist/platform.js +20 -23
  69. package/dist/platform.js.map +1 -1
  70. package/dist/program.d.ts +2 -1
  71. package/dist/program.js +22 -5
  72. package/dist/program.js.map +1 -1
  73. package/dist/question-settlement-process.d.ts +15 -0
  74. package/dist/question-settlement-process.js +35 -0
  75. package/dist/question-settlement-process.js.map +1 -0
  76. package/dist/readiness.d.ts +25 -1
  77. package/dist/readiness.js +32 -2
  78. package/dist/readiness.js.map +1 -1
  79. package/dist/release.d.ts +5 -5
  80. package/dist/release.js +5 -5
  81. package/dist/setup-destinations.d.ts +23 -0
  82. package/dist/setup-destinations.js +44 -0
  83. package/dist/setup-destinations.js.map +1 -0
  84. package/dist/skill-integrity.d.ts +49 -0
  85. package/dist/skill-integrity.js +154 -0
  86. package/dist/skill-integrity.js.map +1 -0
  87. package/dist/skill-source/manifest.json +16 -0
  88. package/dist/skill-source/notifai/SKILL.md +390 -0
  89. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  90. package/package.json +11 -14
  91. package/src/atomic-file.ts +0 -198
  92. package/src/claude-wake.test.ts +0 -321
  93. package/src/claude-wake.ts +0 -499
  94. package/src/cli-bin.test.ts +0 -38
  95. package/src/cli-bin.ts +0 -67
  96. package/src/cli-release.test.ts +0 -46
  97. package/src/cli-release.ts +0 -78
  98. package/src/client.test.ts +0 -390
  99. package/src/client.ts +0 -345
  100. package/src/codex-wake.test.ts +0 -254
  101. package/src/codex-wake.ts +0 -297
  102. package/src/command-session.ts +0 -38
  103. package/src/commands-agent-sessions.ts +0 -72
  104. package/src/commands-auth.ts +0 -205
  105. package/src/commands-config.ts +0 -336
  106. package/src/commands-core.ts +0 -298
  107. package/src/commands-devices.test.ts +0 -15
  108. package/src/commands-devices.ts +0 -173
  109. package/src/commands-doctor.ts +0 -1440
  110. package/src/commands-guidance.ts +0 -181
  111. package/src/commands-harness-context.ts +0 -207
  112. package/src/commands-hook-shape.ts +0 -69
  113. package/src/commands-hooks.ts +0 -2268
  114. package/src/commands-init.ts +0 -878
  115. package/src/commands-io.ts +0 -108
  116. package/src/commands-logs.ts +0 -243
  117. package/src/commands-project.ts +0 -47
  118. package/src/commands-send-support.test.ts +0 -156
  119. package/src/commands-send-support.ts +0 -311
  120. package/src/commands-send.ts +0 -769
  121. package/src/commands-setup-proof.test.ts +0 -72
  122. package/src/commands-setup-proof.ts +0 -124
  123. package/src/commands-skill.test.ts +0 -44
  124. package/src/commands-skill.ts +0 -204
  125. package/src/commands-sounds.test.ts +0 -106
  126. package/src/commands-sounds.ts +0 -49
  127. package/src/commands.test.ts +0 -7828
  128. package/src/commands.ts +0 -75
  129. package/src/config-schema.test.ts +0 -153
  130. package/src/config-schema.ts +0 -349
  131. package/src/config.test.ts +0 -581
  132. package/src/config.ts +0 -475
  133. package/src/credentials.test.ts +0 -403
  134. package/src/credentials.ts +0 -403
  135. package/src/file-lock-process-worker.test.ts +0 -64
  136. package/src/file-lock.test.ts +0 -320
  137. package/src/file-lock.ts +0 -393
  138. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  139. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  140. package/src/generated-session-label.ts +0 -155
  141. package/src/guidance-content.ts +0 -278
  142. package/src/guidance-render.ts +0 -56
  143. package/src/guidance.test.ts +0 -327
  144. package/src/guidance.ts +0 -233
  145. package/src/harness-integration.test.ts +0 -294
  146. package/src/harnesses.test.ts +0 -149
  147. package/src/harnesses.ts +0 -164
  148. package/src/hook-adapter.test.ts +0 -496
  149. package/src/hook-adapter.ts +0 -410
  150. package/src/hook-input.test.ts +0 -39
  151. package/src/hook-input.ts +0 -72
  152. package/src/hooks.test.ts +0 -5549
  153. package/src/hooks.ts +0 -3759
  154. package/src/install-hooks.test.ts +0 -1614
  155. package/src/install-hooks.ts +0 -1501
  156. package/src/interactive.test.ts +0 -387
  157. package/src/interactive.ts +0 -858
  158. package/src/invocation-context.test.ts +0 -352
  159. package/src/invocation-context.ts +0 -197
  160. package/src/logging-process-worker.test.ts +0 -45
  161. package/src/logging.test.ts +0 -544
  162. package/src/logging.ts +0 -698
  163. package/src/main.ts +0 -36
  164. package/src/native-skills.test.ts +0 -115
  165. package/src/native-skills.ts +0 -162
  166. package/src/onboarding-docs.test.ts +0 -25
  167. package/src/openclaw-plugin.ts +0 -409
  168. package/src/opencode-plugin.ts +0 -298
  169. package/src/orca-session-title.test.ts +0 -54
  170. package/src/orca-session-title.ts +0 -103
  171. package/src/pack-feedback-slice.test.ts +0 -212
  172. package/src/pack-feedback-slice.ts +0 -187
  173. package/src/platform.test.ts +0 -152
  174. package/src/platform.ts +0 -173
  175. package/src/program.test.ts +0 -282
  176. package/src/program.ts +0 -818
  177. package/src/project-enablement.test.ts +0 -55
  178. package/src/project-enablement.ts +0 -99
  179. package/src/question-caps.test.ts +0 -49
  180. package/src/question-timing.ts +0 -56
  181. package/src/readiness.test.ts +0 -25
  182. package/src/readiness.ts +0 -179
  183. package/src/release.ts +0 -51
  184. package/src/send-attempts.test.ts +0 -143
  185. package/src/send-attempts.ts +0 -188
  186. package/src/send.test.ts +0 -28
  187. package/src/send.ts +0 -436
  188. package/src/session-activation.ts +0 -61
  189. package/src/session-end-log-lock-worker.test.ts +0 -35
  190. package/src/session-labels.test.ts +0 -490
  191. package/src/session-labels.ts +0 -474
  192. package/src/skill.test.ts +0 -415
  193. package/src/sound-ref.test.ts +0 -31
  194. package/src/sound-ref.ts +0 -34
  195. package/src/trust-policy.test.ts +0 -701
  196. package/src/ui/banner.ts +0 -95
  197. package/src/ui/config-view.test.ts +0 -90
  198. package/src/ui/config-view.ts +0 -138
  199. package/src/ui/help.test.ts +0 -16
  200. package/src/ui/help.ts +0 -128
  201. package/src/ui/theme.test.ts +0 -92
  202. package/src/ui/theme.ts +0 -265
  203. package/src/url-policy.ts +0 -399
  204. package/tsconfig.json +0 -10
package/dist/hooks.js CHANGED
@@ -148,10 +148,11 @@ export function findOwningSession(id, env) {
148
148
  const sessionId = persisted.session_id;
149
149
  const state = persisted;
150
150
  const pendingMatch = pendingList(state).some((entry) => entry.question_id === id || entry.request_id === id);
151
- const retiringMatch = (state.retiring ?? []).some((entry) => entry.request_id === id);
151
+ const retiringMatch = (state.retiring ?? []).some((entry) => entry.question_id === id || entry.request_id === id);
152
+ const historyMatch = (state.question_history ?? []).some((entry) => entry.question_id === id || entry.request_id === id);
152
153
  const acknowledgementMatch = (state.acknowledgement_due ?? []).some((entry) => entry.request_id === id);
153
154
  const acceptedMatch = state.accepted?.answers.some(({ pending }) => pending.question_id === id || pending.request_id === id) ?? false;
154
- if (pendingMatch || retiringMatch || acknowledgementMatch || acceptedMatch) {
155
+ if (pendingMatch || retiringMatch || historyMatch || acknowledgementMatch || acceptedMatch) {
155
156
  matches.push(sessionId);
156
157
  if (matches.length > 1)
157
158
  return { sessionId: null, ambiguous: true };
@@ -859,15 +860,16 @@ export function parkForRetirement(sessionId, env, pending, state) {
859
860
  updateSessionState(sessionId, env, (current) => {
860
861
  const already = current.retiring ?? [];
861
862
  const existing = already.findIndex((entry) => entry.request_id === retirement.request_id);
862
- if (existing < 0)
863
- return { ...current, retiring: [...already, retirement] };
863
+ if (existing < 0) {
864
+ return rememberQuestionState({ ...current, retiring: [...already, retirement] }, pending, state === 'answered' ? 'answered' : 'retired');
865
+ }
864
866
  const retiring = [...already];
865
867
  // An observed answer is final truth and upgrades an earlier supersession
866
868
  // parked while the submission callback was racing a newer question.
867
869
  if (retirement.state === 'answered' && retiring[existing].state !== 'answered') {
868
870
  retiring[existing] = retirement;
869
871
  }
870
- return { ...current, retiring };
872
+ return rememberQuestionState({ ...current, retiring }, pending, state === 'answered' ? 'answered' : 'retired');
871
873
  });
872
874
  }
873
875
  /**
@@ -896,6 +898,7 @@ function retiringQuestion(pending, state, cwd) {
896
898
  }
897
899
  const source = sourceContextAtHookEvent(pending.source, cwd);
898
900
  return {
901
+ ...(pending.question_id !== undefined ? { question_id: pending.question_id } : {}),
899
902
  request_id: requestId,
900
903
  collapse_key: collapseKey,
901
904
  device_ids: [...deviceIds],
@@ -1133,6 +1136,122 @@ async function pollPendingReplies(ctx, live, timeoutSeconds) {
1133
1136
  function pendingList(state) {
1134
1137
  return Array.isArray(state.pending) ? state.pending : [];
1135
1138
  }
1139
+ const QUESTION_HISTORY_CAP = 50;
1140
+ /** Keep a bounded, content-free map from local identity to its terminal state. */
1141
+ function rememberQuestionState(current, question, state) {
1142
+ if (question.question_id === undefined)
1143
+ return current;
1144
+ const existing = (current.question_history ?? []).find((entry) => entry.question_id === question.question_id);
1145
+ const terminalState = existing?.state === 'withdrawn' || existing?.state === 'answered'
1146
+ ? existing.state
1147
+ : state === 'answered'
1148
+ ? 'answered'
1149
+ : state;
1150
+ const requestId = question.request_id ?? existing?.request_id;
1151
+ const frozenRequestId = requestId === undefined
1152
+ ? question.submission?.request_id ?? existing?.frozen_request_id
1153
+ : undefined;
1154
+ const record = {
1155
+ question_id: question.question_id,
1156
+ state: terminalState,
1157
+ ...(requestId === undefined ? {} : { request_id: requestId }),
1158
+ ...(frozenRequestId === undefined ? {} : { frozen_request_id: frozenRequestId }),
1159
+ };
1160
+ const history = [
1161
+ ...(current.question_history ?? []).filter((entry) => entry.question_id !== question.question_id),
1162
+ record,
1163
+ ].slice(-QUESTION_HISTORY_CAP);
1164
+ return { ...current, question_history: history };
1165
+ }
1166
+ function pendingQuestionView(entry) {
1167
+ if (entry.question_id === undefined)
1168
+ return null;
1169
+ if (entry.request_id !== undefined) {
1170
+ return {
1171
+ question_id: entry.question_id,
1172
+ state: 'live',
1173
+ submitted: true,
1174
+ request_id: entry.request_id,
1175
+ frozen_request_id: null,
1176
+ };
1177
+ }
1178
+ if (entry.submission !== undefined) {
1179
+ return {
1180
+ question_id: entry.question_id,
1181
+ state: 'frozen',
1182
+ submitted: null,
1183
+ request_id: null,
1184
+ frozen_request_id: entry.submission.request_id,
1185
+ };
1186
+ }
1187
+ return {
1188
+ question_id: entry.question_id,
1189
+ state: 'local',
1190
+ submitted: false,
1191
+ request_id: null,
1192
+ frozen_request_id: null,
1193
+ };
1194
+ }
1195
+ /** Read one stable local question identity without minting or submitting anything. */
1196
+ export function inspectQuestionState(questionId, env) {
1197
+ const owner = findOwningSession(questionId, env);
1198
+ if (owner.ambiguous || owner.sessionId === null) {
1199
+ return { found: false, ambiguous: owner.ambiguous };
1200
+ }
1201
+ const state = readSessionState(owner.sessionId, env);
1202
+ const pending = pendingList(state).find((entry) => entry.question_id === questionId);
1203
+ const pendingView = pending === undefined ? null : pendingQuestionView(pending);
1204
+ if (pendingView !== null) {
1205
+ return { found: true, session_id: owner.sessionId, question: pendingView };
1206
+ }
1207
+ const accepted = state.accepted?.answers.find(({ pending: entry }) => entry.question_id === questionId)?.pending;
1208
+ if (accepted?.question_id !== undefined) {
1209
+ return {
1210
+ found: true,
1211
+ session_id: owner.sessionId,
1212
+ question: {
1213
+ question_id: accepted.question_id,
1214
+ state: 'answered',
1215
+ submitted: true,
1216
+ request_id: accepted.request_id ?? null,
1217
+ frozen_request_id: null,
1218
+ },
1219
+ };
1220
+ }
1221
+ const retiring = (state.retiring ?? []).find((entry) => entry.question_id === questionId);
1222
+ if (retiring?.question_id !== undefined) {
1223
+ return {
1224
+ found: true,
1225
+ session_id: owner.sessionId,
1226
+ question: {
1227
+ question_id: retiring.question_id,
1228
+ state: retiring.state === 'answered' ? 'answered' : 'retired',
1229
+ submitted: true,
1230
+ request_id: retiring.request_id,
1231
+ frozen_request_id: null,
1232
+ },
1233
+ };
1234
+ }
1235
+ const history = (state.question_history ?? []).find((entry) => entry.question_id === questionId);
1236
+ if (history !== undefined) {
1237
+ return {
1238
+ found: true,
1239
+ session_id: owner.sessionId,
1240
+ question: {
1241
+ question_id: history.question_id,
1242
+ state: history.state,
1243
+ submitted: history.request_id !== undefined
1244
+ ? true
1245
+ : history.frozen_request_id !== undefined
1246
+ ? null
1247
+ : false,
1248
+ request_id: history.request_id ?? null,
1249
+ frozen_request_id: history.frozen_request_id ?? null,
1250
+ },
1251
+ };
1252
+ }
1253
+ return { found: false, ambiguous: false };
1254
+ }
1136
1255
  /** Registration identity — the convention every racing writer compares by. */
1137
1256
  function isSamePending(a, b) {
1138
1257
  if (a.question_id !== undefined || b.question_id !== undefined) {
@@ -1148,7 +1267,7 @@ function isSamePending(a, b) {
1148
1267
  function isTerminalDraftRejection(err) {
1149
1268
  return err.status === 422 || (err.status === 400 && err.code === 'invalid_request');
1150
1269
  }
1151
- export function dropPendingQuestion(sessionId, env, entry) {
1270
+ export function dropPendingQuestion(sessionId, env, entry, terminalState = 'retired') {
1152
1271
  updateSessionState(sessionId, env, (current) => {
1153
1272
  const pending = pendingList(current).filter((candidate) => !isSamePending(candidate, entry));
1154
1273
  const next = { ...current };
@@ -1156,7 +1275,7 @@ export function dropPendingQuestion(sessionId, env, entry) {
1156
1275
  next.pending = pending;
1157
1276
  else
1158
1277
  delete next.pending;
1159
- return next;
1278
+ return rememberQuestionState(next, entry, terminalState);
1160
1279
  });
1161
1280
  }
1162
1281
  /**
@@ -1177,7 +1296,7 @@ export function withdrawUnpushedQuestions(sessionId, env) {
1177
1296
  next.pending = remaining;
1178
1297
  else
1179
1298
  delete next.pending;
1180
- return next;
1299
+ return withdrawn.reduce((remembered, entry) => rememberQuestionState(remembered, entry, 'withdrawn'), next);
1181
1300
  });
1182
1301
  return withdrawn;
1183
1302
  }
@@ -1336,7 +1455,7 @@ function stageAcceptedAnswers(ctx, sessionId, answered, remaining) {
1336
1455
  next.acknowledgement_due = acknowledgementDue;
1337
1456
  else
1338
1457
  delete next.acknowledgement_due;
1339
- return next;
1458
+ return answered.reduce((remembered, answer) => rememberQuestionState(remembered, answer.pending, 'answered'), next);
1340
1459
  });
1341
1460
  return accepted;
1342
1461
  }
@@ -1530,7 +1649,7 @@ async function retirePendings(ctx, envelope, sessionId, entries, state) {
1530
1649
  next.retiring = retiring;
1531
1650
  else
1532
1651
  delete next.retiring;
1533
- return next;
1652
+ return entries.reduce((remembered, entry) => rememberQuestionState(remembered, entry, state === 'answered' ? 'answered' : 'retired'), next);
1534
1653
  });
1535
1654
  // Network retirement is deliberately deferred to a later no-question hook.
1536
1655
  // The state write above is the durable operation; blocking here creates a
@@ -1610,7 +1729,7 @@ export async function handleUserPromptSubmit(ctx, envelope) {
1610
1729
  // Park before dropping `pending`. If the process dies between these writes,
1611
1730
  // the next hook sees both copies and dedupes them; the old order could die in
1612
1731
  // the gap after erasing the only request/collapse/device identifiers.
1613
- updateSessionState(sessionId, ctx.env, (current) => {
1732
+ const updated = updateSessionState(sessionId, ctx.env, (current) => {
1614
1733
  const retiring = [...(current.retiring ?? [])];
1615
1734
  const matchedNow = pendingAnsweredByPrompt(envelope.prompt, pendingList(current));
1616
1735
  const unmatched = pendingList(current).filter((entry) => !matchedNow.some((item) => isSamePending(item, entry)));
@@ -1647,7 +1766,7 @@ export async function handleUserPromptSubmit(ctx, envelope) {
1647
1766
  if (current.continuation !== undefined) {
1648
1767
  next.continuation = { ...current.continuation, count: 0 };
1649
1768
  }
1650
- return next;
1769
+ return matchedNow.reduce((remembered, entry) => rememberQuestionState(remembered, entry, entry.request_id === undefined ? 'withdrawn' : 'retired'), next);
1651
1770
  });
1652
1771
  // The bridge that lets a plain `notifai ask` find the hook's canonical
1653
1772
  // session: an agent shell command gets no hook payload, and not every
@@ -1660,7 +1779,10 @@ export async function handleUserPromptSubmit(ctx, envelope) {
1660
1779
  // model. Keep the journal and let the next Stop use its acknowledged
1661
1780
  // continuation channel instead of recreating the crash-before-stdout gap.
1662
1781
  notes.push('the late device answer will continue the agent at this turn’s Stop');
1663
- return { notes };
1782
+ return {
1783
+ notes,
1784
+ settlementRequired: pendingList(updated).some((entry) => entry.request_id === undefined),
1785
+ };
1664
1786
  }
1665
1787
  const retired = await drainRetirements(ctx, sessionId, ctx.env);
1666
1788
  const orphaned = await drainOrphanRetirements(ctx, ctx.env, ctx.now());
@@ -1668,7 +1790,10 @@ export async function handleUserPromptSubmit(ctx, envelope) {
1668
1790
  if (swept.length > 0) {
1669
1791
  notes.push(`retired question${swept.length > 1 ? 's' : ''} ${swept.join(', ')}`);
1670
1792
  }
1671
- return { notes };
1793
+ return {
1794
+ notes,
1795
+ settlementRequired: pendingList(updated).some((entry) => entry.request_id === undefined),
1796
+ };
1672
1797
  }
1673
1798
  // ---------------------------------------------------------------------------
1674
1799
  // Stop — the turn ended; escalate a registered question
@@ -1678,7 +1803,7 @@ export async function handleUserPromptSubmit(ctx, envelope) {
1678
1803
  * `notifai ask`. Guessing from the last assistant message was the alternative
1679
1804
  * and it is not worth it: a false positive here hijacks the terminal.
1680
1805
  */
1681
- export async function handleStop(ctx, envelope, processDeadlineAt = ctx.now() + QUESTION_WAITER_CEILING_SECONDS * 1000, route = hookContinuationRoute()) {
1806
+ export async function handleStop(ctx, envelope, processDeadlineAt = ctx.now() + QUESTION_WAITER_CEILING_SECONDS * 1000, route = hookContinuationRoute(), recordStop = true) {
1682
1807
  const sessionId = envelope.session_id;
1683
1808
  if (!sessionId) {
1684
1809
  gate(ctx, 'held', 'no-session', { stage: 'queued' });
@@ -1689,6 +1814,7 @@ export async function handleStop(ctx, envelope, processDeadlineAt = ctx.now() +
1689
1814
  envelope,
1690
1815
  route,
1691
1816
  processDeadlineAt,
1817
+ recordStop,
1692
1818
  });
1693
1819
  }
1694
1820
  /**
@@ -1734,11 +1860,13 @@ export async function runEscalationWaiter(ctx, options) {
1734
1860
  // This event marker is diagnostic evidence in its own right. Record it
1735
1861
  // before every early return so doctor can distinguish a working Stop route
1736
1862
  // from the prompt-only state that previously looked healthy.
1737
- updateSessionState(sessionId, ctx.env, (current) => ({
1738
- ...current,
1739
- ...(ctx.harness === undefined ? {} : { harness: ctx.harness }),
1740
- last_stop_at: ctx.now(),
1741
- }));
1863
+ if (options.recordStop !== false) {
1864
+ updateSessionState(sessionId, ctx.env, (current) => ({
1865
+ ...current,
1866
+ ...(ctx.harness === undefined ? {} : { harness: ctx.harness }),
1867
+ last_stop_at: ctx.now(),
1868
+ }));
1869
+ }
1742
1870
  // The waiter, not its host, owns this lease. It spans grace, submission,
1743
1871
  // polling, close fencing, route delivery, and every retirement path.
1744
1872
  // Real clock, deliberately, not `ctx.now` — the claim answers whether
@@ -2585,6 +2713,16 @@ export function handleSessionEnd(env, envelope, now = Date.now()) {
2585
2713
  if (envelope.cwd !== undefined)
2586
2714
  clearMatchingProjectSession(envelope.cwd, env, sessionId);
2587
2715
  const state = readSessionState(sessionId, env);
2716
+ let stateWithHistory = state;
2717
+ for (const entry of pendingList(state)) {
2718
+ stateWithHistory = rememberQuestionState(stateWithHistory, entry, entry.request_id === undefined && entry.submission === undefined ? 'withdrawn' : 'retired');
2719
+ }
2720
+ for (const answer of state.accepted?.answers ?? []) {
2721
+ stateWithHistory = rememberQuestionState(stateWithHistory, answer.pending, 'answered');
2722
+ }
2723
+ for (const retirement of state.retiring ?? []) {
2724
+ stateWithHistory = rememberQuestionState(stateWithHistory, retirement, retirement.state === 'answered' ? 'answered' : 'retired');
2725
+ }
2588
2726
  const orphans = [...(state.retiring ?? [])];
2589
2727
  const retirementCandidates = [
2590
2728
  ...pendingList(state),
@@ -2613,7 +2751,7 @@ export function handleSessionEnd(env, envelope, now = Date.now()) {
2613
2751
  notes.push(`queued ${orphans.length} question${orphans.length > 1 ? 's' : ''} for retirement on the next hook`);
2614
2752
  }
2615
2753
  if (state.accepted !== undefined || (state.acknowledgement_due?.length ?? 0) > 0) {
2616
- const preserved = { ...state };
2754
+ const preserved = { ...stateWithHistory };
2617
2755
  delete preserved.pending;
2618
2756
  delete preserved.retiring;
2619
2757
  delete preserved.acknowledgement_blocks;
@@ -2634,7 +2772,11 @@ export function handleSessionEnd(env, envelope, now = Date.now()) {
2634
2772
  },
2635
2773
  };
2636
2774
  }
2775
+ const history = stateWithHistory.question_history;
2637
2776
  clearSessionState(sessionId, env);
2777
+ if ((history?.length ?? 0) > 0) {
2778
+ writeSessionState(sessionId, env, { question_history: history });
2779
+ }
2638
2780
  return { notes, log: { outcome: 'cleaned', queued_retirements: orphans.length } };
2639
2781
  }
2640
2782
  // ---------------------------------------------------------------------------