@tomflow/proflow-platform-host 0.1.30 → 0.1.31

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,782 @@
1
+ import { createHash } from "node:crypto";
2
+ function object(value, name) {
3
+ if (typeof value !== "object" || value === null || Array.isArray(value))
4
+ throw new Error(`${name}_INVALID`);
5
+ return value;
6
+ }
7
+ function string(value, name) {
8
+ if (typeof value !== "string" || value.length === 0)
9
+ throw new Error(`${name}_INVALID`);
10
+ return value;
11
+ }
12
+ function positiveInteger(value, name) {
13
+ if (!Number.isInteger(value) || Number(value) <= 0)
14
+ throw new Error(`${name}_INVALID`);
15
+ return Number(value);
16
+ }
17
+ function optionalString(value, name) {
18
+ return value === undefined ? undefined : string(value, name);
19
+ }
20
+ function parseConfig(value) {
21
+ const raw = object(value, "MONITOR_CONFIG");
22
+ if (raw.contract !== "proflow.monitor-config.v1")
23
+ throw new Error("MONITOR_CONFIG_INVALID");
24
+ const projectRef = raw.projectRef === null ? null : string(raw.projectRef, "MONITOR_PROJECT_REF");
25
+ const projectLocator = raw.projectLocator === null
26
+ ? null
27
+ : string(raw.projectLocator, "MONITOR_PROJECT_LOCATOR");
28
+ return {
29
+ revision: positiveInteger(raw.revision, "MONITOR_CONFIG_REVISION"),
30
+ enabled: raw.enabled === true,
31
+ notificationsEnabled: raw.notificationsEnabled === true,
32
+ projectRef,
33
+ projectLocator,
34
+ };
35
+ }
36
+ function parseShift(value, expectedShiftId) {
37
+ const raw = object(value, "MONITOR_SHIFT");
38
+ const shiftId = string(raw.shiftId, "MONITOR_SHIFT_ID");
39
+ if (shiftId !== expectedShiftId)
40
+ throw new Error("MONITOR_SHIFT_ID_MISMATCH");
41
+ const state = raw.state;
42
+ if (!new Set(["BOOTING", "ACTIVE", "DRAINING", "RETIRED", "STOPPED"]).has(String(state)))
43
+ throw new Error("MONITOR_SHIFT_STATE_INVALID");
44
+ const rotationDueAt = string(raw.rotationDueAt, "MONITOR_ROTATION_DUE_AT");
45
+ const deadlineAt = string(raw.deadlineAt, "MONITOR_DEADLINE_AT");
46
+ if (Number.isNaN(Date.parse(rotationDueAt)) ||
47
+ Number.isNaN(Date.parse(deadlineAt)))
48
+ throw new Error("MONITOR_SHIFT_TIME_INVALID");
49
+ return {
50
+ shiftId,
51
+ chatRef: string(raw.chatRef, "MONITOR_CHAT_REF"),
52
+ generation: positiveInteger(raw.generation, "MONITOR_SHIFT_GENERATION"),
53
+ state: state,
54
+ rotationDueAt,
55
+ deadlineAt,
56
+ ...(raw.bootProof === undefined ? {} : { bootProof: raw.bootProof }),
57
+ ...(optionalString(raw.handoffRef, "MONITOR_HANDOFF_REF") === undefined
58
+ ? {}
59
+ : { handoffRef: String(raw.handoffRef) }),
60
+ ...(optionalString(raw.nextChatPromptRef, "MONITOR_NEXT_CHAT_PROMPT_REF") === undefined
61
+ ? {}
62
+ : { nextChatPromptRef: String(raw.nextChatPromptRef) }),
63
+ };
64
+ }
65
+ function parseRun(value) {
66
+ const raw = object(value, "MONITOR_RUN");
67
+ if (raw.contract !== "proflow.monitor-run.v1")
68
+ throw new Error("MONITOR_RUN_INVALID");
69
+ const runId = string(raw.runId, "MONITOR_RUN_ID");
70
+ const shiftsRaw = object(raw.shifts, "MONITOR_SHIFTS");
71
+ const shifts = {};
72
+ for (const [shiftId, shift] of Object.entries(shiftsRaw))
73
+ shifts[shiftId] = parseShift(shift, shiftId);
74
+ const activeShiftId = raw.activeShiftId === null
75
+ ? null
76
+ : string(raw.activeShiftId, "MONITOR_ACTIVE_SHIFT_ID");
77
+ if (activeShiftId !== null && !shifts[activeShiftId])
78
+ throw new Error("MONITOR_ACTIVE_SHIFT_MISSING");
79
+ return {
80
+ runId,
81
+ version: positiveInteger(raw.version, "MONITOR_RUN_VERSION"),
82
+ activeShiftId,
83
+ shifts,
84
+ };
85
+ }
86
+ function parseRuns(value) {
87
+ if (!Array.isArray(value))
88
+ throw new Error("MONITOR_RUN_LIST_INVALID");
89
+ return value.map(parseRun);
90
+ }
91
+ function parseObservationIdentity(value) {
92
+ const raw = object(value, "MONITOR_OBSERVATION");
93
+ return {
94
+ chatRef: string(raw.chatRef, "MONITOR_CHAT_REF"),
95
+ url: string(raw.url, "MONITOR_CHAT_URL"),
96
+ };
97
+ }
98
+ function parseObservation(value) {
99
+ const raw = object(value, "MONITOR_OBSERVATION");
100
+ const identity = parseObservationIdentity(raw);
101
+ const state = raw.state;
102
+ if (!new Set([
103
+ "IDLE",
104
+ "GENERATING",
105
+ "TOOL_RUNNING",
106
+ "AWAITING_USER",
107
+ "UNKNOWN",
108
+ ]).has(String(state)))
109
+ throw new Error("MONITOR_CHAT_STATE_INVALID");
110
+ const settledRef = optionalString(raw.settledRef, "MONITOR_SETTLED_REF");
111
+ if (settledRef !== undefined && state !== "IDLE")
112
+ throw new Error("MONITOR_SETTLED_REF_INVALID");
113
+ return {
114
+ ...identity,
115
+ state: state,
116
+ ...(settledRef === undefined ? {} : { settledRef }),
117
+ };
118
+ }
119
+ function parseArtifact(value) {
120
+ const raw = object(value, "MONITOR_ARTIFACT");
121
+ return {
122
+ artifactRef: string(raw.artifactRef, "MONITOR_ARTIFACT_REF"),
123
+ content: string(raw.content, "MONITOR_ARTIFACT_CONTENT"),
124
+ };
125
+ }
126
+ function stableRef(kind, parts) {
127
+ return `monitor-${kind}:${createHash("sha256")
128
+ .update(JSON.stringify(parts))
129
+ .digest("hex")}`;
130
+ }
131
+ export function monitorHandoffMessageRef(input) {
132
+ return stableRef("handoff-request", [
133
+ input.runId,
134
+ input.shiftId,
135
+ input.generation,
136
+ ]);
137
+ }
138
+ export function monitorBootstrapMessageRef(input) {
139
+ return stableRef("bootstrap", [
140
+ input.runId,
141
+ input.shiftId,
142
+ input.generation,
143
+ ]);
144
+ }
145
+ export function monitorBootstrapText(input) {
146
+ return [
147
+ `你是现有 ProFlow Monitor shift:${input.shiftId}。现在只完成正式 bootstrap recovery,不创建新 Chat 或新 shift,也不要继续旧断点。`,
148
+ "使用 Local Dev 按当前 chat-local-engineering-protocol 与 proflow-chat-loop 恢复 authority:读取公共上下文 README、长期规则 01/02/05、CURRENT、CURRENT.REQUIRED_CONTEXT、Phase 4 的 03/04 与 latest handoff,并重新读取当前 owner/runtime facts。",
149
+ `完成真实读取后,使用 Workspace 的 automation/proflow-browser-extension/monitor-boot-proof.mjs 为 run ${input.runId} / shift ${input.shiftId} 发布 boot proof(必须由你这个真实 Monitor Chat 自己执行 --authorities-read)。`,
150
+ "boot proof 成功后停止本轮并等待系统激活;不要替维护 Chat、Product、Dev 或 Test 执行业务工作。",
151
+ ].join("\n");
152
+ }
153
+ export function monitorTurnWakeMessageRef(input) {
154
+ return stableRef("turn-wake", [
155
+ input.runId,
156
+ input.shiftId,
157
+ input.generation,
158
+ input.settledRef,
159
+ ]);
160
+ }
161
+ export function monitorTurnWakeText() {
162
+ return "请依据当前上下文、当前真实状态与既有规则,自主判断并执行当前应采取的动作。";
163
+ }
164
+ export function monitorNextShiftId(input) {
165
+ return stableRef("shift", [
166
+ input.runId,
167
+ input.shiftId,
168
+ input.generation + 1,
169
+ ]);
170
+ }
171
+ export function monitorNextChatMessageRef(input) {
172
+ return stableRef("next-chat", [
173
+ input.runId,
174
+ input.shiftId,
175
+ input.generation,
176
+ input.nextShiftId,
177
+ ]);
178
+ }
179
+ export function monitorHandoffRequestText(_input) {
180
+ return "请依据当前上下文、当前真实状态与既有规则,自主完成当前班次交接。";
181
+ }
182
+ function monitorCapability(request) {
183
+ return (request.capability === "monitor.chat.submit" ||
184
+ request.capability === "monitor.chat.create");
185
+ }
186
+ export async function authorizeMonitorExecution(input) {
187
+ try {
188
+ const request = input.request;
189
+ if (!monitorCapability(request))
190
+ return false;
191
+ if (request.callerRef !== "platform-host:monitor-rotation")
192
+ return false;
193
+ if (request.taskId !== undefined ||
194
+ request.nodeId !== undefined ||
195
+ request.runNo !== undefined ||
196
+ request.roleRef !== undefined ||
197
+ request.workerRef !== undefined ||
198
+ request.projectRoot !== undefined)
199
+ return false;
200
+ if (request.input.contentFingerprint !== request.input.messageRef)
201
+ return false;
202
+ const config = parseConfig(await input.monitor.invoke("config.read", {}));
203
+ if (!config.enabled ||
204
+ config.projectRef === null ||
205
+ config.projectLocator === null ||
206
+ config.revision !== request.input.configRevision ||
207
+ config.projectRef !== request.input.projectRef)
208
+ return false;
209
+ const run = parseRun(await input.monitor.invoke("run.read", {
210
+ runId: request.input.runId,
211
+ }));
212
+ if (run.version !== request.input.expectedRunVersion)
213
+ return false;
214
+ const shift = run.shifts[request.input.shiftId];
215
+ if (!shift || shift.generation !== request.input.generation)
216
+ return false;
217
+ const nowMs = (input.now ?? (() => new Date()))().getTime();
218
+ if (request.capability === "monitor.chat.create") {
219
+ if (shift.state !== "DRAINING" ||
220
+ !shift.handoffRef ||
221
+ !shift.nextChatPromptRef ||
222
+ request.input.projectLocator !== config.projectLocator)
223
+ return false;
224
+ const nextShiftId = monitorNextShiftId({
225
+ runId: run.runId,
226
+ shiftId: shift.shiftId,
227
+ generation: shift.generation,
228
+ });
229
+ if (request.input.nextShiftId !== nextShiftId ||
230
+ run.shifts[nextShiftId])
231
+ return false;
232
+ const expectedRef = monitorNextChatMessageRef({
233
+ runId: run.runId,
234
+ shiftId: shift.shiftId,
235
+ generation: shift.generation,
236
+ nextShiftId,
237
+ });
238
+ if (request.input.messageRef !== expectedRef)
239
+ return false;
240
+ const prompt = parseArtifact(await input.monitor.invoke("artifact.read", {
241
+ artifactRef: shift.nextChatPromptRef,
242
+ }));
243
+ return (prompt.artifactRef === shift.nextChatPromptRef &&
244
+ request.input.text === prompt.content);
245
+ }
246
+ if (shift.chatRef !== request.input.chatRef)
247
+ return false;
248
+ const rawObservation = await input.monitor.invoke("observation.read", {
249
+ chatRef: shift.chatRef,
250
+ });
251
+ const identity = parseObservationIdentity(rawObservation);
252
+ if (identity.chatRef !== shift.chatRef ||
253
+ identity.url !== request.input.conversationLocator)
254
+ return false;
255
+ if (shift.state === "BOOTING") {
256
+ const observation = parseObservation(rawObservation);
257
+ if (run.activeShiftId !== null ||
258
+ observation.state !== "IDLE")
259
+ return false;
260
+ const expectedRef = monitorBootstrapMessageRef({
261
+ runId: run.runId,
262
+ shiftId: shift.shiftId,
263
+ generation: shift.generation,
264
+ });
265
+ return (request.input.messageRef === expectedRef &&
266
+ request.input.text ===
267
+ monitorBootstrapText({
268
+ runId: run.runId,
269
+ shiftId: shift.shiftId,
270
+ }));
271
+ }
272
+ if (shift.state === "ACTIVE") {
273
+ const observation = parseObservation(rawObservation);
274
+ if (run.activeShiftId !== shift.shiftId ||
275
+ nowMs >= Date.parse(shift.rotationDueAt) ||
276
+ observation.state !== "IDLE" ||
277
+ !observation.settledRef)
278
+ return false;
279
+ const expectedRef = monitorTurnWakeMessageRef({
280
+ runId: run.runId,
281
+ shiftId: shift.shiftId,
282
+ generation: shift.generation,
283
+ settledRef: observation.settledRef,
284
+ });
285
+ if (request.input.messageRef !== expectedRef ||
286
+ request.input.text !== monitorTurnWakeText())
287
+ return false;
288
+ return true;
289
+ }
290
+ if (shift.state === "DRAINING") {
291
+ if (nowMs > Date.parse(shift.deadlineAt))
292
+ return false;
293
+ const expectedRef = monitorHandoffMessageRef({
294
+ runId: run.runId,
295
+ shiftId: shift.shiftId,
296
+ generation: shift.generation,
297
+ });
298
+ return (request.input.messageRef === expectedRef &&
299
+ request.input.text ===
300
+ monitorHandoffRequestText({
301
+ runId: run.runId,
302
+ shiftId: shift.shiftId,
303
+ generation: shift.generation,
304
+ projectRef: config.projectRef,
305
+ }));
306
+ }
307
+ return false;
308
+ }
309
+ catch {
310
+ return false;
311
+ }
312
+ }
313
+ function appliedExecutionResult(value, capability) {
314
+ const record = object(value, "MONITOR_EXECUTION_RECORD");
315
+ if (record.status !== "SUCCEEDED" ||
316
+ record.sideEffectState !== "APPLIED")
317
+ return null;
318
+ const result = object(record.result, "MONITOR_EXECUTION_RESULT");
319
+ if (result.capability !== capability)
320
+ return null;
321
+ return object(result.data, "MONITOR_EXECUTION_RESULT_DATA");
322
+ }
323
+ function drainingShift(run) {
324
+ return Object.values(run.shifts)
325
+ .filter((shift) => shift.state === "DRAINING")
326
+ .sort((left, right) => right.generation - left.generation)[0];
327
+ }
328
+ function initialBootingShift(run) {
329
+ const booting = Object.values(run.shifts).filter((shift) => shift.state === "BOOTING" && shift.generation === 1);
330
+ return booting.length === 1 ? booting[0] : undefined;
331
+ }
332
+ export function createMonitorRotationCoordinator(options) {
333
+ const intervalMs = options.intervalMs ?? 15_000;
334
+ if (!Number.isInteger(intervalMs) || intervalMs < 1_000)
335
+ throw new TypeError("monitor rotation interval must be an integer >= 1000");
336
+ const now = options.now ?? (() => new Date());
337
+ const drivenSettledRefs = new Map();
338
+ let timer;
339
+ let inFlight = null;
340
+ let stopping = false;
341
+ const assertRunning = () => {
342
+ if (stopping)
343
+ throw new Error("MONITOR_ROTATION_STOPPED");
344
+ };
345
+ const notifyDrive = async (config, run, shift, input) => {
346
+ if (!config.notificationsEnabled)
347
+ return { status: "DISABLED" };
348
+ await options.monitor.invoke("notification.enqueue", {
349
+ event: {
350
+ contract: "proflow.monitor-notification.v1",
351
+ eventId: stableRef("notification", [
352
+ input.event,
353
+ input.messageRef,
354
+ ]),
355
+ lane: "PROFLOW_SELF_ITERATION",
356
+ source: "MONITOR",
357
+ event: input.event,
358
+ severity: "INFO",
359
+ time: now().toISOString(),
360
+ runRef: {
361
+ runId: run.runId,
362
+ shiftId: shift.shiftId,
363
+ chatRef: shift.chatRef,
364
+ },
365
+ summary: input.summary,
366
+ evidenceRefs: [input.messageRef],
367
+ },
368
+ fence: {
369
+ runId: run.runId,
370
+ shiftId: shift.shiftId,
371
+ generation: shift.generation,
372
+ expectedVersion: run.version,
373
+ configRevision: config.revision,
374
+ },
375
+ });
376
+ if (!input.deliverNow)
377
+ return { status: "QUEUED" };
378
+ try {
379
+ return await options.monitor.invoke("notification.deliver", {
380
+ limit: 10,
381
+ });
382
+ }
383
+ catch (error) {
384
+ return {
385
+ status: "FAILED",
386
+ error: error instanceof Error
387
+ ? error.message
388
+ : "MONITOR_NOTIFICATION_FAILED",
389
+ };
390
+ }
391
+ };
392
+ const requestBootstrap = async (config, run, shift) => {
393
+ if (config.projectRef === null)
394
+ return { state: "PROJECT_BINDING_MISSING" };
395
+ const observation = parseObservation(await options.monitor.invoke("observation.read", {
396
+ chatRef: shift.chatRef,
397
+ }));
398
+ if (observation.state !== "IDLE")
399
+ return {
400
+ state: `BOOTSTRAP_${observation.state}`,
401
+ };
402
+ const messageRef = monitorBootstrapMessageRef({
403
+ runId: run.runId,
404
+ shiftId: shift.shiftId,
405
+ generation: shift.generation,
406
+ });
407
+ const request = {
408
+ contract: "execution",
409
+ contractVersion: "1.0.0",
410
+ callerRef: "platform-host:monitor-rotation",
411
+ idempotencyKey: messageRef,
412
+ capability: "monitor.chat.submit",
413
+ input: {
414
+ runId: run.runId,
415
+ shiftId: shift.shiftId,
416
+ generation: shift.generation,
417
+ expectedRunVersion: run.version,
418
+ configRevision: config.revision,
419
+ projectRef: config.projectRef,
420
+ chatRef: shift.chatRef,
421
+ messageRef,
422
+ conversationLocator: observation.url,
423
+ contentFingerprint: messageRef,
424
+ text: monitorBootstrapText({
425
+ runId: run.runId,
426
+ shiftId: shift.shiftId,
427
+ }),
428
+ },
429
+ };
430
+ assertRunning();
431
+ const result = await options.execution.invoke("executeCapability", request);
432
+ return appliedExecutionResult(result, "monitor.chat.submit")
433
+ ? { state: "BOOTSTRAP_DISPATCHED", messageRef }
434
+ : { state: "BOOTSTRAP_EFFECT_PENDING", messageRef };
435
+ };
436
+ const requestTurnWake = async (config, run, shift) => {
437
+ if (config.projectRef === null)
438
+ return { state: "PROJECT_BINDING_MISSING" };
439
+ const observation = parseObservation(await options.monitor.invoke("observation.read", {
440
+ chatRef: shift.chatRef,
441
+ }));
442
+ if (observation.state !== "IDLE")
443
+ return {
444
+ state: `ACTIVE_${observation.state}`,
445
+ };
446
+ if (!observation.settledRef)
447
+ return {
448
+ state: "ACTIVE_IDLE_WITHOUT_SETTLED_REF",
449
+ };
450
+ const drivenKey = `${run.runId}:${shift.shiftId}:${shift.generation}`;
451
+ if (drivenSettledRefs.get(drivenKey) === observation.settledRef)
452
+ return { state: "ACTIVE_TURN_ALREADY_DRIVEN" };
453
+ const messageRef = monitorTurnWakeMessageRef({
454
+ runId: run.runId,
455
+ shiftId: shift.shiftId,
456
+ generation: shift.generation,
457
+ settledRef: observation.settledRef,
458
+ });
459
+ const request = {
460
+ contract: "execution",
461
+ contractVersion: "1.0.0",
462
+ callerRef: "platform-host:monitor-rotation",
463
+ idempotencyKey: messageRef,
464
+ capability: "monitor.chat.submit",
465
+ input: {
466
+ runId: run.runId,
467
+ shiftId: shift.shiftId,
468
+ generation: shift.generation,
469
+ expectedRunVersion: run.version,
470
+ configRevision: config.revision,
471
+ projectRef: config.projectRef,
472
+ chatRef: shift.chatRef,
473
+ messageRef,
474
+ conversationLocator: observation.url,
475
+ contentFingerprint: messageRef,
476
+ text: monitorTurnWakeText(),
477
+ },
478
+ };
479
+ assertRunning();
480
+ const result = await options.execution.invoke("executeCapability", request);
481
+ if (!appliedExecutionResult(result, "monitor.chat.submit"))
482
+ return { state: "TURN_WAKE_EFFECT_PENDING" };
483
+ await notifyDrive(config, run, shift, {
484
+ event: "CHAT_WAKE",
485
+ messageRef,
486
+ summary: "Monitor turn drive dispatched.",
487
+ });
488
+ drivenSettledRefs.set(drivenKey, observation.settledRef);
489
+ return {
490
+ state: "TURN_WAKE_DISPATCHED",
491
+ messageRef,
492
+ };
493
+ };
494
+ const requestHandoff = async (config, run, shift) => {
495
+ if (config.projectRef === null)
496
+ return { state: "PROJECT_BINDING_MISSING" };
497
+ const observation = parseObservationIdentity(await options.monitor.invoke("observation.read", {
498
+ chatRef: shift.chatRef,
499
+ }));
500
+ const messageRef = monitorHandoffMessageRef({
501
+ runId: run.runId,
502
+ shiftId: shift.shiftId,
503
+ generation: shift.generation,
504
+ });
505
+ const request = {
506
+ contract: "execution",
507
+ contractVersion: "1.0.0",
508
+ callerRef: "platform-host:monitor-rotation",
509
+ idempotencyKey: messageRef,
510
+ capability: "monitor.chat.submit",
511
+ input: {
512
+ runId: run.runId,
513
+ shiftId: shift.shiftId,
514
+ generation: shift.generation,
515
+ expectedRunVersion: run.version,
516
+ configRevision: config.revision,
517
+ projectRef: config.projectRef,
518
+ chatRef: shift.chatRef,
519
+ messageRef,
520
+ conversationLocator: observation.url,
521
+ contentFingerprint: messageRef,
522
+ text: monitorHandoffRequestText({
523
+ runId: run.runId,
524
+ shiftId: shift.shiftId,
525
+ generation: shift.generation,
526
+ projectRef: config.projectRef,
527
+ }),
528
+ },
529
+ };
530
+ assertRunning();
531
+ const result = await options.execution.invoke("executeCapability", request);
532
+ return appliedExecutionResult(result, "monitor.chat.submit")
533
+ ? { state: "HANDOFF_REQUESTED" }
534
+ : { state: "HANDOFF_EFFECT_PENDING" };
535
+ };
536
+ const createNextChat = async (config, run, shift) => {
537
+ if (config.projectRef === null ||
538
+ config.projectLocator === null)
539
+ return { state: "PROJECT_BINDING_MISSING" };
540
+ if (!shift.nextChatPromptRef)
541
+ return { state: "NEXT_CHAT_PROMPT_MISSING" };
542
+ const prompt = parseArtifact(await options.monitor.invoke("artifact.read", {
543
+ artifactRef: shift.nextChatPromptRef,
544
+ }));
545
+ const nextShiftId = monitorNextShiftId({
546
+ runId: run.runId,
547
+ shiftId: shift.shiftId,
548
+ generation: shift.generation,
549
+ });
550
+ const messageRef = monitorNextChatMessageRef({
551
+ runId: run.runId,
552
+ shiftId: shift.shiftId,
553
+ generation: shift.generation,
554
+ nextShiftId,
555
+ });
556
+ const request = {
557
+ contract: "execution",
558
+ contractVersion: "1.0.0",
559
+ callerRef: "platform-host:monitor-rotation",
560
+ idempotencyKey: messageRef,
561
+ capability: "monitor.chat.create",
562
+ input: {
563
+ runId: run.runId,
564
+ shiftId: shift.shiftId,
565
+ generation: shift.generation,
566
+ expectedRunVersion: run.version,
567
+ configRevision: config.revision,
568
+ projectRef: config.projectRef,
569
+ nextShiftId,
570
+ messageRef,
571
+ projectLocator: config.projectLocator,
572
+ contentFingerprint: messageRef,
573
+ text: prompt.content,
574
+ },
575
+ };
576
+ assertRunning();
577
+ const execution = await options.execution.invoke("executeCapability", request);
578
+ const data = appliedExecutionResult(execution, "monitor.chat.create");
579
+ if (!data)
580
+ return { state: "NEXT_CHAT_EFFECT_PENDING" };
581
+ const chatRef = string(data.chatRef, "MONITOR_NEXT_CHAT_REF");
582
+ string(data.conversationLocator, "MONITOR_NEXT_CHAT_LOCATOR");
583
+ assertRunning();
584
+ const nextRun = parseRun(await options.monitor.invoke("shift.prepareNext", {
585
+ runId: run.runId,
586
+ fromShiftId: shift.shiftId,
587
+ fromGeneration: shift.generation,
588
+ expectedVersion: run.version,
589
+ configRevision: config.revision,
590
+ shiftId: nextShiftId,
591
+ chatRef,
592
+ }));
593
+ return {
594
+ state: "NEXT_SHIFT_PREPARED",
595
+ run: nextRun,
596
+ };
597
+ };
598
+ const completeBoot = async (config, run, shift, previous) => {
599
+ if (!shift.bootProof)
600
+ return requestBootstrap(config, run, shift);
601
+ assertRunning();
602
+ const activated = parseRun(previous
603
+ ? await options.monitor.invoke("shift.takeover", {
604
+ runId: run.runId,
605
+ fromShiftId: previous.shiftId,
606
+ fromGeneration: previous.generation,
607
+ toShiftId: shift.shiftId,
608
+ toGeneration: shift.generation,
609
+ expectedVersion: run.version,
610
+ configRevision: config.revision,
611
+ })
612
+ : await options.monitor.invoke("shift.activate", {
613
+ runId: run.runId,
614
+ shiftId: shift.shiftId,
615
+ generation: shift.generation,
616
+ expectedVersion: run.version,
617
+ configRevision: config.revision,
618
+ }));
619
+ return {
620
+ state: "TAKEOVER_COMPLETED",
621
+ run: activated,
622
+ };
623
+ };
624
+ const tickRun = async (config, run) => {
625
+ if (run.activeShiftId) {
626
+ const active = run.shifts[run.activeShiftId];
627
+ if (!active || active.state !== "ACTIVE")
628
+ throw new Error("MONITOR_ACTIVE_SHIFT_INVALID");
629
+ const status = object(await options.monitor.invoke("shift.rotationStatus", {
630
+ runId: run.runId,
631
+ shiftId: active.shiftId,
632
+ }), "MONITOR_ROTATION_STATUS");
633
+ if (status.due !== true)
634
+ return requestTurnWake(config, run, active);
635
+ const handoffMessageRef = monitorHandoffMessageRef({
636
+ runId: run.runId,
637
+ shiftId: active.shiftId,
638
+ generation: active.generation,
639
+ });
640
+ await notifyDrive(config, run, active, {
641
+ event: "HANDOFF_STARTED",
642
+ messageRef: handoffMessageRef,
643
+ summary: "Monitor handoff drive dispatched.",
644
+ deliverNow: true,
645
+ });
646
+ assertRunning();
647
+ const drained = parseRun(await options.monitor.invoke("shift.beginDrain", {
648
+ runId: run.runId,
649
+ shiftId: active.shiftId,
650
+ generation: active.generation,
651
+ expectedVersion: run.version,
652
+ configRevision: config.revision,
653
+ reason: "ROTATION_DUE",
654
+ }));
655
+ const shift = drained.shifts[active.shiftId];
656
+ if (!shift)
657
+ throw new Error("MONITOR_DRAINING_SHIFT_MISSING");
658
+ return requestHandoff(config, drained, shift);
659
+ }
660
+ const previous = drainingShift(run);
661
+ if (previous) {
662
+ const nextShiftId = monitorNextShiftId({
663
+ runId: run.runId,
664
+ shiftId: previous.shiftId,
665
+ generation: previous.generation,
666
+ });
667
+ const next = run.shifts[nextShiftId];
668
+ if (!previous.handoffRef ||
669
+ !previous.nextChatPromptRef) {
670
+ const status = object(await options.monitor.invoke("shift.rotationStatus", {
671
+ runId: run.runId,
672
+ shiftId: previous.shiftId,
673
+ }), "MONITOR_ROTATION_STATUS");
674
+ if (status.expired === true)
675
+ return {
676
+ state: "HANDOFF_DEADLINE_EXPIRED",
677
+ };
678
+ return requestHandoff(config, run, previous);
679
+ }
680
+ if (!next)
681
+ return createNextChat(config, run, previous);
682
+ if (next.state !== "BOOTING")
683
+ return {
684
+ state: `NEXT_SHIFT_${next.state}`,
685
+ };
686
+ return completeBoot(config, run, next, previous);
687
+ }
688
+ const initial = initialBootingShift(run);
689
+ if (initial)
690
+ return completeBoot(config, run, initial);
691
+ return { state: "NO_ACTIVE_ROTATION" };
692
+ };
693
+ const tick = async () => {
694
+ assertRunning();
695
+ const config = parseConfig(await options.monitor.invoke("config.read", {}));
696
+ if (!config.enabled)
697
+ return { state: "DISABLED", runs: [] };
698
+ if (config.projectRef === null ||
699
+ config.projectLocator === null)
700
+ return {
701
+ state: "PROJECT_BINDING_REQUIRED",
702
+ runs: [],
703
+ };
704
+ const outcomes = [];
705
+ for (const run of parseRuns(await options.monitor.invoke("run.list", {}))) {
706
+ assertRunning();
707
+ try {
708
+ outcomes.push({
709
+ runId: run.runId,
710
+ outcome: await tickRun(config, run),
711
+ });
712
+ }
713
+ catch (error) {
714
+ outcomes.push({
715
+ runId: run.runId,
716
+ outcome: {
717
+ state: "BLOCKED",
718
+ error: error instanceof Error
719
+ ? error.message
720
+ : "MONITOR_ROTATION_FAILED",
721
+ },
722
+ });
723
+ }
724
+ }
725
+ let notification = { status: "DISABLED" };
726
+ if (config.notificationsEnabled) {
727
+ try {
728
+ assertRunning();
729
+ notification = await options.monitor.invoke("notification.deliver", { limit: 10 });
730
+ }
731
+ catch (error) {
732
+ notification = {
733
+ status: "FAILED",
734
+ error: error instanceof Error
735
+ ? error.message
736
+ : "MONITOR_NOTIFICATION_FAILED",
737
+ };
738
+ }
739
+ }
740
+ return {
741
+ state: "CHECKED",
742
+ runs: outcomes,
743
+ notification,
744
+ };
745
+ };
746
+ const runTick = () => {
747
+ if (stopping || inFlight)
748
+ return inFlight;
749
+ inFlight = tick().finally(() => {
750
+ inFlight = null;
751
+ });
752
+ return inFlight;
753
+ };
754
+ const scheduleTick = () => {
755
+ void runTick()?.catch(async () => {
756
+ if (stopping)
757
+ return;
758
+ try {
759
+ await options.onError?.("MONITOR_ROTATION_POLL_FAILED");
760
+ }
761
+ catch { }
762
+ });
763
+ };
764
+ return Object.freeze({
765
+ tick,
766
+ start() {
767
+ if (timer)
768
+ return;
769
+ stopping = false;
770
+ scheduleTick();
771
+ timer = setInterval(scheduleTick, intervalMs);
772
+ timer.unref?.();
773
+ },
774
+ async stop() {
775
+ stopping = true;
776
+ if (timer)
777
+ clearInterval(timer);
778
+ timer = undefined;
779
+ await inFlight?.catch(() => undefined);
780
+ },
781
+ });
782
+ }