@raidiant/notifai 0.4.0 → 0.5.1
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/README.md +3 -4
- package/dist/claude-wake.d.ts +101 -0
- package/dist/claude-wake.js +397 -0
- package/dist/claude-wake.js.map +1 -0
- package/dist/codex-wake.d.ts +84 -0
- package/dist/codex-wake.js +238 -0
- package/dist/codex-wake.js.map +1 -0
- package/dist/commands.d.ts +15 -10
- package/dist/commands.js +321 -88
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +2 -2
- package/dist/config-schema.js +9 -35
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +5 -38
- package/dist/config.js +11 -32
- package/dist/config.js.map +1 -1
- package/dist/harnesses.d.ts +3 -13
- package/dist/harnesses.js +6 -17
- package/dist/harnesses.js.map +1 -1
- package/dist/hooks.d.ts +119 -65
- package/dist/hooks.js +330 -285
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +22 -7
- package/dist/install-hooks.js +43 -28
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +2 -4
- package/dist/interactive.js.map +1 -1
- package/dist/main.js +6 -13
- package/dist/main.js.map +1 -1
- package/dist/release.d.ts +31 -0
- package/dist/release.js +52 -0
- package/dist/release.js.map +1 -0
- package/dist/ui/theme.d.ts +1 -1
- package/package.json +9 -9
- package/src/claude-wake.test.ts +320 -0
- package/src/claude-wake.ts +489 -0
- package/src/codex-wake.test.ts +253 -0
- package/src/codex-wake.ts +288 -0
- package/src/commands.test.ts +393 -40
- package/src/commands.ts +433 -109
- package/src/config-schema.test.ts +3 -3
- package/src/config-schema.ts +11 -40
- package/src/config.test.ts +21 -16
- package/src/config.ts +15 -69
- package/src/harnesses.test.ts +7 -21
- package/src/harnesses.ts +17 -19
- package/src/hooks.test.ts +780 -290
- package/src/hooks.ts +466 -339
- package/src/install-hooks.test.ts +44 -3
- package/src/install-hooks.ts +43 -28
- package/src/interactive.ts +2 -7
- package/src/logging.test.ts +3 -3
- package/src/main.ts +6 -12
- package/src/release.ts +51 -0
- package/src/skill.test.ts +23 -0
- package/src/ui/config-view.test.ts +5 -5
- package/dist/idle.d.ts +0 -16
- package/dist/idle.js +0 -44
- package/dist/idle.js.map +0 -1
- package/src/idle.test.ts +0 -32
- package/src/idle.ts +0 -41
package/dist/hooks.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { LifecycleEndState, NotificationDraftT, QuestionT, ReplyView } from
|
|
|
2
2
|
import { type ApiClient } from './client.js';
|
|
3
3
|
import { type CliConfig } from './config.js';
|
|
4
4
|
import type { Logger } from './logging.js';
|
|
5
|
-
import { type Harness } from './harnesses.js';
|
|
5
|
+
import { type DeliveryRoute, type Harness } from './harnesses.js';
|
|
6
6
|
/**
|
|
7
7
|
* Harness hook handlers.
|
|
8
8
|
*
|
|
@@ -11,10 +11,13 @@ import { type Harness } from './harnesses.js';
|
|
|
11
11
|
* and Codex can continue directly from a turn-end answer. Harnesses without a
|
|
12
12
|
* proven exact-session continuation fail closed at question admission.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* Where the user is standing no longer decides anything here. It used to: the
|
|
15
|
+
* turn-end hook held the terminal for as long as it waited, so waiting was
|
|
16
|
+
* only defensible while the person was demonstrably not at the keyboard. On
|
|
17
|
+
* Claude Code the handler now returns instantly and the waiter runs out of
|
|
18
|
+
* band, and nothing is monopolised in the first place — so the question is
|
|
19
|
+
* simply whether the user wants to be reached, which they answer once in
|
|
20
|
+
* configuration rather than implicitly with every keystroke.
|
|
18
21
|
*/
|
|
19
22
|
/** Fields we read from harness hook JSON. Everything else is passed through. */
|
|
20
23
|
export interface HookEnvelope {
|
|
@@ -57,16 +60,20 @@ export interface SessionState {
|
|
|
57
60
|
* costs nothing and a missed one costs a stale notification for ever.
|
|
58
61
|
*/
|
|
59
62
|
retiring?: RetiringQuestion[];
|
|
60
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Tracks bounded Stop continuations so a follow-up ask is delivered once.
|
|
65
|
+
* `count` is how many answer generations have run consecutively without the
|
|
66
|
+
* user taking a turn themselves; their next prompt starts it over.
|
|
67
|
+
*/
|
|
61
68
|
continuation?: {
|
|
62
69
|
answered_at: number;
|
|
63
70
|
count: number;
|
|
64
71
|
};
|
|
65
72
|
/**
|
|
66
|
-
* A phone answer durably captured but not yet acknowledged
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* the
|
|
73
|
+
* A phone answer durably captured but not yet acknowledged. It stays here
|
|
74
|
+
* until a delivery is acknowledged — by the route's own write, or by the
|
|
75
|
+
* successor Stop of a blocking continuation — so a crash before the answer
|
|
76
|
+
* reaches the harness replays it instead of erasing it.
|
|
70
77
|
*/
|
|
71
78
|
accepted?: AcceptedAnswerDelivery;
|
|
72
79
|
}
|
|
@@ -74,7 +81,20 @@ interface AcceptedAnswerDelivery {
|
|
|
74
81
|
answers: AnsweredPending[];
|
|
75
82
|
remaining: number;
|
|
76
83
|
recorded_at: number;
|
|
77
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Epoch ms when a route's own write handed this answer to the harness, with
|
|
86
|
+
* the route that performed it. Recorded so that "this answer was delivered"
|
|
87
|
+
* is a fact on the journal rather than something inferred from a harness flag
|
|
88
|
+
* that only one route ever sets.
|
|
89
|
+
*/
|
|
90
|
+
delivered_at?: number;
|
|
91
|
+
delivered_route?: string;
|
|
92
|
+
/**
|
|
93
|
+
* How many times this answer has been handed to any route. The route-agnostic
|
|
94
|
+
* loop backstop: bounded by `MAX_CONTINUATION_COUNT` in one place every route
|
|
95
|
+
* passes through.
|
|
96
|
+
*/
|
|
97
|
+
delivery_attempts?: number;
|
|
78
98
|
}
|
|
79
99
|
/** A delivered question awaiting its retirement push. */
|
|
80
100
|
export interface RetiringQuestion {
|
|
@@ -139,33 +159,16 @@ interface PendingSubmissionIntent {
|
|
|
139
159
|
draft: NotificationDraftT;
|
|
140
160
|
owner_deadline_at: number;
|
|
141
161
|
}
|
|
142
|
-
export type GraceOutcome =
|
|
143
|
-
/** The window elapsed with the user still gone; escalate. */
|
|
144
|
-
'absent'
|
|
145
|
-
/** The user touched the machine; the terminal is theirs. */
|
|
146
|
-
| 'user-returned'
|
|
147
|
-
/** No idle source, so waiting would hold a terminal we cannot monitor. */
|
|
148
|
-
| 'no-signal';
|
|
149
162
|
/**
|
|
150
|
-
* The
|
|
151
|
-
*
|
|
152
|
-
* devices.
|
|
153
|
-
*
|
|
154
|
-
* Holding a blocking Stop hook open is normally hostile — while it blocks, the
|
|
155
|
-
* harness cannot show its prompt either, so a user wanting to answer locally is
|
|
156
|
-
* locked out. What makes it safe is that the wait is abandoned the moment the
|
|
157
|
-
* user touches the keyboard or mouse. The block therefore only ever persists
|
|
158
|
-
* while they are demonstrably not using the machine, which costs them nothing.
|
|
163
|
+
* The waiter's whole wall clock: the terminal-first timer, submission, and the
|
|
164
|
+
* wait for an answer, from the moment the turn-end hook starts.
|
|
159
165
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* want the question to reach them whether or not they are at the keyboard, so
|
|
165
|
-
* there is nothing to watch for and this becomes what its name always claimed —
|
|
166
|
-
* a plain timer. It then works on machines with no idle source too.
|
|
166
|
+
* One plain ceiling, deliberately not a budget carved into reserves. The
|
|
167
|
+
* reserves existed to fit every phase inside a *held* turn; on Claude Code the
|
|
168
|
+
* turn is not held any more, and on a host that still holds one the useful
|
|
169
|
+
* question is only how long that host is willing to wait in total.
|
|
167
170
|
*/
|
|
168
|
-
export declare
|
|
171
|
+
export declare const WAITER_CEILING_SECONDS = 480;
|
|
169
172
|
export interface HookOutcome {
|
|
170
173
|
/** Written to stdout verbatim — the harness parses this as the decision. */
|
|
171
174
|
stdout?: string;
|
|
@@ -174,6 +177,54 @@ export interface HookOutcome {
|
|
|
174
177
|
/** Structured lifecycle detail that belongs on hook.end without user text. */
|
|
175
178
|
log?: Record<string, unknown>;
|
|
176
179
|
}
|
|
180
|
+
/** An accepted continuation ready for whichever host owns the last meter. */
|
|
181
|
+
export interface ContinuationEvent {
|
|
182
|
+
context: string;
|
|
183
|
+
answers: number;
|
|
184
|
+
remaining: number;
|
|
185
|
+
request_ids: string[];
|
|
186
|
+
journal_recorded_at: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* How much a route's own return proves about where the answer ended up.
|
|
190
|
+
*
|
|
191
|
+
* Acknowledgement belongs to the delivery, not to the harness envelope: routes
|
|
192
|
+
* end in different places, so no single field on a hook payload can speak for
|
|
193
|
+
* all of them. `stop_hook_active` is the harness confirming that a *blocking
|
|
194
|
+
* continuation* was admitted; it stays false for ever on a route that starts a
|
|
195
|
+
* brand-new turn instead of continuing this one, so a journal keyed to it alone
|
|
196
|
+
* never settles and every later turn-end redelivers the same answer.
|
|
197
|
+
*
|
|
198
|
+
* - `delivered` — the route completed a write to the harness itself (an inbox
|
|
199
|
+
* socket, a cold resume). Delivery is not consumption: the write proves the
|
|
200
|
+
* harness accepted the message, never that the model acted on it. But nothing
|
|
201
|
+
* later will ever prove more, and redelivering an answer without end is
|
|
202
|
+
* strictly worse than settling on the write, so the journal settles here.
|
|
203
|
+
* - `stdout` — the answer is this process's stdout, which the harness reads only
|
|
204
|
+
* after this process exits. Nothing this process can write is proof, so the
|
|
205
|
+
* journal waits for the successor Stop's `stop_hook_active`, and a crash
|
|
206
|
+
* before stdout replays the answer instead of losing it.
|
|
207
|
+
* - `held` — the route handed nothing over; the journal replays the answer.
|
|
208
|
+
*/
|
|
209
|
+
export type DeliveryAcknowledgement = 'delivered' | 'stdout' | 'held';
|
|
210
|
+
/** What a route returns: the hook's outcome plus what the attempt proved. */
|
|
211
|
+
export interface DeliveryOutcome {
|
|
212
|
+
stdout?: string;
|
|
213
|
+
notes?: string[];
|
|
214
|
+
log?: Record<string, unknown>;
|
|
215
|
+
acknowledgement: DeliveryAcknowledgement;
|
|
216
|
+
}
|
|
217
|
+
/** Host adapter injected into the waiter; no route is implemented by the waiter. */
|
|
218
|
+
export interface EscalationDeliveryRoute {
|
|
219
|
+
kind: Exclude<DeliveryRoute, 'unsupported'>;
|
|
220
|
+
deliver(event: ContinuationEvent): Promise<DeliveryOutcome>;
|
|
221
|
+
}
|
|
222
|
+
export interface EscalationWaiterOptions {
|
|
223
|
+
sessionId: string;
|
|
224
|
+
envelope: HookEnvelope;
|
|
225
|
+
route: EscalationDeliveryRoute;
|
|
226
|
+
processDeadlineAt?: number;
|
|
227
|
+
}
|
|
177
228
|
export declare function readSessionState(sessionId: string, env: NodeJS.ProcessEnv): SessionState;
|
|
178
229
|
export declare function writeSessionState(sessionId: string, env: NodeJS.ProcessEnv, state: SessionState): void;
|
|
179
230
|
export declare function clearSessionState(sessionId: string, env: NodeJS.ProcessEnv): void;
|
|
@@ -192,42 +243,25 @@ export interface ProjectSessionPointer {
|
|
|
192
243
|
* into state nothing is watching.
|
|
193
244
|
*/
|
|
194
245
|
export declare function readProjectSession(cwd: string, env: NodeJS.ProcessEnv, now: number, maxAgeMs?: number): string | null;
|
|
195
|
-
|
|
196
|
-
export
|
|
246
|
+
/** A live pointer keeps when it was last written, so callers can rank by it. */
|
|
247
|
+
export interface LiveProjectSessionPointer extends ProjectSessionPointer {
|
|
248
|
+
updatedAt: number;
|
|
249
|
+
}
|
|
197
250
|
/**
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* OS idle time answers that directly, so where it exists it decides alone.
|
|
201
|
-
* Silence since the user's last prompt is only ever a proxy for it, and a poor
|
|
202
|
-
* one — it is wrong in both directions:
|
|
203
|
-
*
|
|
204
|
-
* - Too long: it counts the agent's own turn, so a user watching a build was
|
|
205
|
-
* read as absent and had the question pushed at them.
|
|
206
|
-
* - Too short: a session that has just been spawned always has a fresh
|
|
207
|
-
* prompt, so its FIRST question could never escalate however long its
|
|
208
|
-
* owner had been gone. That is the "kick off some agents and
|
|
209
|
-
* walk away" case this feature mainly exists for, and requiring both
|
|
210
|
-
* signals to agree is what broke it.
|
|
211
|
-
*
|
|
212
|
-
* The proxy therefore survives only as the fallback for machines with no idle
|
|
213
|
-
* source. There, never having seen a prompt is not evidence of absence — it is
|
|
214
|
-
* a missing `UserPromptSubmit` hook — so it resolves to "present".
|
|
215
|
-
*
|
|
216
|
-
* Answering from a companion device does not make the user present; only touching this
|
|
217
|
-
* machine does. Answering on a device is evidence of being away from it.
|
|
251
|
+
* Every session that has fired a hook in this directory recently, newest first.
|
|
218
252
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
253
|
+
* This is the only local record of which harness sessions are actually alive
|
|
254
|
+
* here, which makes it the tiebreaker when the environment alone cannot say
|
|
255
|
+
* which harness owns the current shell.
|
|
222
256
|
*/
|
|
223
|
-
export declare function
|
|
257
|
+
export declare function readLiveProjectSessionPointers(cwd: string, env: NodeJS.ProcessEnv, now: number, maxAgeMs?: number): LiveProjectSessionPointer[];
|
|
258
|
+
export declare function readProjectSessionPointer(cwd: string, env: NodeJS.ProcessEnv, now: number, maxAgeMs?: number): ProjectSessionPointer | null;
|
|
259
|
+
export declare function readMatchingProjectSessionPointer(cwd: string, env: NodeJS.ProcessEnv, now: number, sessionId: string, harness: HookHarness, maxAgeMs?: number): ProjectSessionPointer | null;
|
|
224
260
|
export interface HookContext {
|
|
225
261
|
client: ApiClient;
|
|
226
262
|
config: CliConfig;
|
|
227
263
|
env: NodeJS.ProcessEnv;
|
|
228
264
|
now: () => number;
|
|
229
|
-
/** Seconds since the last keyboard/mouse event, or null if unknowable. */
|
|
230
|
-
idleSeconds: () => number | null;
|
|
231
265
|
/** Injected so tests advance a virtual clock instead of sleeping. */
|
|
232
266
|
sleep: (milliseconds: number) => Promise<void>;
|
|
233
267
|
/** Bounded wait for the first reply; injected so tests do not sleep. */
|
|
@@ -254,7 +288,7 @@ export interface HookContext {
|
|
|
254
288
|
* the user sees in the terminal — but it is free to be rewritten, and a log a
|
|
255
289
|
* filter cannot rely on is a log nobody filters.
|
|
256
290
|
*/
|
|
257
|
-
export type GateReason = 'no-session' | 'no-question' | 'answered' | 'already-asked' | 'continuation-repeat' | 'continuation-limit' | '
|
|
291
|
+
export type GateReason = 'no-session' | 'no-question' | 'answered' | 'already-asked' | 'continuation-repeat' | 'continuation-limit' | 'delivery-limit' | 'notifications-off' | 'claimed-elsewhere' | 'no-devices' | 'proceeding';
|
|
258
292
|
export type HookHarness = Harness;
|
|
259
293
|
/**
|
|
260
294
|
* Everything retirement needs. Narrower than a HookContext on purpose:
|
|
@@ -283,6 +317,15 @@ export declare function orphanRetirements(env: NodeJS.ProcessEnv, entries: Retir
|
|
|
283
317
|
* next holder of a client; entries past the TTL are dropped as already dead.
|
|
284
318
|
*/
|
|
285
319
|
export declare function drainOrphanRetirements(ctx: RetireDeps, env: NodeJS.ProcessEnv, now: number): Promise<string[]>;
|
|
320
|
+
/**
|
|
321
|
+
* The one bound on answer deliveries, for every route.
|
|
322
|
+
*
|
|
323
|
+
* It caps two things that both used to be able to run away: how many times a
|
|
324
|
+
* single accepted answer may be handed to a route before the journal gives up
|
|
325
|
+
* on it, and how many answer-driven continuation generations one chain may
|
|
326
|
+
* produce.
|
|
327
|
+
*/
|
|
328
|
+
export declare const MAX_CONTINUATION_COUNT = 3;
|
|
286
329
|
/** One answered registered question, with everything the agent needs to read it. */
|
|
287
330
|
interface AnsweredPending {
|
|
288
331
|
pending: PendingQuestion;
|
|
@@ -301,7 +344,18 @@ export declare function handleUserPromptSubmit(ctx: HookContext, envelope: HookE
|
|
|
301
344
|
* `notifai ask`. Guessing from the last assistant message was the alternative
|
|
302
345
|
* and it is not worth it: a false positive here hijacks the terminal.
|
|
303
346
|
*/
|
|
304
|
-
export declare function handleStop(ctx: HookContext, envelope: HookEnvelope, processDeadlineAt?: number): Promise<HookOutcome>;
|
|
347
|
+
export declare function handleStop(ctx: HookContext, envelope: HookEnvelope, processDeadlineAt?: number, route?: EscalationDeliveryRoute): Promise<HookOutcome>;
|
|
348
|
+
/**
|
|
349
|
+
* Own one registered question pipeline from its terminal-first timer through
|
|
350
|
+
* its final delivery or retirement. Hosts differ only in the injected route.
|
|
351
|
+
*/
|
|
352
|
+
export declare function runEscalationWaiter(ctx: HookContext, options: EscalationWaiterOptions): Promise<HookOutcome>;
|
|
353
|
+
/**
|
|
354
|
+
* The blocking Stop hook's own last meter: the answer becomes this process's
|
|
355
|
+
* stdout, and the harness admits it as a new user turn. Exported because a
|
|
356
|
+
* host adapter that has other routes still falls back to this one.
|
|
357
|
+
*/
|
|
358
|
+
export declare function hookContinuationRoute(): EscalationDeliveryRoute;
|
|
305
359
|
/**
|
|
306
360
|
* Claude Code gives SessionEnd hooks a 1.5-second shared budget and Codex 1
|
|
307
361
|
* second, so this cannot make a network call. It drops the local marker — but
|