@raidiant/notifai 0.2.0 → 0.3.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.
@@ -0,0 +1,274 @@
1
+ /**
2
+ * What every configuration key means, in the reader's words.
3
+ *
4
+ * `config.ts` already documented all fourteen keys carefully — but in TypeScript
5
+ * comments, which is the one audience that never needed them. Someone running
6
+ * `notifai config show` saw `require_idle = true` and had no way to learn what
7
+ * it did short of reading the source, so this module is that knowledge moved to
8
+ * where it can be printed.
9
+ *
10
+ * It is the single source for `config show`, `config set`'s errors, `config
11
+ * explain`, the help footer, and the interactive settings screen. One
12
+ * description, rendered five ways, so the surfaces cannot disagree.
13
+ *
14
+ * `summary` is the one-line form that fits beside a value in a list.
15
+ * `detail` is the paragraph shown once the reader has asked for this key
16
+ * specifically — progressive disclosure, so a list of fourteen settings stays
17
+ * a list and not an essay.
18
+ */
19
+ import { CLI_SOUNDS, INTERRUPTION_LEVELS } from '@raidiant/notifai-protocol';
20
+ import { configBounds } from './config.js';
21
+ export const CONFIG_GROUPS = [
22
+ {
23
+ id: 'questions',
24
+ title: 'Questions & presence',
25
+ blurb: 'When a question an agent registered is allowed to leave this terminal and reach your devices.',
26
+ },
27
+ {
28
+ id: 'delivery',
29
+ title: 'Delivery defaults',
30
+ blurb: 'What a notification looks and behaves like when a command does not say otherwise.',
31
+ },
32
+ {
33
+ id: 'project',
34
+ title: 'This project',
35
+ blurb: 'How work in this directory identifies itself, and the house rules agents read before notifying.',
36
+ },
37
+ {
38
+ id: 'connection',
39
+ title: 'Connection',
40
+ blurb: 'Which Notifai service this machine talks to. Most people never change this.',
41
+ },
42
+ ];
43
+ const INFO = {
44
+ ask_notifications: {
45
+ label: 'Send questions to my devices',
46
+ group: 'questions',
47
+ kind: 'boolean',
48
+ summary: 'Whether a registered question may reach your phone and Mac at all',
49
+ detail: 'The master switch for question routing. When this is off, a question an agent registers with `notifai ask` stays in the terminal and never leaves this machine, whatever the presence settings say. Turn it off to stop being reached for a while without uninstalling the harness hooks.',
50
+ example: 'true',
51
+ },
52
+ require_idle: {
53
+ label: 'Only when I have stepped away',
54
+ group: 'questions',
55
+ kind: 'boolean',
56
+ summary: 'Whether sitting at this keyboard holds a question back',
57
+ detail: 'On (the default), a question waits while you are using this machine, because you are right there and a push would be redundant. Off means notify me anyway — `ask_grace_seconds` still runs, so the terminal is still offered the question first; it just no longer needs you to walk away before it may leave.\n\nThis is separate from `ask_notifications`, which switches routing off altogether: wanting to be reached while working is the opposite of not wanting to be reached.',
58
+ example: 'true',
59
+ },
60
+ away_after_seconds: {
61
+ label: 'Away after',
62
+ group: 'questions',
63
+ kind: 'integer',
64
+ unit: 's',
65
+ summary: 'Keyboard and mouse idle time before this machine counts as unattended',
66
+ detail: 'How long the keyboard and mouse must be quiet before you count as away. Only consulted while `require_idle` is on.\n\nWhere the operating system exposes no idle signal, silence since your last prompt is used instead, which is the conservative reading.',
67
+ example: '120',
68
+ },
69
+ ask_grace_seconds: {
70
+ label: 'Terminal-first grace',
71
+ group: 'questions',
72
+ kind: 'integer',
73
+ unit: 's',
74
+ summary: 'How long a question stays terminal-only before it may reach your devices',
75
+ detail: 'Measured from the moment the agent asked. The question appears in the terminal and a timer starts; only once it elapses may the question go to your devices.\n\nA timer and nothing more. It is honoured whether or not presence is consulted at all, because how long to wait and whether anyone is watching are separate questions, and answering one should not answer the other.',
76
+ example: '300',
77
+ },
78
+ hook_reply_timeout_seconds: {
79
+ label: 'Hook wait for an answer',
80
+ group: 'questions',
81
+ kind: 'integer',
82
+ unit: 's',
83
+ summary: 'How long the harness hook blocks waiting for your answer',
84
+ detail: 'After a question has gone out, the harness hook holds the agent for up to this long waiting for your answer before falling through to the harness\'s own prompt.\n\nThe ceiling exists because both Claude Code and Codex kill a command hook at 600 seconds. Grace plus this value plus the installer\'s headroom has to stay under that, or the hook is killed mid-wait — after you have already answered.',
85
+ example: '180',
86
+ },
87
+ sound: {
88
+ label: 'Sound',
89
+ group: 'delivery',
90
+ kind: 'enum',
91
+ choices: CLI_SOUNDS,
92
+ choiceHints: {
93
+ default: 'the standard notification tone',
94
+ done: 'the completion chime',
95
+ attention: 'used for questions that want an answer',
96
+ alert: 'the most insistent tone',
97
+ none: 'silent — arrives without a sound',
98
+ },
99
+ summary: 'Override the sound every notification uses',
100
+ detail: 'Normally the sound comes from what the notification is: progress updates are silent, completions get the done chime, questions get the attention tone. Setting this pins one sound for everything sent from here, overriding those profiles.\n\nA common use is `none`, to make a noisy project completely silent.',
101
+ unsetMeans: 'each notification uses the sound for its kind',
102
+ },
103
+ interruption_level: {
104
+ label: 'Interruption level',
105
+ group: 'delivery',
106
+ kind: 'enum',
107
+ choices: INTERRUPTION_LEVELS,
108
+ choiceHints: {
109
+ passive: 'no sound or wake — waits in the notification list',
110
+ active: 'wakes the screen and plays the sound',
111
+ time_sensitive: 'may break through Focus modes',
112
+ },
113
+ summary: 'Override how insistently notifications arrive',
114
+ detail: 'How hard a notification tries to reach you, in the operating system\'s terms. Like the sound, this is normally decided by what the notification is; setting it pins one level for everything.\n\n`time_sensitive` can pierce a Focus mode, so it is worth reserving for the cases that genuinely earn it.',
115
+ unsetMeans: 'each notification uses the level for its kind',
116
+ },
117
+ devices: {
118
+ label: 'Default devices',
119
+ group: 'delivery',
120
+ kind: 'list',
121
+ summary: 'Which of your devices receive by default',
122
+ detail: 'A comma-separated list of device ids. When set, notifications go only to these devices unless a command overrides it.\n\nRun `notifai devices` to see the ids and which of them can currently receive.',
123
+ unsetMeans: 'every device that can receive',
124
+ example: 'dev_abc123,dev_def456',
125
+ },
126
+ ttl_seconds: {
127
+ label: 'Delivery window',
128
+ group: 'delivery',
129
+ kind: 'integer',
130
+ unit: 's',
131
+ summary: 'How long the service keeps trying to deliver before giving up',
132
+ detail: 'A notification for a device that is off or offline is held for this long and delivered when the device reappears. After it lapses the notification is dropped rather than arriving stale.\n\nThe default of 86400 is one day.',
133
+ example: '86400',
134
+ },
135
+ wait_seconds: {
136
+ label: 'Wait for outcomes',
137
+ group: 'delivery',
138
+ kind: 'integer',
139
+ unit: 's',
140
+ summary: 'How long `send` waits to report what happened to a notification',
141
+ detail: 'After a notification is accepted, `notifai send` can wait for the delivery outcome so its exit code reflects what actually happened rather than only that the service took it.\n\nSet to 0 to return as soon as the service accepts, which is the fastest option and the one that tells you least.',
142
+ example: '10',
143
+ },
144
+ collapse_key: {
145
+ label: 'Collapse key',
146
+ group: 'delivery',
147
+ kind: 'string',
148
+ summary: 'Replace earlier notifications carrying the same key',
149
+ detail: 'Two notifications sharing a collapse key do not stack — the newer one replaces the older on the lock screen. This is how a status that changes repeatedly stays one line instead of becoming twenty.\n\nSetting it here applies one key to everything sent from here, which is usually too broad; most uses pass `--collapse-key` per send instead.',
150
+ unsetMeans: 'each notification stands on its own',
151
+ advanced: true,
152
+ },
153
+ project: {
154
+ label: 'Project id',
155
+ group: 'project',
156
+ kind: 'string',
157
+ summary: 'The name this project reports itself as',
158
+ detail: 'Stamped on every notification sent from this directory, and how the companion apps group and filter what arrives. Normally set once in `.notifai/config.toml` and committed, so everyone working on the repository reports the same name.',
159
+ unsetMeans: 'notifications arrive without a project',
160
+ example: 'my-app',
161
+ },
162
+ notify_criteria: {
163
+ label: 'House rules for agents',
164
+ group: 'project',
165
+ kind: 'string',
166
+ summary: 'Free text an agent reads before deciding whether to notify you',
167
+ detail: 'Your own guidance, in your own words, about what is worth a notification in this project — read by agents before they send. Something like "only tell me about failures and finished deploys; never per-file progress".\n\nIt is advice to the agent, not a filter the service enforces.',
168
+ unsetMeans: 'agents fall back to their default judgement',
169
+ example: 'Only failures and completed deploys',
170
+ },
171
+ base_url: {
172
+ label: 'Service URL',
173
+ group: 'connection',
174
+ kind: 'url',
175
+ summary: 'Which Notifai service this machine talks to',
176
+ detail: 'Points the CLI at a Notifai service. The default is the hosted service, and signing in stores the URL the credential belongs to, so this rarely needs setting by hand.\n\nChanging it does not move your credential: a URL that does not match the one you paired against will not authenticate.',
177
+ advanced: true,
178
+ example: 'https://notifai.fly.dev',
179
+ },
180
+ };
181
+ export function configInfo(key) {
182
+ return { key, ...INFO[key] };
183
+ }
184
+ /** Every key in its group, in the order a reader should meet them. */
185
+ export function configKeysByGroup(group) {
186
+ return Object.keys(INFO)
187
+ .filter((key) => INFO[key].group === group)
188
+ .map((key) => configInfo(key));
189
+ }
190
+ /** Bounds rendered the way an error message wants them, or null. */
191
+ export function boundsHint(key) {
192
+ const bounds = configBounds(key);
193
+ if (bounds === undefined)
194
+ return null;
195
+ const info = INFO[key];
196
+ const unit = info.unit ?? '';
197
+ return `${bounds.min}${unit}–${bounds.max}${unit}`;
198
+ }
199
+ /** What a `config set` caller may legally pass, phrased for an error message. */
200
+ export function acceptedValues(key) {
201
+ const info = INFO[key];
202
+ switch (info.kind) {
203
+ case 'boolean':
204
+ return 'true or false';
205
+ case 'enum':
206
+ return (info.choices ?? []).join(', ');
207
+ case 'integer': {
208
+ const bounds = boundsHint(key);
209
+ return bounds === null ? 'a whole number' : `a whole number from ${bounds}`;
210
+ }
211
+ case 'list':
212
+ return 'a comma-separated list';
213
+ case 'url':
214
+ return 'a URL';
215
+ case 'string':
216
+ return 'any text';
217
+ }
218
+ }
219
+ /** How a resolved value should read in a list. */
220
+ export function formatValue(key, value) {
221
+ const info = INFO[key];
222
+ if (value === null || value === undefined)
223
+ return info.unsetMeans ?? 'not set';
224
+ if (Array.isArray(value))
225
+ return value.length === 0 ? (info.unsetMeans ?? 'not set') : value.join(', ');
226
+ if (typeof value === 'boolean')
227
+ return value ? 'yes' : 'no';
228
+ if (typeof value === 'number' && info.unit !== undefined)
229
+ return formatDuration(value, info.unit);
230
+ return String(value);
231
+ }
232
+ /**
233
+ * Where a value came from, in words rather than in a tagged path.
234
+ *
235
+ * `global:/Users/you/.config/notifai/config.toml` answers "which file" but not
236
+ * "why is this the value" — and the second question is the one someone reading
237
+ * `config show` is actually asking. The path is still available for anyone
238
+ * about to edit the file; it is just no longer the whole answer.
239
+ */
240
+ export function describeSource(source) {
241
+ if (source === 'default')
242
+ return { label: 'default', path: null };
243
+ if (source === 'flag')
244
+ return { label: 'this command', path: null };
245
+ const separator = source.indexOf(':');
246
+ if (separator === -1)
247
+ return { label: source, path: null };
248
+ const layer = source.slice(0, separator);
249
+ const filePath = source.slice(separator + 1);
250
+ const labels = {
251
+ session: 'this session',
252
+ 'project-local': 'this project (personal)',
253
+ project: 'this project (shared)',
254
+ global: 'this machine',
255
+ };
256
+ return { label: labels[layer] ?? layer, path: filePath === '' ? null : filePath };
257
+ }
258
+ /** Seconds as something a person reads: `300s` is true, `5m` is legible. */
259
+ export function formatDuration(value, unit) {
260
+ if (unit !== 's')
261
+ return `${value}${unit}`;
262
+ if (value === 0)
263
+ return '0s (immediately)';
264
+ if (value < 60)
265
+ return `${value}s`;
266
+ if (value % 86400 === 0)
267
+ return `${value / 86400}d`;
268
+ if (value % 3600 === 0)
269
+ return `${value / 3600}h`;
270
+ if (value % 60 === 0)
271
+ return `${value / 60}m`;
272
+ return `${Math.floor(value / 60)}m ${value % 60}s`;
273
+ }
274
+ //# sourceMappingURL=config-schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-schema.js","sourceRoot":"","sources":["../src/config-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAC5E,OAAO,EAAE,YAAY,EAAkB,MAAM,aAAa,CAAA;AAU1D,MAAM,CAAC,MAAM,aAAa,GAAwD;IAChF;QACE,EAAE,EAAE,WAAW;QACf,KAAK,EAAE,sBAAsB;QAC7B,KAAK,EAAE,+FAA+F;KACvG;IACD;QACE,EAAE,EAAE,UAAU;QACd,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,mFAAmF;KAC3F;IACD;QACE,EAAE,EAAE,SAAS;QACb,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,iGAAiG;KACzG;IACD;QACE,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,6EAA6E;KACrF;CACF,CAAA;AA8BD,MAAM,IAAI,GAAkD;IAC1D,iBAAiB,EAAE;QACjB,KAAK,EAAE,8BAA8B;QACrC,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,mEAAmE;QAC5E,MAAM,EACJ,2RAA2R;QAC7R,OAAO,EAAE,MAAM;KAChB;IACD,YAAY,EAAE;QACZ,KAAK,EAAE,+BAA+B;QACtC,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,SAAS;QACf,OAAO,EAAE,wDAAwD;QACjE,MAAM,EACJ,wdAAwd;QAC1d,OAAO,EAAE,MAAM;KAChB;IACD,kBAAkB,EAAE;QAClB,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,uEAAuE;QAChF,MAAM,EACJ,6PAA6P;QAC/P,OAAO,EAAE,KAAK;KACf;IACD,iBAAiB,EAAE;QACjB,KAAK,EAAE,sBAAsB;QAC7B,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,0EAA0E;QACnF,MAAM,EACJ,sXAAsX;QACxX,OAAO,EAAE,KAAK;KACf;IACD,0BAA0B,EAAE;QAC1B,KAAK,EAAE,yBAAyB;QAChC,KAAK,EAAE,WAAW;QAClB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,0DAA0D;QACnE,MAAM,EACJ,8YAA8Y;QAChZ,OAAO,EAAE,KAAK;KACf;IAED,KAAK,EAAE;QACL,KAAK,EAAE,OAAO;QACd,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,UAAU;QACnB,WAAW,EAAE;YACX,OAAO,EAAE,gCAAgC;YACzC,IAAI,EAAE,sBAAsB;YAC5B,SAAS,EAAE,wCAAwC;YACnD,KAAK,EAAE,yBAAyB;YAChC,IAAI,EAAE,kCAAkC;SACzC;QACD,OAAO,EAAE,4CAA4C;QACrD,MAAM,EACJ,oTAAoT;QACtT,UAAU,EAAE,+CAA+C;KAC5D;IACD,kBAAkB,EAAE;QAClB,KAAK,EAAE,oBAAoB;QAC3B,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,mBAAmB;QAC5B,WAAW,EAAE;YACX,OAAO,EAAE,mDAAmD;YAC5D,MAAM,EAAE,sCAAsC;YAC9C,cAAc,EAAE,+BAA+B;SAChD;QACD,OAAO,EAAE,+CAA+C;QACxD,MAAM,EACJ,2SAA2S;QAC7S,UAAU,EAAE,+CAA+C;KAC5D;IACD,OAAO,EAAE;QACP,KAAK,EAAE,iBAAiB;QACxB,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,MAAM;QACZ,OAAO,EAAE,0CAA0C;QACnD,MAAM,EACJ,wMAAwM;QAC1M,UAAU,EAAE,+BAA+B;QAC3C,OAAO,EAAE,uBAAuB;KACjC;IACD,WAAW,EAAE;QACX,KAAK,EAAE,iBAAiB;QACxB,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,+DAA+D;QACxE,MAAM,EACJ,+NAA+N;QACjO,OAAO,EAAE,OAAO;KACjB;IACD,YAAY,EAAE;QACZ,KAAK,EAAE,mBAAmB;QAC1B,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,SAAS;QACf,IAAI,EAAE,GAAG;QACT,OAAO,EAAE,iEAAiE;QAC1E,MAAM,EACJ,oSAAoS;QACtS,OAAO,EAAE,IAAI;KACd;IACD,YAAY,EAAE;QACZ,KAAK,EAAE,cAAc;QACrB,KAAK,EAAE,UAAU;QACjB,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,qDAAqD;QAC9D,MAAM,EACJ,qVAAqV;QACvV,UAAU,EAAE,qCAAqC;QACjD,QAAQ,EAAE,IAAI;KACf;IAED,OAAO,EAAE;QACP,KAAK,EAAE,YAAY;QACnB,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,yCAAyC;QAClD,MAAM,EACJ,2OAA2O;QAC7O,UAAU,EAAE,wCAAwC;QACpD,OAAO,EAAE,QAAQ;KAClB;IACD,eAAe,EAAE;QACf,KAAK,EAAE,wBAAwB;QAC/B,KAAK,EAAE,SAAS;QAChB,IAAI,EAAE,QAAQ;QACd,OAAO,EAAE,gEAAgE;QACzE,MAAM,EACJ,0RAA0R;QAC5R,UAAU,EAAE,6CAA6C;QACzD,OAAO,EAAE,qCAAqC;KAC/C;IAED,QAAQ,EAAE;QACR,KAAK,EAAE,aAAa;QACpB,KAAK,EAAE,YAAY;QACnB,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,6CAA6C;QACtD,MAAM,EACJ,kSAAkS;QACpS,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,yBAAyB;KACnC;CACF,CAAA;AAED,MAAM,UAAU,UAAU,CAAC,GAAc;IACvC,OAAO,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;AAC9B,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,iBAAiB,CAAC,KAAkB;IAClD,OAAQ,MAAM,CAAC,IAAI,CAAC,IAAI,CAAiB;SACtC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC;SAC1C,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAA;AAClC,CAAC;AAED,oEAAoE;AACpE,MAAM,UAAU,UAAU,CAAC,GAAc;IACvC,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAA;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,EAAE,CAAA;IAC5B,OAAO,GAAG,MAAM,CAAC,GAAG,GAAG,IAAI,IAAI,MAAM,CAAC,GAAG,GAAG,IAAI,EAAE,CAAA;AACpD,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,cAAc,CAAC,GAAc;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,SAAS;YACZ,OAAO,eAAe,CAAA;QACxB,KAAK,MAAM;YACT,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxC,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;YAC9B,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,uBAAuB,MAAM,EAAE,CAAA;QAC7E,CAAC;QACD,KAAK,MAAM;YACT,OAAO,wBAAwB,CAAA;QACjC,KAAK,KAAK;YACR,OAAO,OAAO,CAAA;QAChB,KAAK,QAAQ;YACX,OAAO,UAAU,CAAA;IACrB,CAAC;AACH,CAAC;AAED,kDAAkD;AAClD,MAAM,UAAU,WAAW,CAAC,GAAc,EAAE,KAAc;IACxD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAA;IACtB,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC,UAAU,IAAI,SAAS,CAAA;IAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,IAAI,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACvG,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;IAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,CAAA;IACjG,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;AACtB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAc;IAC3C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IACjE,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IACnE,MAAM,SAAS,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IACrC,IAAI,SAAS,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;IAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,CAAA;IAC5C,MAAM,MAAM,GAA2B;QACrC,OAAO,EAAE,cAAc;QACvB,eAAe,EAAE,yBAAyB;QAC1C,OAAO,EAAE,uBAAuB;QAChC,MAAM,EAAE,cAAc;KACvB,CAAA;IACD,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;AACnF,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,IAAY;IACxD,IAAI,IAAI,KAAK,GAAG;QAAE,OAAO,GAAG,KAAK,GAAG,IAAI,EAAE,CAAA;IAC1C,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,kBAAkB,CAAA;IAC1C,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,GAAG,KAAK,GAAG,CAAA;IAClC,IAAI,KAAK,GAAG,KAAK,KAAK,CAAC;QAAE,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,CAAA;IACnD,IAAI,KAAK,GAAG,IAAI,KAAK,CAAC;QAAE,OAAO,GAAG,KAAK,GAAG,IAAI,GAAG,CAAA;IACjD,IAAI,KAAK,GAAG,EAAE,KAAK,CAAC;QAAE,OAAO,GAAG,KAAK,GAAG,EAAE,GAAG,CAAA;IAC7C,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC,KAAK,KAAK,GAAG,EAAE,GAAG,CAAA;AACpD,CAAC"}
package/dist/hooks.d.ts CHANGED
@@ -32,19 +32,26 @@ export interface HookEnvelope {
32
32
  export interface SessionState {
33
33
  /** Epoch ms of the user's last prompt in this session — our presence signal. */
34
34
  last_prompt_at?: number;
35
- /** Question registered by `notifai ask`, awaiting the turn to end. */
36
- pending?: PendingQuestion;
35
+ /**
36
+ * Questions registered by `notifai ask`, in registration order, each
37
+ * awaiting escalation or its answer. A list, deliberately: registering a
38
+ * question never ends an earlier one. Superseding is reply semantics — a
39
+ * later reply corrects an earlier reply to the same question — never
40
+ * question semantics; the single-slot model silently discarded a live
41
+ * question the moment a second was registered (2026-08-09).
42
+ */
43
+ pending?: PendingQuestion[];
37
44
  /**
38
45
  * Questions that have been delivered to the user's devices and are now dead,
39
46
  * but whose retirement has not been confirmed yet.
40
47
  *
41
48
  * A retirement needs a network call and the moment we learn a question is
42
- * dead is not always a moment we can make one — `notifai ask` supersedes the
43
- * previous question from a bare shell command, and the machine may be
44
- * offline. Dropping the ids there is how a delivered question becomes
45
- * permanently unretirable, so they are parked here instead and every later
46
- * hook with a client drains them. Retirement is idempotent, so a duplicate
47
- * attempt costs nothing and a missed one costs a stale notification for ever.
49
+ * dead is not always a moment we can make one — the user's return to the
50
+ * terminal is observed by a bare hook, and the machine may be offline.
51
+ * Dropping the ids there is how a delivered question becomes permanently
52
+ * unretirable, so they are parked here instead and every later hook with a
53
+ * client drains them. Retirement is idempotent, so a duplicate attempt
54
+ * costs nothing and a missed one costs a stale notification for ever.
48
55
  */
49
56
  retiring?: RetiringQuestion[];
50
57
  /** Tracks bounded Stop continuations so a follow-up ask is delivered once. */
@@ -250,18 +257,23 @@ export declare function handleStop(ctx: HookContext, envelope: HookEnvelope): Pr
250
257
  */
251
258
  export declare function handleSessionEnd(env: NodeJS.ProcessEnv, envelope: HookEnvelope, now?: number): HookOutcome;
252
259
  /**
253
- * One session holds one live question, so registering a second one ends the
254
- * first.
255
- *
256
- * This used to replace `pending` wholesale, which silently discarded the
257
- * `request_id` and `collapse_key` of a question already delivered to the user's
258
- * devices. Nothing else knows those ids, so the notification became
259
- * unretirable — it sat on the lock screen for ever asking a question no answer
260
- * could reach. That is where the stale pile-up came from.
260
+ * More pending questions than this means an agent is looping, not asking.
261
+ * Related questions belong in one `ask --form` (the wire carries up to four
262
+ * questions per notification); four separate pushes is already a lot of lock
263
+ * screen.
264
+ */
265
+ export declare const MAX_PENDING_QUESTIONS = 4;
266
+ /**
267
+ * A session may hold several registered questions at once: a new `ask` never
268
+ * ends an earlier one. Each reaches the user as its own notification and is
269
+ * answerable independently. Superseding is reply semantics — a later reply
270
+ * corrects an earlier reply to the same question (that correction model lives
271
+ * server-side) — never question semantics: the old single-slot model silently
272
+ * discarded a question the moment a second was registered, and the user was
273
+ * pointed at a notification that never existed (2026-08-09).
261
274
  *
262
- * Supersession is keyed on the session, not the project: several agents may be
263
- * running in one project at once, and one agent's new question killing another
264
- * agent's live one would be worse than the staleness this fixes.
275
+ * The queue is keyed on the session, not the project: several agents may be
276
+ * running in one project at once, and their questions must never interfere.
265
277
  */
266
278
  export declare function registerQuestion(sessionId: string, env: NodeJS.ProcessEnv, question: PendingQuestion, now?: number): void;
267
279
  /** Parses hook JSON from stdin, tolerating an empty or malformed body. */