@h-rig/run-plugin 0.0.6-alpha.186
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 +1 -0
- package/dist/src/plugin.d.ts +4 -0
- package/dist/src/plugin.js +4319 -0
- package/dist/src/read-model/inspect-command.d.ts +19 -0
- package/dist/src/read-model/inspect-command.js +341 -0
- package/dist/src/read-model/plugin.d.ts +4 -0
- package/dist/src/read-model/plugin.js +2550 -0
- package/dist/src/read-model/read-model-backend/diagnostics.d.ts +9 -0
- package/dist/src/read-model/read-model-backend/diagnostics.js +51 -0
- package/dist/src/read-model/read-model-backend/guard.d.ts +4 -0
- package/dist/src/read-model/read-model-backend/guard.js +25 -0
- package/dist/src/read-model/read-model-backend/inbox.d.ts +23 -0
- package/dist/src/read-model/read-model-backend/inbox.js +669 -0
- package/dist/src/read-model/read-model-backend/index.d.ts +8 -0
- package/dist/src/read-model/read-model-backend/index.js +1163 -0
- package/dist/src/read-model/read-model-backend/inspect.d.ts +26 -0
- package/dist/src/read-model/read-model-backend/inspect.js +770 -0
- package/dist/src/read-model/read-model-backend/projection.d.ts +39 -0
- package/dist/src/read-model/read-model-backend/projection.js +669 -0
- package/dist/src/read-model/read-model-backend/run-status.d.ts +27 -0
- package/dist/src/read-model/read-model-backend/run-status.js +237 -0
- package/dist/src/read-model/read-model-backend/stats.d.ts +12 -0
- package/dist/src/read-model/read-model-backend/stats.js +800 -0
- package/dist/src/read-model/read-model-service.d.ts +2 -0
- package/dist/src/read-model/read-model-service.js +1725 -0
- package/dist/src/read-model/reconcile.d.ts +23 -0
- package/dist/src/read-model/reconcile.js +306 -0
- package/dist/src/read-model/run-format.d.ts +23 -0
- package/dist/src/read-model/run-format.js +202 -0
- package/dist/src/read-model/runs-screen.d.ts +14 -0
- package/dist/src/read-model/runs-screen.js +217 -0
- package/dist/src/read-model/session-journal.d.ts +26 -0
- package/dist/src/read-model/session-journal.js +355 -0
- package/dist/src/read-model/stats-command.d.ts +16 -0
- package/dist/src/read-model/stats-command.js +88 -0
- package/dist/src/read-model/stats-format.d.ts +1 -0
- package/dist/src/read-model/stats-format.js +8 -0
- package/dist/src/worker/autohost.d.ts +11 -0
- package/dist/src/worker/autohost.js +858 -0
- package/dist/src/worker/constants.d.ts +3 -0
- package/dist/src/worker/constants.js +10 -0
- package/dist/src/worker/extension.d.ts +14 -0
- package/dist/src/worker/extension.js +881 -0
- package/dist/src/worker/host.d.ts +1 -0
- package/dist/src/worker/host.js +1 -0
- package/dist/src/worker/inbox-command.d.ts +23 -0
- package/dist/src/worker/inbox-command.js +163 -0
- package/dist/src/worker/index.d.ts +2 -0
- package/dist/src/worker/index.js +1767 -0
- package/dist/src/worker/local-run-changes.d.ts +3 -0
- package/dist/src/worker/local-run-changes.js +65 -0
- package/dist/src/worker/notifications.d.ts +1 -0
- package/dist/src/worker/notifications.js +27 -0
- package/dist/src/worker/notify-cap.d.ts +11 -0
- package/dist/src/worker/notify-cap.js +13 -0
- package/dist/src/worker/panel-plugin.d.ts +11 -0
- package/dist/src/worker/panel-plugin.js +37 -0
- package/dist/src/worker/plugin.d.ts +3 -0
- package/dist/src/worker/plugin.js +1761 -0
- package/dist/src/worker/run-control-service.d.ts +2 -0
- package/dist/src/worker/run-control-service.js +210 -0
- package/dist/src/worker/session-journal-writer.d.ts +4 -0
- package/dist/src/worker/session-journal-writer.js +184 -0
- package/dist/src/worker/stall.d.ts +21 -0
- package/dist/src/worker/stall.js +55 -0
- package/dist/src/worker/utils.d.ts +21 -0
- package/dist/src/worker/utils.js +29 -0
- package/dist/src/worker/workflow-journal-writer.d.ts +7 -0
- package/dist/src/worker/workflow-journal-writer.js +76 -0
- package/package.json +47 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/run-plugin/src/worker/run-control-service.ts
|
|
5
|
+
import { defineCapability } from "@rig/core/capability";
|
|
6
|
+
import { loadCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
7
|
+
import {
|
|
8
|
+
RIG_CONTROL_SENTINEL_END,
|
|
9
|
+
RIG_INBOX_RESOLUTION_SENTINEL,
|
|
10
|
+
RIG_PAUSE_SENTINEL,
|
|
11
|
+
RIG_RESUME_SENTINEL,
|
|
12
|
+
RIG_STOP_SENTINEL,
|
|
13
|
+
RIG_STOP_SENTINEL_END,
|
|
14
|
+
RUN_READ_MODEL,
|
|
15
|
+
RUN_SESSION_JOURNAL_WRITER
|
|
16
|
+
} from "@rig/contracts";
|
|
17
|
+
var RunReadModelCap = defineCapability(RUN_READ_MODEL);
|
|
18
|
+
async function readModel(projectRoot) {
|
|
19
|
+
const service = await loadCapabilityForRoot(projectRoot, RunReadModelCap);
|
|
20
|
+
if (!service)
|
|
21
|
+
throw new Error("RUN_READ_MODEL capability unavailable: load @rig/run-plugin before using run control.");
|
|
22
|
+
return service;
|
|
23
|
+
}
|
|
24
|
+
function buildPauseSentinel(runId) {
|
|
25
|
+
return `${RIG_PAUSE_SENTINEL} runId=${runId} requestedBy=operator ${RIG_CONTROL_SENTINEL_END}`;
|
|
26
|
+
}
|
|
27
|
+
function buildResumeSentinel(runId) {
|
|
28
|
+
return `${RIG_RESUME_SENTINEL} runId=${runId} requestedBy=operator ${RIG_CONTROL_SENTINEL_END}`;
|
|
29
|
+
}
|
|
30
|
+
function buildStopSentinel(runId, reason) {
|
|
31
|
+
return `${RIG_STOP_SENTINEL} runId=${runId} reason=${reason} ${RIG_STOP_SENTINEL_END}`;
|
|
32
|
+
}
|
|
33
|
+
function buildInboxResolutionSentinel(runId, payload) {
|
|
34
|
+
return `${RIG_INBOX_RESOLUTION_SENTINEL} runId=${runId} data=${encodeURIComponent(JSON.stringify(payload))} requestedBy=operator ${RIG_CONTROL_SENTINEL_END}`;
|
|
35
|
+
}
|
|
36
|
+
function collabControlFrames(runId, control) {
|
|
37
|
+
switch (control.kind) {
|
|
38
|
+
case "steer":
|
|
39
|
+
return [{ t: "prompt", text: control.message }];
|
|
40
|
+
case "resume":
|
|
41
|
+
return [{ t: "prompt", text: `${buildResumeSentinel(runId)}
|
|
42
|
+
Continue the run from where it paused.` }];
|
|
43
|
+
case "pause":
|
|
44
|
+
return [
|
|
45
|
+
{ t: "prompt", text: `${buildPauseSentinel(runId)}
|
|
46
|
+
Pause this run after the current interruption settles.` },
|
|
47
|
+
{ t: "abort" }
|
|
48
|
+
];
|
|
49
|
+
case "stop":
|
|
50
|
+
return [
|
|
51
|
+
{ t: "prompt", text: `${buildStopSentinel(runId, control.reason)}
|
|
52
|
+
Stop this run and do not continue after the interruption settles.` },
|
|
53
|
+
{ t: "abort" }
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function loadCollabControlDeps() {
|
|
58
|
+
const [{ importRoomKey, seal }, { COLLAB_PROTO, packEnvelope, parseCollabLink }] = await Promise.all([
|
|
59
|
+
import("@oh-my-pi/pi-coding-agent/collab/crypto"),
|
|
60
|
+
import("@oh-my-pi/pi-coding-agent/collab/protocol")
|
|
61
|
+
]);
|
|
62
|
+
return { importRoomKey, seal, COLLAB_PROTO, packEnvelope, parseCollabLink };
|
|
63
|
+
}
|
|
64
|
+
async function sendSealedCollabFrames(joinLink, frames) {
|
|
65
|
+
const { importRoomKey, seal, COLLAB_PROTO, packEnvelope, parseCollabLink } = await loadCollabControlDeps();
|
|
66
|
+
const parsed = parseCollabLink(joinLink);
|
|
67
|
+
if ("error" in parsed)
|
|
68
|
+
throw new Error(parsed.error);
|
|
69
|
+
if (!parsed.writeToken)
|
|
70
|
+
throw new Error("Run collab link is read-only; cannot send control.");
|
|
71
|
+
const key = await importRoomKey(parsed.key);
|
|
72
|
+
const ws = new WebSocket(`${parsed.wsUrl}?role=guest`);
|
|
73
|
+
await new Promise((resolveOpen, rejectOpen) => {
|
|
74
|
+
ws.addEventListener("open", () => resolveOpen(), { once: true });
|
|
75
|
+
ws.addEventListener("error", () => rejectOpen(new Error("collab websocket error")), { once: true });
|
|
76
|
+
});
|
|
77
|
+
const hello = {
|
|
78
|
+
t: "hello",
|
|
79
|
+
proto: COLLAB_PROTO,
|
|
80
|
+
name: "rig-operator",
|
|
81
|
+
writeToken: Buffer.from(parsed.writeToken).toString("base64url")
|
|
82
|
+
};
|
|
83
|
+
ws.send(packEnvelope(0, await seal(key, hello)));
|
|
84
|
+
for (const frame of frames)
|
|
85
|
+
ws.send(packEnvelope(0, await seal(key, frame)));
|
|
86
|
+
try {
|
|
87
|
+
ws.close();
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
function isSteerableStatus(status, classification) {
|
|
91
|
+
switch (status) {
|
|
92
|
+
case "needs-attention":
|
|
93
|
+
case "waiting-approval":
|
|
94
|
+
case "waiting-user-input":
|
|
95
|
+
case "paused":
|
|
96
|
+
return false;
|
|
97
|
+
default:
|
|
98
|
+
return classification.isActive;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function controlAffordance(readModel2, run, purpose) {
|
|
102
|
+
const writable = Boolean(run.joinLink && !run.stale);
|
|
103
|
+
if (!writable)
|
|
104
|
+
return { allowed: false, reason: run.stale ? "run is stale" : "run has no join link" };
|
|
105
|
+
const classification = readModel2.classifyRun(run);
|
|
106
|
+
switch (purpose) {
|
|
107
|
+
case "steer":
|
|
108
|
+
return classification.phase === "active" || classification.phase === "starting" || classification.isNeedsAttention && isSteerableStatus(classification.status, classification) ? { allowed: true } : { allowed: false, reason: "run cannot be steered in its current phase" };
|
|
109
|
+
case "stop":
|
|
110
|
+
return classification.isActive && !classification.isTerminal ? { allowed: true } : { allowed: false, reason: "run cannot be stopped in its current phase" };
|
|
111
|
+
case "pause":
|
|
112
|
+
return classification.phase === "active" || classification.phase === "starting" ? { allowed: true } : { allowed: false, reason: "run cannot be paused in its current phase" };
|
|
113
|
+
case "resume":
|
|
114
|
+
return classification.phase === "paused" ? { allowed: true } : { allowed: false, reason: "run cannot be resumed in its current phase" };
|
|
115
|
+
case "restart":
|
|
116
|
+
case "delete":
|
|
117
|
+
case "prune":
|
|
118
|
+
return !run.live || run.stale ? { allowed: true } : { allowed: false, reason: "run is still live" };
|
|
119
|
+
case "attach":
|
|
120
|
+
case "resolve-inbox":
|
|
121
|
+
case undefined:
|
|
122
|
+
return { allowed: true };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function selectedRun(projectRoot, runId, discoveryFilter) {
|
|
126
|
+
const service = await readModel(projectRoot);
|
|
127
|
+
const run = await service.getRun({ projectRoot, selector: { id: runId, kind: "any" }, ...discoveryFilter !== undefined ? { discoveryFilter } : {} });
|
|
128
|
+
return { service, run };
|
|
129
|
+
}
|
|
130
|
+
function targetFromRun(service, run, purpose) {
|
|
131
|
+
const affordance = controlAffordance(service, run, purpose);
|
|
132
|
+
return {
|
|
133
|
+
runId: run.runId,
|
|
134
|
+
taskId: run.taskId,
|
|
135
|
+
sessionId: run.runId,
|
|
136
|
+
sessionPath: run.sessionPath,
|
|
137
|
+
joinLink: run.joinLink,
|
|
138
|
+
webLink: run.webLink,
|
|
139
|
+
relayUrl: run.relayUrl,
|
|
140
|
+
collabCwd: run.collabCwd,
|
|
141
|
+
live: run.live,
|
|
142
|
+
stale: run.stale,
|
|
143
|
+
canDeliver: affordance.allowed,
|
|
144
|
+
reason: affordance.allowed ? null : affordance.reason ?? null
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function inboxResolutionControl(runId, resolution) {
|
|
148
|
+
return { kind: "steer", message: buildInboxResolutionSentinel(runId, resolution) };
|
|
149
|
+
}
|
|
150
|
+
var runControlService = {
|
|
151
|
+
async resolveControlTarget(input) {
|
|
152
|
+
const { service, run } = await selectedRun(input.projectRoot, input.runId, input.discoveryFilter);
|
|
153
|
+
return run ? targetFromRun(service, run, input.purpose) : null;
|
|
154
|
+
},
|
|
155
|
+
async deliverControl(input) {
|
|
156
|
+
const { service, run } = await selectedRun(input.projectRoot, input.runId, input.discoveryFilter);
|
|
157
|
+
if (!run)
|
|
158
|
+
return { runId: input.runId, kind: input.control.kind, status: "not-found", delivered: false, detail: "run not found" };
|
|
159
|
+
const affordance = controlAffordance(service, run, input.control.kind);
|
|
160
|
+
if (!affordance.allowed) {
|
|
161
|
+
return { runId: run.runId, kind: input.control.kind, status: run.joinLink && !run.stale ? "unsupported" : "not-live", delivered: false, detail: affordance.reason ?? `Run ${run.runId} is not writable in its current phase.` };
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
if (!run.joinLink)
|
|
165
|
+
throw new Error(`Run ${run.runId} has no writable collab join link; attach interactively to ${input.control.kind}.`);
|
|
166
|
+
await sendSealedCollabFrames(run.joinLink, collabControlFrames(run.runId, input.control));
|
|
167
|
+
return { runId: run.runId, kind: input.control.kind, status: "delivered", delivered: true };
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return { runId: run.runId, kind: input.control.kind, status: "unsupported", delivered: false, detail: error instanceof Error ? error.message : String(error) };
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
async resolveInboxRequest(input) {
|
|
173
|
+
const target = await this.resolveControlTarget({ projectRoot: input.projectRoot, runId: input.runId, purpose: "resolve-inbox", ...input.discoveryFilter !== undefined ? { discoveryFilter: input.discoveryFilter } : {} });
|
|
174
|
+
if (!target)
|
|
175
|
+
return { runId: input.runId, requestId: input.resolution.requestId, kind: input.resolution.kind, status: "not-found", delivered: false, detail: "run not found" };
|
|
176
|
+
if (!target.canDeliver)
|
|
177
|
+
return { runId: target.runId, requestId: input.resolution.requestId, kind: input.resolution.kind, status: "not-live", delivered: false, detail: target.reason ?? "run is not deliverable" };
|
|
178
|
+
const result = await this.deliverControl({ projectRoot: input.projectRoot, runId: target.runId, control: inboxResolutionControl(target.runId, input.resolution), ...input.discoveryFilter !== undefined ? { discoveryFilter: input.discoveryFilter } : {} });
|
|
179
|
+
const status = result.delivered ? "resolved" : result.status === "delivered" ? "resolved" : result.status;
|
|
180
|
+
return { runId: target.runId, requestId: input.resolution.requestId, kind: input.resolution.kind, status, delivered: result.delivered, ...result.detail !== undefined ? { detail: result.detail } : {} };
|
|
181
|
+
},
|
|
182
|
+
async resolveResumePlan(input) {
|
|
183
|
+
const { service, run } = await selectedRun(input.projectRoot, input.runId, input.discoveryFilter);
|
|
184
|
+
if (!run)
|
|
185
|
+
return null;
|
|
186
|
+
const affordance = controlAffordance(service, run, "resume");
|
|
187
|
+
if (affordance.allowed)
|
|
188
|
+
return { kind: "deliver-control", runId: run.runId, run };
|
|
189
|
+
if (!run.live || run.stale) {
|
|
190
|
+
return run.taskId ? { kind: "redispatch-task", runId: run.runId, taskId: run.taskId, title: run.title } : { kind: "unavailable", runId: run.runId, reason: "run is not live and has no task id to re-dispatch" };
|
|
191
|
+
}
|
|
192
|
+
return { kind: "unavailable", runId: run.runId, reason: affordance.reason ?? "run cannot be resumed in its current phase" };
|
|
193
|
+
},
|
|
194
|
+
async reconcileDeadPid(input) {
|
|
195
|
+
const RunSessionJournalWriterCap = defineCapability(RUN_SESSION_JOURNAL_WRITER);
|
|
196
|
+
const writer = await loadCapabilityForRoot(input.projectRoot, RunSessionJournalWriterCap);
|
|
197
|
+
if (!writer) {
|
|
198
|
+
return { runId: input.runId, updated: false, status: "unavailable", detail: "RUN_SESSION_JOURNAL_WRITER capability unavailable" };
|
|
199
|
+
}
|
|
200
|
+
return writer.reconcileDeadPid({
|
|
201
|
+
projectRoot: input.projectRoot,
|
|
202
|
+
runId: input.runId,
|
|
203
|
+
sessionPath: input.sessionPath,
|
|
204
|
+
...input.reason !== undefined ? { reason: input.reason } : {}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
export {
|
|
209
|
+
runControlService
|
|
210
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RunDeadPidJournalReconciliationInput, type RunDeadPidJournalReconciliationResult, type RunJournal, type RunJournalSessionManager, type RunSessionJournalWriterService } from "@rig/contracts";
|
|
2
|
+
export declare function createRunJournal(sessionManager: RunJournalSessionManager, runId: string): Promise<RunJournal | null>;
|
|
3
|
+
export declare function reconcileDeadPid(input: RunDeadPidJournalReconciliationInput): Promise<RunDeadPidJournalReconciliationResult>;
|
|
4
|
+
export declare const runSessionJournalWriterService: RunSessionJournalWriterService;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var __require = import.meta.require;
|
|
3
|
+
|
|
4
|
+
// packages/run-plugin/src/worker/session-journal-writer.ts
|
|
5
|
+
import { defineCapability } from "@rig/core/capability";
|
|
6
|
+
import { getInstalledCapability, installCapability, loadCapabilityForRoot } from "@rig/core/capability-loaders";
|
|
7
|
+
import {
|
|
8
|
+
CUSTOM_TYPE_FOR,
|
|
9
|
+
RUN_SESSION_JOURNAL
|
|
10
|
+
} from "@rig/contracts";
|
|
11
|
+
var RunSessionJournalCap = defineCapability(RUN_SESSION_JOURNAL);
|
|
12
|
+
function requireReadCodec() {
|
|
13
|
+
const codec = getInstalledCapability(RunSessionJournalCap);
|
|
14
|
+
if (!codec) {
|
|
15
|
+
throw new Error("RUN_SESSION_JOURNAL codec capability unavailable: the run-worker autohost / run-host preboot install it before binding the journal writer; ensure @rig/run-plugin (default bundle) is installed.");
|
|
16
|
+
}
|
|
17
|
+
return codec;
|
|
18
|
+
}
|
|
19
|
+
function asCustomEntries(entries) {
|
|
20
|
+
return entries.filter((entry) => Boolean(entry && typeof entry === "object" && entry.type === "custom"));
|
|
21
|
+
}
|
|
22
|
+
function dataRecord(entry) {
|
|
23
|
+
return entry.data && typeof entry.data === "object" && !Array.isArray(entry.data) ? entry.data : null;
|
|
24
|
+
}
|
|
25
|
+
function entryBelongsToRun(entry, runId, defaultRunId) {
|
|
26
|
+
const data = dataRecord(entry);
|
|
27
|
+
if (!data)
|
|
28
|
+
return runId === defaultRunId;
|
|
29
|
+
const candidate = data.runId;
|
|
30
|
+
return candidate === undefined ? runId === defaultRunId : candidate === runId;
|
|
31
|
+
}
|
|
32
|
+
function isRunJournalEventInit(event) {
|
|
33
|
+
if (event === null || typeof event !== "object")
|
|
34
|
+
return false;
|
|
35
|
+
const type = event.type;
|
|
36
|
+
return typeof type === "string" && Object.hasOwn(CUSTOM_TYPE_FOR, type);
|
|
37
|
+
}
|
|
38
|
+
function createJournalSessionProvider(options) {
|
|
39
|
+
const now = options.now ?? (() => new Date);
|
|
40
|
+
const appendRunEvent = async (event, runId = options.runId) => {
|
|
41
|
+
options.store.appendCustomEntry(CUSTOM_TYPE_FOR[event.type], { ...event, runId, at: now().toISOString() });
|
|
42
|
+
};
|
|
43
|
+
const readEntries = () => asCustomEntries(options.store.getEntries());
|
|
44
|
+
const entriesForRun = (runId) => readEntries().filter((entry) => entryBelongsToRun(entry, runId, options.runId));
|
|
45
|
+
const readProjection = () => {
|
|
46
|
+
const entries = entriesForRun(options.runId);
|
|
47
|
+
return requireReadCodec().projectRunFromSession(entries, options.runId);
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
runId: options.runId,
|
|
51
|
+
async append(event) {
|
|
52
|
+
if (isRunJournalEventInit(event))
|
|
53
|
+
await appendRunEvent(event);
|
|
54
|
+
},
|
|
55
|
+
appendRunEvent,
|
|
56
|
+
async recordPipeline(runId, pipeline) {
|
|
57
|
+
await appendRunEvent({ type: "pipeline-resolved", pipeline }, runId);
|
|
58
|
+
},
|
|
59
|
+
async recordStageOutcome(runId, outcome) {
|
|
60
|
+
await appendRunEvent({ type: "stage-outcome", outcome }, runId);
|
|
61
|
+
},
|
|
62
|
+
async read(runId) {
|
|
63
|
+
return entriesForRun(runId);
|
|
64
|
+
},
|
|
65
|
+
readEntries,
|
|
66
|
+
readProjection
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class RunSessionJournal {
|
|
71
|
+
sm;
|
|
72
|
+
runId;
|
|
73
|
+
readProjection;
|
|
74
|
+
constructor(sm, runId, readProjection) {
|
|
75
|
+
this.sm = sm;
|
|
76
|
+
this.runId = runId;
|
|
77
|
+
this.readProjection = readProjection;
|
|
78
|
+
}
|
|
79
|
+
#append(init) {
|
|
80
|
+
this.sm.appendCustomEntry(CUSTOM_TYPE_FOR[init.type], { ...init, at: new Date().toISOString() });
|
|
81
|
+
}
|
|
82
|
+
appendStatus(to, opts = {}) {
|
|
83
|
+
const from = this.readProjection().status;
|
|
84
|
+
if (!opts.force)
|
|
85
|
+
requireReadCodec().assertRunStatusTransition(from, to);
|
|
86
|
+
if (opts.errorText !== undefined)
|
|
87
|
+
this.#append({ type: "record-patch", patch: { errorText: opts.errorText } });
|
|
88
|
+
this.#append({
|
|
89
|
+
type: "status-changed",
|
|
90
|
+
from,
|
|
91
|
+
to,
|
|
92
|
+
...opts.reason !== undefined ? { reason: opts.reason } : {},
|
|
93
|
+
...opts.actor !== undefined ? { actor: opts.actor } : {}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
appendTimeline(payload) {
|
|
97
|
+
this.#append({ type: "timeline-entry", payload });
|
|
98
|
+
}
|
|
99
|
+
appendCloseoutPhase(input) {
|
|
100
|
+
this.#append({
|
|
101
|
+
type: "closeout-phase",
|
|
102
|
+
phase: input.phase,
|
|
103
|
+
outcome: input.outcome,
|
|
104
|
+
...input.detail !== undefined ? { detail: input.detail } : {}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
appendApprovalResolved(input) {
|
|
108
|
+
this.#append({
|
|
109
|
+
type: "approval-resolved",
|
|
110
|
+
requestId: input.requestId,
|
|
111
|
+
decision: input.decision,
|
|
112
|
+
actor: input.actor,
|
|
113
|
+
...input.note !== undefined ? { note: input.note } : {}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
appendInputResolved(input) {
|
|
117
|
+
this.#append({ type: "input-resolved", requestId: input.requestId, answers: input.answers, actor: input.actor });
|
|
118
|
+
}
|
|
119
|
+
appendStall(input) {
|
|
120
|
+
this.#append({ type: "stall-detected", detail: input.detail });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function createRunJournal(sessionManager, runId) {
|
|
124
|
+
try {
|
|
125
|
+
const writableSessionManager = sessionManager;
|
|
126
|
+
if (typeof writableSessionManager.appendCustomEntry !== "function")
|
|
127
|
+
return null;
|
|
128
|
+
const kernel = createJournalSessionProvider({
|
|
129
|
+
runId,
|
|
130
|
+
store: {
|
|
131
|
+
appendCustomEntry: (customType, data) => writableSessionManager.appendCustomEntry(customType, data),
|
|
132
|
+
getEntries: () => sessionManager.getEntries?.() ?? sessionManager.getBranch?.() ?? []
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
const journal = new RunSessionJournal(writableSessionManager, runId, kernel.readProjection);
|
|
136
|
+
return {
|
|
137
|
+
kernel,
|
|
138
|
+
appendStatus: journal.appendStatus.bind(journal),
|
|
139
|
+
appendTimeline: journal.appendTimeline.bind(journal),
|
|
140
|
+
appendCloseoutPhase: journal.appendCloseoutPhase.bind(journal),
|
|
141
|
+
appendApprovalResolved: journal.appendApprovalResolved.bind(journal),
|
|
142
|
+
appendInputResolved: journal.appendInputResolved.bind(journal),
|
|
143
|
+
appendStall: journal.appendStall.bind(journal)
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.warn(`[rig-run] RunSessionJournal writer unavailable; run-state arming deferred: ${error instanceof Error ? error.message : String(error)}`);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async function reconcileDeadPid(input) {
|
|
151
|
+
if (!input.sessionPath.trim()) {
|
|
152
|
+
return { runId: input.runId, updated: false, status: "not-found", detail: "session path missing" };
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const codec = await loadCapabilityForRoot(input.projectRoot, RunSessionJournalCap);
|
|
156
|
+
if (!codec) {
|
|
157
|
+
return { runId: input.runId, updated: false, status: "unavailable", detail: "RUN_SESSION_JOURNAL codec capability unavailable" };
|
|
158
|
+
}
|
|
159
|
+
installCapability(RunSessionJournalCap, codec);
|
|
160
|
+
const { SessionManager } = await import("@oh-my-pi/pi-coding-agent/session/session-manager");
|
|
161
|
+
const session = await SessionManager.open(input.sessionPath, undefined, undefined, { suppressBreadcrumb: true });
|
|
162
|
+
const journal = await createRunJournal(session, input.runId);
|
|
163
|
+
if (!journal) {
|
|
164
|
+
return { runId: input.runId, updated: false, status: "not-found", detail: "run journal not found" };
|
|
165
|
+
}
|
|
166
|
+
journal.appendStatus("failed", {
|
|
167
|
+
reason: input.reason ?? "lazy-reconcile:dead-pid",
|
|
168
|
+
actor: { kind: "agent" },
|
|
169
|
+
force: true
|
|
170
|
+
});
|
|
171
|
+
return { runId: input.runId, updated: true, status: "reconciled" };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
return { runId: input.runId, updated: false, status: "unavailable", detail: error instanceof Error ? error.message : String(error) };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
var runSessionJournalWriterService = {
|
|
177
|
+
createRunJournal,
|
|
178
|
+
reconcileDeadPid
|
|
179
|
+
};
|
|
180
|
+
export {
|
|
181
|
+
runSessionJournalWriterService,
|
|
182
|
+
reconcileDeadPid,
|
|
183
|
+
createRunJournal
|
|
184
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type StallJournal = {
|
|
2
|
+
appendStall(input: {
|
|
3
|
+
detail: string;
|
|
4
|
+
}): void;
|
|
5
|
+
};
|
|
6
|
+
export declare function timestampMs(value: number | string | Date | null | undefined): number | null;
|
|
7
|
+
export declare function computeRunStall(input: {
|
|
8
|
+
readonly lastActivityAt: number | string | Date | null | undefined;
|
|
9
|
+
readonly now: number | string | Date;
|
|
10
|
+
readonly thresholdMs: number;
|
|
11
|
+
}): boolean;
|
|
12
|
+
export declare function appendRunStallDetected(journal: StallJournal | null | undefined, detail?: string): boolean;
|
|
13
|
+
export declare function startRunProcessStallMonitor(opts: {
|
|
14
|
+
readonly journal: StallJournal | null | undefined;
|
|
15
|
+
readonly lastActivityAt: () => number;
|
|
16
|
+
readonly alreadyStalled?: boolean;
|
|
17
|
+
readonly thresholdMs?: number;
|
|
18
|
+
readonly intervalMs?: number;
|
|
19
|
+
readonly now?: () => number;
|
|
20
|
+
}): () => void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-plugin/src/worker/constants.ts
|
|
3
|
+
var TRACKED_RUN_STALL_MS = 20 * 60 * 1000;
|
|
4
|
+
var RUN_PROCESS_STALL_SWEEP_MS = 60 * 1000;
|
|
5
|
+
var RUN_PROCESS_STALL_DETAIL = "Run process made no OMP session progress for 20+ minutes; recording a stall so recovery can requeue or resume the session.";
|
|
6
|
+
|
|
7
|
+
// packages/run-plugin/src/worker/stall.ts
|
|
8
|
+
function timestampMs(value) {
|
|
9
|
+
if (value === null || value === undefined)
|
|
10
|
+
return null;
|
|
11
|
+
const ms = value instanceof Date ? value.getTime() : typeof value === "number" ? value : Date.parse(value);
|
|
12
|
+
return Number.isFinite(ms) ? ms : null;
|
|
13
|
+
}
|
|
14
|
+
function computeRunStall(input) {
|
|
15
|
+
const lastActivityAt = timestampMs(input.lastActivityAt);
|
|
16
|
+
const now = timestampMs(input.now);
|
|
17
|
+
if (lastActivityAt === null || now === null || input.thresholdMs <= 0)
|
|
18
|
+
return false;
|
|
19
|
+
return now - lastActivityAt >= input.thresholdMs;
|
|
20
|
+
}
|
|
21
|
+
function appendRunStallDetected(journal, detail = RUN_PROCESS_STALL_DETAIL) {
|
|
22
|
+
if (!journal)
|
|
23
|
+
return false;
|
|
24
|
+
try {
|
|
25
|
+
journal.appendStall({ detail });
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.warn(`[rig-run] stall-detected append failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function startRunProcessStallMonitor(opts) {
|
|
33
|
+
if (!opts.journal)
|
|
34
|
+
return () => {};
|
|
35
|
+
let stallDetected = opts.alreadyStalled === true;
|
|
36
|
+
const thresholdMs = opts.thresholdMs ?? TRACKED_RUN_STALL_MS;
|
|
37
|
+
const now = opts.now ?? (() => Date.now());
|
|
38
|
+
const timer = setInterval(() => {
|
|
39
|
+
if (stallDetected)
|
|
40
|
+
return;
|
|
41
|
+
if (!computeRunStall({ lastActivityAt: opts.lastActivityAt(), now: now(), thresholdMs }))
|
|
42
|
+
return;
|
|
43
|
+
stallDetected = true;
|
|
44
|
+
appendRunStallDetected(opts.journal);
|
|
45
|
+
}, opts.intervalMs ?? RUN_PROCESS_STALL_SWEEP_MS);
|
|
46
|
+
if (typeof timer.unref === "function")
|
|
47
|
+
timer.unref();
|
|
48
|
+
return () => clearInterval(timer);
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
timestampMs,
|
|
52
|
+
startRunProcessStallMonitor,
|
|
53
|
+
computeRunStall,
|
|
54
|
+
appendRunStallDetected
|
|
55
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@oh-my-pi/pi-coding-agent";
|
|
2
|
+
import type { ExtensionCollabFacade, CollabRegistryFilter, CollabSessionProjection, ExtensionJoinCollabSessionInput, ExtensionStartCollabHostInput, LiveCollabProjection } from "@oh-my-pi/pi-coding-agent/collab-api";
|
|
3
|
+
import type { CustomEntry } from "@oh-my-pi/pi-coding-agent/session/session-entries";
|
|
4
|
+
import { type RigWorkflowStatus } from "@rig/contracts";
|
|
5
|
+
export type CompatCollabFacade = ExtensionCollabFacade & {
|
|
6
|
+
listActiveCollabSessions?: (filter?: CollabRegistryFilter) => Promise<readonly LiveCollabProjection[]>;
|
|
7
|
+
get?: (sessionId: string) => Promise<LiveCollabProjection | null>;
|
|
8
|
+
getCollabForSession?: (sessionId: string) => Promise<LiveCollabProjection | null>;
|
|
9
|
+
startCollabHost?: (input: ExtensionStartCollabHostInput) => Promise<LiveCollabProjection>;
|
|
10
|
+
joinCollabSession?: (input: ExtensionJoinCollabSessionInput) => Promise<void>;
|
|
11
|
+
listSessions?: (filter?: CollabRegistryFilter) => Promise<readonly CollabSessionProjection[]>;
|
|
12
|
+
};
|
|
13
|
+
export type RigCollabContext = ExtensionContext & {
|
|
14
|
+
collab?: CompatCollabFacade;
|
|
15
|
+
};
|
|
16
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
17
|
+
export declare function customEntries(entries: readonly {
|
|
18
|
+
type: string;
|
|
19
|
+
}[]): CustomEntry[];
|
|
20
|
+
export declare function appendStatus(api: ExtensionAPI, status: RigWorkflowStatus, detail?: string): void;
|
|
21
|
+
export { rigProjectRootFromEnv as rigProjectRoot } from "@rig/core/root-resolver";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-plugin/src/worker/utils.ts
|
|
3
|
+
import { RIG_WORKFLOW_STATUS_CHANGED } from "@rig/contracts";
|
|
4
|
+
import { rigProjectRootFromEnv } from "@rig/core/root-resolver";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
7
|
+
}
|
|
8
|
+
function customEntries(entries) {
|
|
9
|
+
const customs = [];
|
|
10
|
+
for (const entry of entries) {
|
|
11
|
+
if (entry.type === "custom")
|
|
12
|
+
customs.push(entry);
|
|
13
|
+
}
|
|
14
|
+
return customs;
|
|
15
|
+
}
|
|
16
|
+
function appendStatus(api, status, detail) {
|
|
17
|
+
api.appendEntry(RIG_WORKFLOW_STATUS_CHANGED, {
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
status,
|
|
20
|
+
...detail ? { detail } : {},
|
|
21
|
+
changedAt: new Date().toISOString()
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export {
|
|
25
|
+
rigProjectRootFromEnv as rigProjectRoot,
|
|
26
|
+
isRecord,
|
|
27
|
+
customEntries,
|
|
28
|
+
appendStatus
|
|
29
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RigWorkflowJournalWriterService, type RigWorkflowOperatorNote, type RigWorkflowOperatorNoteInput, type RigWorkflowStarted, type RigWorkflowStartedInput, type RigWorkflowStatusChanged, type RigWorkflowStatusChangedInput, type RigWorkflowTargetSelected, type RigWorkflowTargetSelectedInput, type RigWorkflowTaskSelected, type RigWorkflowTaskSelectedInput } from "@rig/contracts";
|
|
2
|
+
export declare function createWorkflowStarted(input: RigWorkflowStartedInput): RigWorkflowStarted;
|
|
3
|
+
export declare function createWorkflowTargetSelected(input: RigWorkflowTargetSelectedInput): RigWorkflowTargetSelected;
|
|
4
|
+
export declare function createWorkflowTaskSelected(input: RigWorkflowTaskSelectedInput): RigWorkflowTaskSelected;
|
|
5
|
+
export declare function createWorkflowStatusChanged(input: RigWorkflowStatusChangedInput): RigWorkflowStatusChanged;
|
|
6
|
+
export declare function createWorkflowOperatorNote(input: RigWorkflowOperatorNoteInput): RigWorkflowOperatorNote;
|
|
7
|
+
export declare const workflowJournalWriterService: RigWorkflowJournalWriterService;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/run-plugin/src/worker/workflow-journal-writer.ts
|
|
3
|
+
import {
|
|
4
|
+
RIG_WORKFLOW_OPERATOR_NOTE,
|
|
5
|
+
RIG_WORKFLOW_STARTED,
|
|
6
|
+
RIG_WORKFLOW_STATUS_CHANGED,
|
|
7
|
+
RIG_WORKFLOW_TARGET_SELECTED,
|
|
8
|
+
RIG_WORKFLOW_TASK_SELECTED
|
|
9
|
+
} from "@rig/contracts";
|
|
10
|
+
function createWorkflowStarted(input) {
|
|
11
|
+
return {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
workflowId: input.workflowId,
|
|
14
|
+
target: input.target,
|
|
15
|
+
selectedRepo: input.selectedRepo,
|
|
16
|
+
owner: input.owner,
|
|
17
|
+
createdAt: input.createdAt ?? new Date().toISOString()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function createWorkflowTargetSelected(input) {
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
target: input.target,
|
|
24
|
+
...input.reason ? { reason: input.reason } : {},
|
|
25
|
+
selectedAt: input.selectedAt ?? new Date().toISOString()
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createWorkflowTaskSelected(input) {
|
|
29
|
+
return {
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
taskId: input.taskId,
|
|
32
|
+
...input.title ? { title: input.title } : {},
|
|
33
|
+
selectedAt: input.selectedAt ?? new Date().toISOString()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createWorkflowStatusChanged(input) {
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: 1,
|
|
39
|
+
status: input.status,
|
|
40
|
+
...input.detail ? { detail: input.detail } : {},
|
|
41
|
+
changedAt: input.changedAt ?? new Date().toISOString()
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function createWorkflowOperatorNote(input) {
|
|
45
|
+
return {
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
...input.noteId ? { noteId: input.noteId } : {},
|
|
48
|
+
note: input.note,
|
|
49
|
+
notedAt: input.notedAt ?? new Date().toISOString()
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var workflowJournalWriterService = {
|
|
53
|
+
appendStarted(appender, input) {
|
|
54
|
+
appender.appendEntry(RIG_WORKFLOW_STARTED, createWorkflowStarted(input));
|
|
55
|
+
},
|
|
56
|
+
appendTargetSelected(appender, input) {
|
|
57
|
+
appender.appendEntry(RIG_WORKFLOW_TARGET_SELECTED, createWorkflowTargetSelected(input));
|
|
58
|
+
},
|
|
59
|
+
appendTaskSelected(appender, input) {
|
|
60
|
+
appender.appendEntry(RIG_WORKFLOW_TASK_SELECTED, createWorkflowTaskSelected(input));
|
|
61
|
+
},
|
|
62
|
+
appendStatusChanged(appender, input) {
|
|
63
|
+
appender.appendEntry(RIG_WORKFLOW_STATUS_CHANGED, createWorkflowStatusChanged(input));
|
|
64
|
+
},
|
|
65
|
+
appendOperatorNote(appender, input) {
|
|
66
|
+
appender.appendEntry(RIG_WORKFLOW_OPERATOR_NOTE, createWorkflowOperatorNote(input));
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
export {
|
|
70
|
+
workflowJournalWriterService,
|
|
71
|
+
createWorkflowTaskSelected,
|
|
72
|
+
createWorkflowTargetSelected,
|
|
73
|
+
createWorkflowStatusChanged,
|
|
74
|
+
createWorkflowStarted,
|
|
75
|
+
createWorkflowOperatorNote
|
|
76
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@h-rig/run-plugin",
|
|
3
|
+
"version": "0.0.6-alpha.186",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Run execution + run read-model: the one owner of run state (worker write path, projection read path).",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/plugin.d.ts",
|
|
14
|
+
"import": "./dist/src/plugin.js"
|
|
15
|
+
},
|
|
16
|
+
"./plugin": {
|
|
17
|
+
"types": "./dist/src/plugin.d.ts",
|
|
18
|
+
"import": "./dist/src/plugin.js"
|
|
19
|
+
},
|
|
20
|
+
"./worker": {
|
|
21
|
+
"types": "./dist/src/worker/index.d.ts",
|
|
22
|
+
"import": "./dist/src/worker/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./worker/host": {
|
|
25
|
+
"types": "./dist/src/worker/host.d.ts",
|
|
26
|
+
"import": "./dist/src/worker/host.js"
|
|
27
|
+
},
|
|
28
|
+
"./session-journal": {
|
|
29
|
+
"types": "./dist/src/read-model/session-journal.d.ts",
|
|
30
|
+
"import": "./dist/src/read-model/session-journal.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"bun": ">=1.3.11"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@oh-my-pi/omp-stats": "16.2.4",
|
|
38
|
+
"@oh-my-pi/pi-coding-agent": "16.2.4",
|
|
39
|
+
"@oh-my-pi/pi-tui": "16.2.4",
|
|
40
|
+
"@oh-my-pi/pi-utils": "16.2.4",
|
|
41
|
+
"@rig/contracts": "npm:@h-rig/contracts@0.0.6-alpha.186",
|
|
42
|
+
"@rig/core": "npm:@h-rig/core@0.0.6-alpha.186",
|
|
43
|
+
"@rig/std-shared": "npm:@h-rig/std-shared@0.0.6-alpha.186",
|
|
44
|
+
"effect": "4.0.0-beta.90",
|
|
45
|
+
"picocolors": "^1.1.1"
|
|
46
|
+
}
|
|
47
|
+
}
|