@raidiant/notifai 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -4
- package/dist/atomic-file.d.ts +4 -0
- package/dist/atomic-file.js +60 -10
- package/dist/atomic-file.js.map +1 -1
- package/dist/client.d.ts +25 -2
- package/dist/client.js +45 -1
- package/dist/client.js.map +1 -1
- package/dist/commands.d.ts +56 -7
- package/dist/commands.js +980 -318
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +46 -8
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +38 -3
- package/dist/config.js +61 -14
- package/dist/config.js.map +1 -1
- package/dist/file-lock.d.ts +26 -0
- package/dist/file-lock.js +268 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/harnesses.d.ts +28 -0
- package/dist/harnesses.js +40 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/hook-adapter.d.ts +27 -0
- package/dist/hook-adapter.js +188 -0
- package/dist/hook-adapter.js.map +1 -0
- package/dist/hook-input.d.ts +10 -0
- package/dist/hook-input.js +64 -0
- package/dist/hook-input.js.map +1 -0
- package/dist/hooks.d.ts +66 -10
- package/dist/hooks.js +751 -276
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +40 -16
- package/dist/install-hooks.js +217 -73
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +3 -3
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +142 -0
- package/dist/logging.js +533 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.js +97 -36
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.d.ts +7 -7
- package/dist/opencode-plugin.js +12 -13
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/ui/config-view.js +3 -0
- package/dist/ui/config-view.js.map +1 -1
- package/dist/ui/theme.d.ts +1 -1
- package/package.json +3 -2
- package/src/atomic-file.ts +86 -7
- package/src/client.test.ts +12 -0
- package/src/client.ts +72 -4
- package/src/commands.test.ts +891 -43
- package/src/commands.ts +1133 -332
- package/src/config-schema.ts +51 -9
- package/src/config.test.ts +6 -5
- package/src/config.ts +85 -16
- package/src/file-lock-process-worker.test.ts +64 -0
- package/src/file-lock.test.ts +279 -0
- package/src/file-lock.ts +318 -0
- package/src/harnesses.test.ts +48 -0
- package/src/harnesses.ts +59 -0
- package/src/hook-adapter.test.ts +187 -0
- package/src/hook-adapter.ts +212 -0
- package/src/hook-input.test.ts +39 -0
- package/src/hook-input.ts +72 -0
- package/src/hooks.test.ts +1318 -240
- package/src/hooks.ts +1046 -296
- package/src/install-hooks.test.ts +90 -41
- package/src/install-hooks.ts +259 -74
- package/src/interactive.ts +3 -3
- package/src/logging-process-worker.test.ts +39 -0
- package/src/logging.test.ts +453 -0
- package/src/logging.ts +639 -0
- package/src/main.ts +113 -32
- package/src/opencode-plugin.ts +15 -16
- package/src/session-end-log-lock-worker.test.ts +35 -0
- package/src/skill.test.ts +11 -0
- package/src/ui/config-view.test.ts +4 -2
- package/src/ui/config-view.ts +3 -0
package/src/client.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
SubmissionReceipt,
|
|
14
14
|
SubmitNotificationRequestT,
|
|
15
15
|
} from '@raidiant/notifai-protocol'
|
|
16
|
+
import type { Logger } from './logging.js'
|
|
16
17
|
|
|
17
18
|
export class ApiCallError extends Error {
|
|
18
19
|
constructor(
|
|
@@ -28,6 +29,18 @@ export class ApiCallError extends Error {
|
|
|
28
29
|
|
|
29
30
|
export class NetworkError extends Error {}
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Whether a replies-poll failure is temporary while the durable request stays
|
|
34
|
+
* live. Transport faults, throttling, request timeouts, and server failures
|
|
35
|
+
* can recover on a later poll; client errors cannot be repaired by retrying
|
|
36
|
+
* the same authenticated request.
|
|
37
|
+
*/
|
|
38
|
+
export function isRetryableReplyPollError(err: unknown): boolean {
|
|
39
|
+
if (err instanceof NetworkError) return true
|
|
40
|
+
if (!(err instanceof ApiCallError)) return false
|
|
41
|
+
return err.status >= 500 || err.status === 429 || err.status === 408
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
/**
|
|
32
45
|
* A deadline that fired reads as an abort, which says nothing useful on its
|
|
33
46
|
* own. Naming the timeout is what lets someone tell a hung server apart from a
|
|
@@ -57,8 +70,8 @@ export interface ApiClient {
|
|
|
57
70
|
requestId: string,
|
|
58
71
|
options: { waitSeconds: number; afterSeq: number },
|
|
59
72
|
): Promise<ListRepliesResponse>
|
|
60
|
-
/** Retire a question
|
|
61
|
-
closeReplies(requestId: string): Promise<
|
|
73
|
+
/** Retire a question and return the replies committed before the close fence. */
|
|
74
|
+
closeReplies(requestId: string): Promise<ListRepliesResponse>
|
|
62
75
|
createMediaUpload(body: CreateMediaUploadRequestT): Promise<CreateMediaUploadResponse>
|
|
63
76
|
uploadMedia(grant: CreateMediaUploadResponse, bytes: Uint8Array): Promise<void>
|
|
64
77
|
health(): Promise<boolean>
|
|
@@ -71,6 +84,21 @@ export interface ClientOptions {
|
|
|
71
84
|
* whole network path stays inside the budget the harness allows them.
|
|
72
85
|
*/
|
|
73
86
|
timeoutMs?: number
|
|
87
|
+
/**
|
|
88
|
+
* Absolute wall-clock ceiling for the caller's whole operation. This is
|
|
89
|
+
* intentionally separate from `timeoutMs`: a late long poll must not get a
|
|
90
|
+
* fresh per-request allowance after its owning harness hook is nearly done.
|
|
91
|
+
*/
|
|
92
|
+
deadlineAt?: number
|
|
93
|
+
/** Test seam for `deadlineAt`; production uses the wall clock. */
|
|
94
|
+
now?: () => number
|
|
95
|
+
/**
|
|
96
|
+
* Records each call locally. Worth having because the two failures that cost
|
|
97
|
+
* the most time — a server older than this CLI, and a network path that only
|
|
98
|
+
* fails inside a hook — both look identical from outside and are told apart
|
|
99
|
+
* instantly by a status code and a duration.
|
|
100
|
+
*/
|
|
101
|
+
logger?: Pick<Logger, 'debug' | 'error'>
|
|
74
102
|
}
|
|
75
103
|
|
|
76
104
|
/** Generous enough for a slow link, short enough that nothing hangs for ever. */
|
|
@@ -83,6 +111,8 @@ export function createClient(
|
|
|
83
111
|
): ApiClient {
|
|
84
112
|
const root = baseUrl.replace(/\/$/, '')
|
|
85
113
|
const budgetMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
114
|
+
const now = options.now ?? Date.now
|
|
115
|
+
const logger = options.logger
|
|
86
116
|
|
|
87
117
|
async function call<T>(
|
|
88
118
|
method: string,
|
|
@@ -90,12 +120,47 @@ export function createClient(
|
|
|
90
120
|
body?: unknown,
|
|
91
121
|
/** Seconds the server was asked to hold the connection open. */
|
|
92
122
|
serverWaitSeconds = 0,
|
|
123
|
+
): Promise<T> {
|
|
124
|
+
const startedAt = Date.now()
|
|
125
|
+
try {
|
|
126
|
+
const result = await callOnce<T>(method, apiPath, body, serverWaitSeconds)
|
|
127
|
+
logger?.debug('http.call', {
|
|
128
|
+
method,
|
|
129
|
+
path: apiPath,
|
|
130
|
+
ok: true,
|
|
131
|
+
duration_ms: Date.now() - startedAt,
|
|
132
|
+
})
|
|
133
|
+
return result
|
|
134
|
+
} catch (err) {
|
|
135
|
+
logger?.error('http.call', {
|
|
136
|
+
method,
|
|
137
|
+
path: apiPath,
|
|
138
|
+
ok: false,
|
|
139
|
+
duration_ms: Date.now() - startedAt,
|
|
140
|
+
...(err instanceof ApiCallError
|
|
141
|
+
? { status: err.status, code: err.code, message: err.message, details: err.details }
|
|
142
|
+
: { message: String(err) }),
|
|
143
|
+
})
|
|
144
|
+
throw err
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function callOnce<T>(
|
|
149
|
+
method: string,
|
|
150
|
+
apiPath: string,
|
|
151
|
+
body?: unknown,
|
|
152
|
+
serverWaitSeconds = 0,
|
|
93
153
|
): Promise<T> {
|
|
94
154
|
// Without this, a server that accepts the connection and then never answers
|
|
95
155
|
// hangs until the harness kills the whole hook — and the reply-poll deadline
|
|
96
156
|
// cannot interrupt an individual fetch. The signal covers the
|
|
97
157
|
// body read too, not just the response headers.
|
|
98
|
-
const
|
|
158
|
+
const requestBudgetMs = budgetMs + serverWaitSeconds * 1000
|
|
159
|
+
const remainingMs =
|
|
160
|
+
options.deadlineAt === undefined
|
|
161
|
+
? requestBudgetMs
|
|
162
|
+
: Math.max(1, options.deadlineAt - now())
|
|
163
|
+
const limitMs = Math.max(1, Math.min(requestBudgetMs, remainingMs))
|
|
99
164
|
const signal = AbortSignal.timeout(limitMs)
|
|
100
165
|
let response: Response
|
|
101
166
|
try {
|
|
@@ -154,7 +219,10 @@ export function createClient(
|
|
|
154
219
|
waitSeconds,
|
|
155
220
|
),
|
|
156
221
|
closeReplies: (requestId) =>
|
|
157
|
-
call(
|
|
222
|
+
call<ListRepliesResponse>(
|
|
223
|
+
'POST',
|
|
224
|
+
`/api/v1/notifications/${encodeURIComponent(requestId)}/replies/close`,
|
|
225
|
+
),
|
|
158
226
|
createMediaUpload: (body) => call('POST', '/api/v1/media', body),
|
|
159
227
|
uploadMedia: async (grant, bytes) => {
|
|
160
228
|
let response: Response
|