@teamscala/service-boot 0.23.366 → 0.23.368
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/package.json
CHANGED
|
@@ -51,7 +51,6 @@ import { loadRegistryConfig } from "@teamscala/db/registry/load-config";
|
|
|
51
51
|
import { recordEvent } from "@teamscala/event-log/record-event";
|
|
52
52
|
import { getAppLogger } from "@teamscala/logger/app-loggers";
|
|
53
53
|
import { pgBossHandlerRegistry } from "../pg-boss-handler-registry.ts";
|
|
54
|
-
import { getServerClient } from "../page-server-orpc/server-client.ts";
|
|
55
54
|
import {
|
|
56
55
|
composeEscalationDescription,
|
|
57
56
|
composeEscalationMessage,
|
|
@@ -83,8 +82,11 @@ const FaultEscalationConfigSchema = v.object({
|
|
|
83
82
|
),
|
|
84
83
|
resolve_after_minutes: v.number(),
|
|
85
84
|
update_after_minutes: v.number(),
|
|
86
|
-
/** The
|
|
87
|
-
|
|
85
|
+
/** The orchestrator's work item — escalations are COMMENTS on it, never
|
|
86
|
+
* task rows (the fleet-task-placement CHECK constraint refuses a system
|
|
87
|
+
* task without session identity, and an escalation is a finding, not an
|
|
88
|
+
* assigned unit of work). */
|
|
89
|
+
orchestrator_work_item_id: v.string(),
|
|
88
90
|
/** Per-kind copy for the message (impact + recommendation). Falls back to default_copy. */
|
|
89
91
|
copy: v.record(v.string(), v.object({ impact: v.string(), recommendation: v.string() })),
|
|
90
92
|
default_copy: v.object({ impact: v.string(), recommendation: v.string() }),
|
|
@@ -118,17 +120,55 @@ async function getSystemIdentity(): Promise<{ userId: string | undefined; organi
|
|
|
118
120
|
}
|
|
119
121
|
|
|
120
122
|
/**
|
|
121
|
-
* The system-authed in-process ORPC client
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
123
|
+
* The system-authed in-process ORPC client, built LAZILY on first use from the
|
|
124
|
+
* SAME composed appRouter the /api/rpc mount uses (getAppRouter — never a raw
|
|
125
|
+
* re-import of the generated routers, per register-page-server-client's
|
|
126
|
+
* documented module-identity trap). Built here rather than read from
|
|
127
|
+
* page-server-orpc/server-client because a pg-boss handler can fire on a
|
|
128
|
+
* BACKEND service, where the page-server client is never registered — the
|
|
129
|
+
* empty-router fallback returns a client with no model namespaces (the
|
|
130
|
+
* TypeError this replaced). Per `no-direct-prisma-outside-orpc`, every WRITE
|
|
131
|
+
* in this handler routes through this client; the single raw read (the
|
|
132
|
+
* event_log recurrence aggregate, a GROUP BY no generated CRUD expresses) uses
|
|
133
|
+
* the assigned-client form with $queryRawUnsafe — the lane-attention-sweep
|
|
127
134
|
* precedent in this same directory. Timestamps are ISO strings, never Date
|
|
128
135
|
* instances (the generated valibot schemas require it).
|
|
129
136
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
let cachedOrpc: OrpcSurface | null = null;
|
|
138
|
+
async function orpc(): Promise<OrpcSurface> {
|
|
139
|
+
if (cachedOrpc) return cachedOrpc;
|
|
140
|
+
const { createRouterClient } = await import("@orpc/server");
|
|
141
|
+
const { getAppRouter } = await import("@teamscala/orpc/app-router");
|
|
142
|
+
const appRouter = (await getAppRouter()) as Record<string, unknown> | null;
|
|
143
|
+
if (!appRouter) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"platform-fault-escalation: no ORPC appRouter — the service has no generated routers to write through",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const { getPrisma } = await import("@teamscala/db/prisma-registry");
|
|
149
|
+
const { loadRegistryConfig } = await import("@teamscala/db/registry/load-config");
|
|
150
|
+
const { PlatformIdentitySchema } = await import(
|
|
151
|
+
"@teamscala/db-validation/registry-schemas/platform-identity"
|
|
152
|
+
);
|
|
153
|
+
const platformIdentity = await loadRegistryConfig(
|
|
154
|
+
"config",
|
|
155
|
+
"platform-identity",
|
|
156
|
+
PlatformIdentitySchema,
|
|
157
|
+
);
|
|
158
|
+
cachedOrpc = createRouterClient(
|
|
159
|
+
{ ...appRouter } as never,
|
|
160
|
+
{
|
|
161
|
+
context: {
|
|
162
|
+
prisma: getPrisma(),
|
|
163
|
+
user: {
|
|
164
|
+
id: "system",
|
|
165
|
+
isSuperAdmin: true,
|
|
166
|
+
organisationId: platformIdentity.platformOrganisationId,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
} as never,
|
|
170
|
+
) as unknown as OrpcSurface;
|
|
171
|
+
return cachedOrpc;
|
|
132
172
|
}
|
|
133
173
|
|
|
134
174
|
/** The router subset this handler uses — mirrors the generated procedure args. */
|
|
@@ -187,7 +227,7 @@ async function fetchAggregates(patterns: string[], windowMinutes: number): Promi
|
|
|
187
227
|
}
|
|
188
228
|
|
|
189
229
|
async function resolveOperatorRelay(): Promise<OperatorRelayBot> {
|
|
190
|
-
const bot = (await orpc().messaging_bots.findFirst({
|
|
230
|
+
const bot = (await (await orpc()).messaging_bots.findFirst({
|
|
191
231
|
where: {
|
|
192
232
|
is_active: true,
|
|
193
233
|
platform: "whatsapp",
|
|
@@ -225,7 +265,7 @@ async function deliverEscalation(
|
|
|
225
265
|
|
|
226
266
|
// 1. Claim the episode BEFORE sending, so a send failure leaves a durable
|
|
227
267
|
// claim the next sweep retries instead of a lost escalation.
|
|
228
|
-
await orpc().platform_fault_escalations.upsert({
|
|
268
|
+
await (await orpc()).platform_fault_escalations.upsert({
|
|
229
269
|
where: { kind },
|
|
230
270
|
create: {
|
|
231
271
|
kind,
|
|
@@ -279,34 +319,30 @@ async function deliverEscalation(
|
|
|
279
319
|
);
|
|
280
320
|
}
|
|
281
321
|
|
|
282
|
-
// 4. Durable record:
|
|
283
|
-
|
|
322
|
+
// 4. Durable record: a COMMENT on the orchestrator's work item — an
|
|
323
|
+
// escalation is a finding the orchestrator/operator triages, not an
|
|
324
|
+
// assigned task (the fleet-task-placement constraint refuses task rows
|
|
325
|
+
// without session identity by design).
|
|
326
|
+
const comment = await (await orpc()).comments.create({
|
|
284
327
|
data: {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
event_kind: kind,
|
|
294
|
-
event_count: aggregate.count,
|
|
295
|
-
first_seen_at: aggregate.firstSeen.toISOString(),
|
|
296
|
-
last_seen_at: aggregate.lastSeen.toISOString(),
|
|
297
|
-
delivered_message_id: messageId,
|
|
298
|
-
escalated_at: now.toISOString(),
|
|
299
|
-
},
|
|
328
|
+
work_item_id: config.orchestrator_work_item_id,
|
|
329
|
+
content: composeEscalationMessage({
|
|
330
|
+
kind,
|
|
331
|
+
aggregate,
|
|
332
|
+
impact: copy.impact,
|
|
333
|
+
recommendation: copy.recommendation,
|
|
334
|
+
}),
|
|
335
|
+
source_cli_session_id: null,
|
|
300
336
|
},
|
|
301
|
-
select: { id: true },
|
|
302
337
|
});
|
|
303
338
|
|
|
304
339
|
// 5. Readback-correct the episode: delivered + linked in one update.
|
|
305
|
-
await orpc().platform_fault_escalations.update({
|
|
340
|
+
await (await orpc()).platform_fault_escalations.update({
|
|
306
341
|
where: { kind },
|
|
307
342
|
data: {
|
|
308
343
|
escalated_at: now.toISOString(),
|
|
309
|
-
work_item_id:
|
|
344
|
+
work_item_id: config.orchestrator_work_item_id,
|
|
345
|
+
delivered_comment_id: comment.id,
|
|
310
346
|
delivered_message_id: messageId,
|
|
311
347
|
last_updated_at: now.toISOString(),
|
|
312
348
|
updated_at: now.toISOString(),
|
|
@@ -318,16 +354,17 @@ async function deliverEscalation(
|
|
|
318
354
|
payload: {
|
|
319
355
|
event_kind: kind,
|
|
320
356
|
count: aggregate.count,
|
|
321
|
-
work_item_id:
|
|
357
|
+
work_item_id: config.orchestrator_work_item_id,
|
|
358
|
+
delivered_comment_id: comment.id,
|
|
322
359
|
delivered_message_id: messageId,
|
|
323
360
|
},
|
|
324
361
|
});
|
|
325
|
-
return { messageId, workItemId:
|
|
362
|
+
return { messageId, workItemId: config.orchestrator_work_item_id };
|
|
326
363
|
}
|
|
327
364
|
|
|
328
365
|
async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpisode): Promise<void> {
|
|
329
366
|
const now = new Date();
|
|
330
|
-
await orpc().platform_fault_escalations.update({
|
|
367
|
+
await (await orpc()).platform_fault_escalations.update({
|
|
331
368
|
where: { kind: episode.kind },
|
|
332
369
|
data: {
|
|
333
370
|
last_seen_at: aggregate.lastSeen.toISOString(),
|
|
@@ -337,7 +374,7 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
|
|
|
337
374
|
},
|
|
338
375
|
});
|
|
339
376
|
if (episode.workItemId) {
|
|
340
|
-
await orpc().comments.create({
|
|
377
|
+
await (await orpc()).comments.create({
|
|
341
378
|
data: {
|
|
342
379
|
work_item_id: episode.workItemId,
|
|
343
380
|
content: `Still recurring: ${aggregate.count} events, last at ${aggregate.lastSeen.toISOString()}. No new escalation (already escalated and unresolved).`,
|
|
@@ -354,12 +391,12 @@ async function updateEpisode(aggregate: FaultAggregate, episode: EscalationEpiso
|
|
|
354
391
|
|
|
355
392
|
async function resolveEpisode(episode: EscalationEpisode, quietMinutes: number): Promise<void> {
|
|
356
393
|
const now = new Date();
|
|
357
|
-
await orpc().platform_fault_escalations.update({
|
|
394
|
+
await (await orpc()).platform_fault_escalations.update({
|
|
358
395
|
where: { kind: episode.kind },
|
|
359
396
|
data: { resolved_at: now.toISOString(), updated_at: now.toISOString() },
|
|
360
397
|
});
|
|
361
398
|
if (episode.workItemId) {
|
|
362
|
-
await orpc().comments.create({
|
|
399
|
+
await (await orpc()).comments.create({
|
|
363
400
|
data: {
|
|
364
401
|
work_item_id: episode.workItemId,
|
|
365
402
|
content: `Fault stopped — no '${episode.kind}' events for ${Math.round(quietMinutes)} min. Episode closed; the fault will re-escalate if it recurs.`,
|
|
@@ -383,7 +420,7 @@ async function platformFaultEscalationSweep(ctx: { config: ScheduledJobConfig })
|
|
|
383
420
|
// Only OPEN episodes matter: a resolved episode and no episode at all have
|
|
384
421
|
// the same meaning to decideSweep (a recurring sustained fault opens a NEW
|
|
385
422
|
// episode and escalates again).
|
|
386
|
-
const openEpisodes = (await orpc().platform_fault_escalations.findMany({
|
|
423
|
+
const openEpisodes = (await (await orpc()).platform_fault_escalations.findMany({
|
|
387
424
|
where: { resolved_at: null },
|
|
388
425
|
})) as Array<{
|
|
389
426
|
kind: string;
|