@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.
Files changed (80) hide show
  1. package/README.md +20 -4
  2. package/dist/atomic-file.d.ts +4 -0
  3. package/dist/atomic-file.js +60 -10
  4. package/dist/atomic-file.js.map +1 -1
  5. package/dist/client.d.ts +25 -2
  6. package/dist/client.js +45 -1
  7. package/dist/client.js.map +1 -1
  8. package/dist/commands.d.ts +56 -7
  9. package/dist/commands.js +980 -318
  10. package/dist/commands.js.map +1 -1
  11. package/dist/config-schema.d.ts +1 -1
  12. package/dist/config-schema.js +46 -8
  13. package/dist/config-schema.js.map +1 -1
  14. package/dist/config.d.ts +38 -3
  15. package/dist/config.js +61 -14
  16. package/dist/config.js.map +1 -1
  17. package/dist/file-lock.d.ts +26 -0
  18. package/dist/file-lock.js +268 -0
  19. package/dist/file-lock.js.map +1 -0
  20. package/dist/harnesses.d.ts +28 -0
  21. package/dist/harnesses.js +40 -0
  22. package/dist/harnesses.js.map +1 -0
  23. package/dist/hook-adapter.d.ts +27 -0
  24. package/dist/hook-adapter.js +188 -0
  25. package/dist/hook-adapter.js.map +1 -0
  26. package/dist/hook-input.d.ts +10 -0
  27. package/dist/hook-input.js +64 -0
  28. package/dist/hook-input.js.map +1 -0
  29. package/dist/hooks.d.ts +66 -10
  30. package/dist/hooks.js +751 -276
  31. package/dist/hooks.js.map +1 -1
  32. package/dist/install-hooks.d.ts +40 -16
  33. package/dist/install-hooks.js +217 -73
  34. package/dist/install-hooks.js.map +1 -1
  35. package/dist/interactive.d.ts +1 -1
  36. package/dist/interactive.js +3 -3
  37. package/dist/interactive.js.map +1 -1
  38. package/dist/logging.d.ts +142 -0
  39. package/dist/logging.js +533 -0
  40. package/dist/logging.js.map +1 -0
  41. package/dist/main.js +97 -36
  42. package/dist/main.js.map +1 -1
  43. package/dist/opencode-plugin.d.ts +7 -7
  44. package/dist/opencode-plugin.js +12 -13
  45. package/dist/opencode-plugin.js.map +1 -1
  46. package/dist/ui/config-view.js +3 -0
  47. package/dist/ui/config-view.js.map +1 -1
  48. package/dist/ui/theme.d.ts +1 -1
  49. package/package.json +3 -2
  50. package/src/atomic-file.ts +86 -7
  51. package/src/client.test.ts +12 -0
  52. package/src/client.ts +72 -4
  53. package/src/commands.test.ts +891 -43
  54. package/src/commands.ts +1133 -332
  55. package/src/config-schema.ts +51 -9
  56. package/src/config.test.ts +6 -5
  57. package/src/config.ts +85 -16
  58. package/src/file-lock-process-worker.test.ts +64 -0
  59. package/src/file-lock.test.ts +279 -0
  60. package/src/file-lock.ts +318 -0
  61. package/src/harnesses.test.ts +48 -0
  62. package/src/harnesses.ts +59 -0
  63. package/src/hook-adapter.test.ts +187 -0
  64. package/src/hook-adapter.ts +212 -0
  65. package/src/hook-input.test.ts +39 -0
  66. package/src/hook-input.ts +72 -0
  67. package/src/hooks.test.ts +1318 -240
  68. package/src/hooks.ts +1046 -296
  69. package/src/install-hooks.test.ts +90 -41
  70. package/src/install-hooks.ts +259 -74
  71. package/src/interactive.ts +3 -3
  72. package/src/logging-process-worker.test.ts +39 -0
  73. package/src/logging.test.ts +453 -0
  74. package/src/logging.ts +639 -0
  75. package/src/main.ts +113 -32
  76. package/src/opencode-plugin.ts +15 -16
  77. package/src/session-end-log-lock-worker.test.ts +35 -0
  78. package/src/skill.test.ts +11 -0
  79. package/src/ui/config-view.test.ts +4 -2
  80. 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 so a late device answer is rejected rather than lost. */
61
- closeReplies(requestId: string): Promise<void>
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 limitMs = budgetMs + serverWaitSeconds * 1000
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('POST', `/api/v1/notifications/${encodeURIComponent(requestId)}/replies/close`),
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