@ponythewhite/base-context 1.0.14 → 1.0.16
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/CHANGELOG.md +18 -0
- package/dist/base-context-runtime/pyproject.toml +1 -1
- package/dist/base-context-runtime/src/rlm/bash.py +19 -0
- package/dist/base-context-runtime/src/rlm/job_watch.py +156 -0
- package/dist/base-context-runtime/src/rlm/repl.md +28 -6
- package/dist/base-context-runtime/src/rlm/repl.py +6 -1
- package/dist/base-context-runtime/test/test_bash.py +4 -4
- package/dist/base-context-runtime/test/test_job_watch.py +67 -0
- package/dist/base-context-runtime/uv.lock +1 -1
- package/dist/build-info.json +1 -1
- package/dist/bundle/{bundled-modules-UODO7ZR7.js → bundled-modules-2DMTK7VK.js} +2 -2
- package/dist/bundle/{chunk-SX7CAXWU.js → chunk-MSH6QRP5.js} +1730 -867
- package/dist/bundle/{chunk-NLRWERWA.js → chunk-T2OK7SRV.js} +53 -21
- package/dist/bundle/{cli-main-6GXXIMQQ.js → cli-main-WL2L5IEQ.js} +2 -2
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/{main-HF64B4HM.js → main-XPIEIEQJ.js} +2 -2
- package/dist/core/agent-session.d.ts +2 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +191 -33
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +1 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +11 -9
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/history-index-worker.d.ts.map +1 -1
- package/dist/core/history-index-worker.js +22 -1
- package/dist/core/history-index-worker.js.map +1 -1
- package/dist/core/history-index.d.ts +2 -0
- package/dist/core/history-index.d.ts.map +1 -1
- package/dist/core/history-index.js.map +1 -1
- package/dist/core/inference-coordinator.d.ts.map +1 -1
- package/dist/core/inference-coordinator.js +13 -2
- package/dist/core/inference-coordinator.js.map +1 -1
- package/dist/core/job-watch.d.ts +120 -0
- package/dist/core/job-watch.d.ts.map +1 -0
- package/dist/core/job-watch.js +648 -0
- package/dist/core/job-watch.js.map +1 -0
- package/dist/core/kernel/repl-manager.d.ts +5 -1
- package/dist/core/kernel/repl-manager.d.ts.map +1 -1
- package/dist/core/kernel/repl-manager.js +57 -0
- package/dist/core/kernel/repl-manager.js.map +1 -1
- package/dist/core/kernel/shared.d.ts +12 -0
- package/dist/core/kernel/shared.d.ts.map +1 -1
- package/dist/core/kernel/shared.js.map +1 -1
- package/dist/core/prompts/rlm.d.ts.map +1 -1
- package/dist/core/prompts/rlm.js +49 -39
- package/dist/core/prompts/rlm.js.map +1 -1
- package/dist/core/session-manager.d.ts +10 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +66 -1
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/tools/prime-context.d.ts.map +1 -1
- package/dist/core/tools/prime-context.js +1 -1
- package/dist/core/tools/prime-context.js.map +1 -1
- package/dist/modes/daemon/daemon-mode.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-mode.js +17 -17
- package/dist/modes/daemon/daemon-mode.js.map +1 -1
- package/dist/modes/daemon/daemon-worker-client.d.ts +1 -0
- package/dist/modes/daemon/daemon-worker-client.d.ts.map +1 -1
- package/dist/modes/daemon/daemon-worker-client.js +8 -0
- package/dist/modes/daemon/daemon-worker-client.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +31 -5
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/skills/agent-message/SKILL.md +8 -2
- package/dist/skills/bounded-inspect/SKILL.md +78 -0
- package/dist/skills/bounded-inspect/pyproject.toml +16 -0
- package/dist/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
- package/dist/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
- package/dist/skills/job-watch/SKILL.md +43 -0
- package/dist/skills/job-watch/pyproject.toml +13 -0
- package/dist/skills/job-watch/src/job_watch/__init__.py +61 -0
- package/dist/skills/rlm-heartbeat/SKILL.md +10 -6
- package/dist/utils/file-lines.d.ts.map +1 -1
- package/dist/utils/file-lines.js +6 -2
- package/dist/utils/file-lines.js.map +1 -1
- package/docs/rlm-runtime.md +4 -1
- package/docs/skills.md +18 -26
- package/docs/upstream-1.0.15.md +83 -0
- package/package.json +4 -4
- package/skills/agent-message/SKILL.md +8 -2
- package/skills/bounded-inspect/SKILL.md +78 -0
- package/skills/bounded-inspect/pyproject.toml +16 -0
- package/skills/bounded-inspect/src/bounded_inspect/__init__.py +313 -0
- package/skills/bounded-inspect/src/bounded_inspect/__main__.py +3 -0
- package/skills/job-watch/SKILL.md +43 -0
- package/skills/job-watch/pyproject.toml +13 -0
- package/skills/job-watch/src/job_watch/__init__.py +61 -0
- package/skills/rlm-heartbeat/SKILL.md +10 -6
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { normalizeHeartbeatSchedule, parseAgentCronSchedule } from "./cron-jobs.js";
|
|
3
|
+
export const JOB_WATCH_STATE = "job_watch_state";
|
|
4
|
+
export const JOB_WATCH_CAPABILITY = "job_watch_probe_v1";
|
|
5
|
+
const TERMINAL = new Set(["succeeded", "failed", "cancelled"]);
|
|
6
|
+
const MAX_WATCHES = 16;
|
|
7
|
+
const MAX_VISIBLE_BYTES = 2048;
|
|
8
|
+
const MAX_STATE_BYTES = 64 * 1024;
|
|
9
|
+
const STATE_REFUSAL_RESERVE = 4096;
|
|
10
|
+
function record(value) {
|
|
11
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function text(value, name, max = 512) {
|
|
14
|
+
if (typeof value !== "string" || !value.length || Buffer.byteLength(value) > max)
|
|
15
|
+
throw new Error(`${name} must be a nonempty string of at most ${max} bytes`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function strings(value, name) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return [];
|
|
21
|
+
if (!Array.isArray(value) || value.length > 16)
|
|
22
|
+
throw new Error(`${name} must contain at most 16 strings`);
|
|
23
|
+
return value.map((v) => text(v, name));
|
|
24
|
+
}
|
|
25
|
+
function interval(value, name) {
|
|
26
|
+
const schedule = parseAgentCronSchedule(normalizeHeartbeatSchedule(text(value, name))).schedule;
|
|
27
|
+
if (schedule.kind !== "interval" || !schedule.intervalMs)
|
|
28
|
+
throw new Error(`${name} must be a recurring interval`);
|
|
29
|
+
return schedule.intervalMs;
|
|
30
|
+
}
|
|
31
|
+
function bounded(value) {
|
|
32
|
+
const out = { ...value };
|
|
33
|
+
if (Buffer.byteLength(JSON.stringify(out)) > MAX_VISIBLE_BYTES) {
|
|
34
|
+
delete out.progress;
|
|
35
|
+
out.omitted = "progress exceeds notification budget; inspect evidence";
|
|
36
|
+
}
|
|
37
|
+
if (Buffer.byteLength(JSON.stringify(out)) > MAX_VISIBLE_BYTES) {
|
|
38
|
+
out.attention = ["attention exceeds notification budget; inspect evidence"];
|
|
39
|
+
out.evidence = Array.isArray(out.evidence) ? out.evidence.slice(0, 1) : [];
|
|
40
|
+
}
|
|
41
|
+
return JSON.stringify(out);
|
|
42
|
+
}
|
|
43
|
+
/** Session-owned waiting state. Timers never invoke a model or execute notebook code. */
|
|
44
|
+
export class JobWatchController {
|
|
45
|
+
hooks;
|
|
46
|
+
watches = new Map();
|
|
47
|
+
parked;
|
|
48
|
+
timer;
|
|
49
|
+
busy = new Set();
|
|
50
|
+
queued = new Set();
|
|
51
|
+
flushing;
|
|
52
|
+
tail = Promise.resolve();
|
|
53
|
+
stopped = false;
|
|
54
|
+
unavailable;
|
|
55
|
+
constructor(hooks) {
|
|
56
|
+
this.hooks = hooks;
|
|
57
|
+
}
|
|
58
|
+
now() {
|
|
59
|
+
return this.hooks.now?.() ?? Date.now();
|
|
60
|
+
}
|
|
61
|
+
owned() {
|
|
62
|
+
return !this.stopped && this.hooks.current();
|
|
63
|
+
}
|
|
64
|
+
serialize(work) {
|
|
65
|
+
const operation = this.tail.then(work);
|
|
66
|
+
this.tail = operation.catch(() => undefined);
|
|
67
|
+
return operation;
|
|
68
|
+
}
|
|
69
|
+
isCurrent() {
|
|
70
|
+
return this.owned();
|
|
71
|
+
}
|
|
72
|
+
snapshot() {
|
|
73
|
+
return structuredClone({
|
|
74
|
+
version: 1,
|
|
75
|
+
session_id: this.hooks.sessionId,
|
|
76
|
+
session_file: this.hooks.sessionFile,
|
|
77
|
+
watches: [...this.watches.values()],
|
|
78
|
+
parked: this.parked,
|
|
79
|
+
unavailable: this.unavailable,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
stateBudget() {
|
|
83
|
+
return Math.min(MAX_STATE_BYTES, this.hooks.stateBudgetBytes ?? MAX_STATE_BYTES);
|
|
84
|
+
}
|
|
85
|
+
stateBytes(state = this.snapshot()) {
|
|
86
|
+
return Buffer.byteLength(JSON.stringify({
|
|
87
|
+
...state,
|
|
88
|
+
watches: state.watches.map((watch) => ({
|
|
89
|
+
...watch,
|
|
90
|
+
pending: watch.pending.map((event) => ({ ...event, admitted: true, uncertain: true, reconcile: true })),
|
|
91
|
+
})),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
async refusePendingBudget(watch, previous) {
|
|
95
|
+
const rejected = JSON.stringify({
|
|
96
|
+
observation: watch.latest,
|
|
97
|
+
pending: watch.pending.slice(previous.pending.length),
|
|
98
|
+
});
|
|
99
|
+
Object.assign(watch, previous);
|
|
100
|
+
this.unavailable =
|
|
101
|
+
"Job-watch pending-state budget exhausted; monitoring is unavailable and further checks cannot continue under this hold. Resume delivery and explicitly register again.";
|
|
102
|
+
for (const current of this.watches.values()) {
|
|
103
|
+
current.active = false;
|
|
104
|
+
this.hooks.cancelProbe(current.id, current.generation);
|
|
105
|
+
}
|
|
106
|
+
const retained = await this.hooks.retain?.(rejected);
|
|
107
|
+
if (!this.owned())
|
|
108
|
+
return;
|
|
109
|
+
const locator = retained ?? this.hooks.sessionFile ?? `job_watch.status(${JSON.stringify(watch.id)})`;
|
|
110
|
+
const id = randomUUID();
|
|
111
|
+
watch.pending.push({
|
|
112
|
+
id,
|
|
113
|
+
text: bounded({
|
|
114
|
+
event_id: id,
|
|
115
|
+
watch_id: watch.id,
|
|
116
|
+
goal_id: watch.goal_id,
|
|
117
|
+
job_id: watch.job_id,
|
|
118
|
+
state: "unknown",
|
|
119
|
+
previous_state: previous.latest.state,
|
|
120
|
+
reasons: ["monitoring_unavailable"],
|
|
121
|
+
observation_error: this.unavailable,
|
|
122
|
+
evidence: [locator],
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
await this.save();
|
|
126
|
+
await this.flushPending();
|
|
127
|
+
this.schedule();
|
|
128
|
+
this.hooks.onError(new Error(`${this.unavailable} Evidence: ${locator}`));
|
|
129
|
+
}
|
|
130
|
+
async save() {
|
|
131
|
+
if (this.owned())
|
|
132
|
+
await this.hooks.persist(this.snapshot());
|
|
133
|
+
}
|
|
134
|
+
async watch(input) {
|
|
135
|
+
return this.serialize(async () => {
|
|
136
|
+
if (!this.owned())
|
|
137
|
+
throw new Error("Watch owner changed");
|
|
138
|
+
if (this.unavailable)
|
|
139
|
+
throw new Error(this.unavailable);
|
|
140
|
+
const resource = text(input.resource_id, "resource_id");
|
|
141
|
+
const existing = [...this.watches.values()].find((w) => w.resource_id === resource && w.active);
|
|
142
|
+
if (existing) {
|
|
143
|
+
if (existing.goal_id !== this.hooks.goalId())
|
|
144
|
+
throw new Error("Watch belongs to a different goal; unregister it first");
|
|
145
|
+
return this.status(existing.id);
|
|
146
|
+
}
|
|
147
|
+
if (this.watches.size >= MAX_WATCHES)
|
|
148
|
+
throw new Error("Unregister an old watch before registering more than 16 watches");
|
|
149
|
+
const source = input.completion_source;
|
|
150
|
+
if (source !== "handle" && source !== "probe")
|
|
151
|
+
throw new Error('completion_source must be "handle" or "probe"');
|
|
152
|
+
const command = input.probe_command == null ? undefined : text(input.probe_command, "probe_command", 8192);
|
|
153
|
+
if (source === "probe" && !command)
|
|
154
|
+
throw new Error("Probe completion requires a read-only probe_command");
|
|
155
|
+
const notify = input.notify ?? "terminal";
|
|
156
|
+
if (notify !== "terminal" && notify !== "changes")
|
|
157
|
+
throw new Error('notify must be "terminal" or "changes"');
|
|
158
|
+
const fields = strings(input.fields, "fields");
|
|
159
|
+
if (fields.includes("observed_at"))
|
|
160
|
+
throw new Error("Observation timestamps are not meaningful comparison fields");
|
|
161
|
+
if (notify === "changes" && fields.length === 0)
|
|
162
|
+
throw new Error("changes requires explicit progress fields");
|
|
163
|
+
const now = this.now();
|
|
164
|
+
const intervalMs = interval(input.interval ?? "5m", "interval");
|
|
165
|
+
const reportMs = input.report_every == null ? undefined : interval(input.report_every, "report_every");
|
|
166
|
+
const timeout = input.timeout_ms ?? 30000;
|
|
167
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout < 1 || timeout > 300000)
|
|
168
|
+
throw new Error("timeout_ms must be between 1 and 300000");
|
|
169
|
+
const deadline = input.deadline == null ? undefined : Date.parse(text(input.deadline, "deadline"));
|
|
170
|
+
if (deadline !== undefined && !Number.isFinite(deadline))
|
|
171
|
+
throw new Error("deadline must be an ISO date");
|
|
172
|
+
const jobId = text(input.job_id ?? resource, "job_id", 256);
|
|
173
|
+
const watch = {
|
|
174
|
+
id: randomUUID(),
|
|
175
|
+
generation: randomUUID(),
|
|
176
|
+
resource_id: resource,
|
|
177
|
+
job_id: jobId,
|
|
178
|
+
completion_source: source,
|
|
179
|
+
probe_command: command,
|
|
180
|
+
timeout_ms: timeout,
|
|
181
|
+
interval_ms: intervalMs,
|
|
182
|
+
report_ms: reportMs,
|
|
183
|
+
next_check: now + intervalMs,
|
|
184
|
+
next_report: reportMs ? now + reportMs : undefined,
|
|
185
|
+
deadline,
|
|
186
|
+
notify,
|
|
187
|
+
fields,
|
|
188
|
+
active: true,
|
|
189
|
+
pending: [],
|
|
190
|
+
goal_id: this.hooks.goalId(),
|
|
191
|
+
latest: {
|
|
192
|
+
observed_at: new Date(now).toISOString(),
|
|
193
|
+
job_id: jobId,
|
|
194
|
+
state: "running",
|
|
195
|
+
progress: {},
|
|
196
|
+
attention: [],
|
|
197
|
+
evidence: strings(input.evidence, "evidence"),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
if (this.stateBytes({ ...this.snapshot(), watches: [...this.watches.values(), watch] }) >
|
|
201
|
+
Math.min(48 * 1024, this.stateBudget() - STATE_REFUSAL_RESERVE))
|
|
202
|
+
throw new Error("Job watch declaration budget exceeded; unregister obsolete watches");
|
|
203
|
+
this.watches.set(watch.id, watch);
|
|
204
|
+
await this.save();
|
|
205
|
+
this.schedule();
|
|
206
|
+
return this.status(watch.id);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
status(id) {
|
|
210
|
+
const values = id === undefined ? [...this.watches.values()] : [this.require(id)];
|
|
211
|
+
const statuses = values.map((w) => ({
|
|
212
|
+
id: w.id,
|
|
213
|
+
generation: w.generation,
|
|
214
|
+
active: w.active,
|
|
215
|
+
completion_source: w.completion_source,
|
|
216
|
+
...w.latest,
|
|
217
|
+
...(this.unavailable
|
|
218
|
+
? { state: "unknown", last_observed_state: w.latest.state, monitoring_error: this.unavailable }
|
|
219
|
+
: {}),
|
|
220
|
+
last_admitted: w.last_admitted,
|
|
221
|
+
pending_events: w.pending.length,
|
|
222
|
+
}));
|
|
223
|
+
return id === undefined ? { watches: statuses, parked: this.isParked() } : statuses[0];
|
|
224
|
+
}
|
|
225
|
+
require(id) {
|
|
226
|
+
const watch = this.watches.get(id);
|
|
227
|
+
if (!watch)
|
|
228
|
+
throw new Error(`Unknown watch ${id}`);
|
|
229
|
+
return watch;
|
|
230
|
+
}
|
|
231
|
+
async park(ids) {
|
|
232
|
+
return this.serialize(async () => {
|
|
233
|
+
if (this.unavailable)
|
|
234
|
+
return { parked: false, reason: "monitoring_unavailable" };
|
|
235
|
+
if (!ids.length)
|
|
236
|
+
throw new Error("park requires at least one watch");
|
|
237
|
+
const selected = ids.map((id) => this.require(id));
|
|
238
|
+
if (!this.owned() || selected.some((w) => w.goal_id !== this.hooks.goalId()))
|
|
239
|
+
throw new Error("Watch owner changed");
|
|
240
|
+
// A completion racing registration/park is already actionable; do not wait forever.
|
|
241
|
+
if (selected.some((w) => !w.active || w.pending.length > 0))
|
|
242
|
+
return { parked: false, reason: "event_available" };
|
|
243
|
+
this.parked = { ids: [...new Set(ids)], goal_id: this.hooks.goalId() };
|
|
244
|
+
await this.save();
|
|
245
|
+
return { parked: true, ids: this.parked.ids };
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
isParked() {
|
|
249
|
+
return (this.owned() &&
|
|
250
|
+
this.parked !== undefined &&
|
|
251
|
+
this.parked.goal_id === this.hooks.goalId() &&
|
|
252
|
+
this.parked.ids.some((id) => this.watches.has(id)));
|
|
253
|
+
}
|
|
254
|
+
async unregister(id) {
|
|
255
|
+
return this.serialize(async () => {
|
|
256
|
+
const watch = this.watches.get(id);
|
|
257
|
+
if (watch) {
|
|
258
|
+
this.hooks.cancelProbe(watch.id, watch.generation);
|
|
259
|
+
this.watches.delete(id);
|
|
260
|
+
if (!this.watches.size)
|
|
261
|
+
this.unavailable = undefined;
|
|
262
|
+
if (this.parked?.ids.includes(id))
|
|
263
|
+
this.parked = undefined;
|
|
264
|
+
await this.save();
|
|
265
|
+
}
|
|
266
|
+
this.schedule();
|
|
267
|
+
return { id, unregistered: true };
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
schedule() {
|
|
271
|
+
if (this.timer)
|
|
272
|
+
clearTimeout(this.timer);
|
|
273
|
+
if (!this.owned() || this.unavailable)
|
|
274
|
+
return;
|
|
275
|
+
const times = [...this.watches.values()]
|
|
276
|
+
.filter((w) => w.active && w.goal_id === this.hooks.goalId() && !this.busy.has(w.id))
|
|
277
|
+
.flatMap((w) => [
|
|
278
|
+
w.next_check,
|
|
279
|
+
w.next_report ?? Infinity,
|
|
280
|
+
w.deadline_reported ? Infinity : (w.deadline ?? Infinity),
|
|
281
|
+
]);
|
|
282
|
+
if (!times.length)
|
|
283
|
+
return;
|
|
284
|
+
this.timer = setTimeout(() => {
|
|
285
|
+
void this.runDue().catch(this.hooks.onError);
|
|
286
|
+
}, Math.max(0, Math.min(2147483647, Math.min(...times) - this.now())));
|
|
287
|
+
this.timer.unref?.();
|
|
288
|
+
}
|
|
289
|
+
async runDue() {
|
|
290
|
+
if (!this.owned() || this.unavailable)
|
|
291
|
+
return;
|
|
292
|
+
const now = this.now();
|
|
293
|
+
const probes = [];
|
|
294
|
+
for (const watch of this.watches.values()) {
|
|
295
|
+
if (!watch.active || watch.goal_id !== this.hooks.goalId() || this.busy.has(watch.id))
|
|
296
|
+
continue;
|
|
297
|
+
if (Math.min(watch.next_check, watch.next_report ?? Infinity, watch.deadline_reported ? Infinity : (watch.deadline ?? Infinity)) > now)
|
|
298
|
+
continue;
|
|
299
|
+
this.busy.add(watch.id);
|
|
300
|
+
// Skip stale checks, not requested reports. Reports retain their own scheduled timestamps.
|
|
301
|
+
if (watch.next_check <= now)
|
|
302
|
+
watch.next_check += (Math.floor((now - watch.next_check) / watch.interval_ms) + 1) * watch.interval_ms;
|
|
303
|
+
probes.push((async () => {
|
|
304
|
+
try {
|
|
305
|
+
await this.hooks.probe({
|
|
306
|
+
id: watch.id,
|
|
307
|
+
generation: watch.generation,
|
|
308
|
+
resource_id: watch.resource_id,
|
|
309
|
+
job_id: watch.job_id,
|
|
310
|
+
completion_source: watch.completion_source,
|
|
311
|
+
command: watch.probe_command,
|
|
312
|
+
timeout_ms: watch.timeout_ms,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
await this.observe({
|
|
317
|
+
id: watch.id,
|
|
318
|
+
generation: watch.generation,
|
|
319
|
+
source: "probe",
|
|
320
|
+
error: String(error),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
})());
|
|
324
|
+
}
|
|
325
|
+
await Promise.all(probes);
|
|
326
|
+
this.schedule();
|
|
327
|
+
}
|
|
328
|
+
async observe(input) {
|
|
329
|
+
return this.serialize(async () => {
|
|
330
|
+
const watch = typeof input.id === "string" ? this.watches.get(input.id) : undefined;
|
|
331
|
+
if (!watch ||
|
|
332
|
+
input.generation !== watch.generation ||
|
|
333
|
+
!this.owned() ||
|
|
334
|
+
!watch.active ||
|
|
335
|
+
watch.goal_id !== this.hooks.goalId())
|
|
336
|
+
return { ignored: true };
|
|
337
|
+
if (input.source === "handle" && watch.completion_source !== "handle")
|
|
338
|
+
return { ignored: true };
|
|
339
|
+
this.busy.delete(watch.id);
|
|
340
|
+
const now = this.now();
|
|
341
|
+
const previous = watch.latest.state;
|
|
342
|
+
const error = typeof input.error === "string" ? input.error : undefined;
|
|
343
|
+
const observed = input.observation;
|
|
344
|
+
let evidence;
|
|
345
|
+
if (typeof input.output === "string" && this.hooks.retain)
|
|
346
|
+
evidence = await this.hooks.retain(input.output);
|
|
347
|
+
if (!this.owned() || this.watches.get(watch.id) !== watch || watch.goal_id !== this.hooks.goalId())
|
|
348
|
+
return { ignored: true };
|
|
349
|
+
const before = structuredClone(watch);
|
|
350
|
+
let observation;
|
|
351
|
+
try {
|
|
352
|
+
if (error)
|
|
353
|
+
throw new Error(error);
|
|
354
|
+
if (!record(observed))
|
|
355
|
+
throw new Error("Probe observation is unavailable");
|
|
356
|
+
const jobId = text(observed.job_id, "job_id", 256);
|
|
357
|
+
if (jobId !== watch.job_id)
|
|
358
|
+
throw new Error("Probe job identity does not match this watch");
|
|
359
|
+
const state = text(observed.state, "state", 32);
|
|
360
|
+
if (!["running", "pending", "unknown", ...TERMINAL].includes(state))
|
|
361
|
+
throw new Error("Unrecognized job state");
|
|
362
|
+
const observedAt = text(observed.observed_at, "observed_at", 64);
|
|
363
|
+
if (!Number.isFinite(Date.parse(observedAt)))
|
|
364
|
+
throw new Error("Invalid observation time");
|
|
365
|
+
if (!record(observed.progress ?? {}))
|
|
366
|
+
throw new Error("progress must be an object");
|
|
367
|
+
observation = {
|
|
368
|
+
observed_at: observedAt,
|
|
369
|
+
job_id: jobId,
|
|
370
|
+
state,
|
|
371
|
+
progress: observed.progress ?? {},
|
|
372
|
+
attention: strings(observed.attention, "attention"),
|
|
373
|
+
evidence: strings(observed.evidence, "evidence"),
|
|
374
|
+
};
|
|
375
|
+
// Handle authority is explicit: progress probes cannot finish a live local handle.
|
|
376
|
+
if (input.source !== "handle" && watch.completion_source === "handle" && TERMINAL.has(state)) {
|
|
377
|
+
if (state !== "succeeded")
|
|
378
|
+
observation.attention.push(`Probe reports job ${state}; waiting for authoritative handle`);
|
|
379
|
+
observation.state = "running";
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch (failure) {
|
|
383
|
+
observation = {
|
|
384
|
+
observed_at: new Date(now).toISOString(),
|
|
385
|
+
job_id: watch.job_id,
|
|
386
|
+
state: "unknown",
|
|
387
|
+
progress: {},
|
|
388
|
+
attention: [],
|
|
389
|
+
evidence: watch.latest.evidence,
|
|
390
|
+
observation_error: String(failure).slice(0, 256),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
if (evidence)
|
|
394
|
+
observation.evidence = [evidence, ...observation.evidence].slice(0, 16);
|
|
395
|
+
const problem = observation.observation_error ?? (observation.state === "unknown" ? "Job state unknown" : undefined);
|
|
396
|
+
const selected = {};
|
|
397
|
+
for (const field of [...new Set([...watch.fields, "exit_code", "duration_seconds"])]) {
|
|
398
|
+
const value = observation.progress[field];
|
|
399
|
+
if (value === undefined)
|
|
400
|
+
continue;
|
|
401
|
+
if (Buffer.byteLength(JSON.stringify(value)) > 512) {
|
|
402
|
+
selected[field] = { unavailable: "value exceeds 512-byte watch field budget; inspect evidence" };
|
|
403
|
+
observation.attention.push(`Oversized selected field: ${field}`);
|
|
404
|
+
}
|
|
405
|
+
else
|
|
406
|
+
selected[field] = value;
|
|
407
|
+
}
|
|
408
|
+
const compared = JSON.stringify(watch.fields.map((field) => [field, selected[field]]));
|
|
409
|
+
const attention = JSON.stringify(observation.attention);
|
|
410
|
+
const reasons = [];
|
|
411
|
+
if (TERMINAL.has(observation.state)) {
|
|
412
|
+
reasons.push(observation.state === "succeeded" ? "terminal" : "failure");
|
|
413
|
+
watch.active = false;
|
|
414
|
+
}
|
|
415
|
+
if (input.source === "handle" && typeof input.error === "string" && input.error.includes("handle unavailable"))
|
|
416
|
+
watch.active = false;
|
|
417
|
+
if (problem && problem !== watch.last_problem)
|
|
418
|
+
reasons.push("observability");
|
|
419
|
+
if (observation.attention.length && attention !== watch.last_attention)
|
|
420
|
+
reasons.push("attention");
|
|
421
|
+
if (watch.notify === "changes" && watch.compared !== undefined && compared !== watch.compared)
|
|
422
|
+
reasons.push("progress");
|
|
423
|
+
if (watch.deadline !== undefined && now >= watch.deadline && !watch.deadline_reported) {
|
|
424
|
+
reasons.push("deadline");
|
|
425
|
+
watch.deadline_reported = true;
|
|
426
|
+
}
|
|
427
|
+
watch.latest = { ...observation, progress: selected };
|
|
428
|
+
watch.compared = compared;
|
|
429
|
+
watch.last_problem = problem;
|
|
430
|
+
watch.last_attention = attention;
|
|
431
|
+
const emit = (eventReasons, due) => {
|
|
432
|
+
const id = randomUUID();
|
|
433
|
+
const value = {
|
|
434
|
+
watch_id: watch.id,
|
|
435
|
+
goal_id: watch.goal_id,
|
|
436
|
+
event_id: id,
|
|
437
|
+
...watch.latest,
|
|
438
|
+
previous_state: previous,
|
|
439
|
+
reasons: eventReasons,
|
|
440
|
+
...(due === undefined
|
|
441
|
+
? {}
|
|
442
|
+
: { scheduled_for: new Date(due).toISOString(), late_by_ms: Math.max(0, now - due) }),
|
|
443
|
+
};
|
|
444
|
+
watch.pending.push({ id, text: bounded(value) });
|
|
445
|
+
};
|
|
446
|
+
if (reasons.length)
|
|
447
|
+
emit(reasons);
|
|
448
|
+
if (watch.report_ms && watch.next_report !== undefined && watch.next_report <= now) {
|
|
449
|
+
while (watch.next_report <= now) {
|
|
450
|
+
emit(["report"], watch.next_report);
|
|
451
|
+
watch.next_report += watch.report_ms;
|
|
452
|
+
if (this.stateBytes() > this.stateBudget() - STATE_REFUSAL_RESERVE) {
|
|
453
|
+
await this.refusePendingBudget(watch, before);
|
|
454
|
+
return { accepted: false, reason: "monitoring_unavailable" };
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
if (this.stateBytes() > this.stateBudget() - STATE_REFUSAL_RESERVE) {
|
|
459
|
+
await this.refusePendingBudget(watch, before);
|
|
460
|
+
return { accepted: false, reason: "monitoring_unavailable" };
|
|
461
|
+
}
|
|
462
|
+
// Persist prepared evidence before native queue admission.
|
|
463
|
+
await this.save();
|
|
464
|
+
await this.flushPending();
|
|
465
|
+
await this.save();
|
|
466
|
+
this.schedule();
|
|
467
|
+
return { accepted: true };
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
flushPending() {
|
|
471
|
+
if (!this.flushing)
|
|
472
|
+
this.flushing = this.flushPrepared().finally(() => {
|
|
473
|
+
this.flushing = undefined;
|
|
474
|
+
});
|
|
475
|
+
return this.flushing;
|
|
476
|
+
}
|
|
477
|
+
async flushPrepared() {
|
|
478
|
+
if (!this.owned())
|
|
479
|
+
return;
|
|
480
|
+
let changed = false;
|
|
481
|
+
for (const watch of this.watches.values()) {
|
|
482
|
+
for (const event of [...watch.pending]) {
|
|
483
|
+
if (!this.owned())
|
|
484
|
+
return;
|
|
485
|
+
if ((!event.admitted && watch.goal_id !== this.hooks.goalId()) || this.queued.has(event.id))
|
|
486
|
+
continue;
|
|
487
|
+
let deliveryText = event.text;
|
|
488
|
+
if (event.reconcile) {
|
|
489
|
+
let original = {};
|
|
490
|
+
try {
|
|
491
|
+
original = JSON.parse(event.text);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
/* unavailable prior body */
|
|
495
|
+
}
|
|
496
|
+
deliveryText = bounded({
|
|
497
|
+
...original,
|
|
498
|
+
event_id: `${event.id}:delivery-unknown`,
|
|
499
|
+
original_event_id: event.id,
|
|
500
|
+
original_state: original.state,
|
|
501
|
+
state: "unknown",
|
|
502
|
+
reasons: ["delivery_unknown"],
|
|
503
|
+
observation_error: "Delivery before interrupted shutdown is uncertain; reconcile evidence before repeating actions",
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (this.hooks.waitForDelivery) {
|
|
507
|
+
event.uncertain = true;
|
|
508
|
+
await this.save();
|
|
509
|
+
}
|
|
510
|
+
if (!this.owned() || (!event.admitted && watch.goal_id !== this.hooks.goalId()))
|
|
511
|
+
return;
|
|
512
|
+
if (!this.hooks.admit(deliveryText, event.id)) {
|
|
513
|
+
event.uncertain = false;
|
|
514
|
+
await this.save();
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
if (this.hooks.waitForDelivery) {
|
|
518
|
+
event.admitted = true;
|
|
519
|
+
this.queued.add(event.id);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
watch.last_admitted = event.id;
|
|
523
|
+
watch.pending.splice(watch.pending.indexOf(event), 1);
|
|
524
|
+
}
|
|
525
|
+
changed = true;
|
|
526
|
+
if (watch.goal_id === this.hooks.goalId() && this.parked?.ids.includes(watch.id))
|
|
527
|
+
this.parked = undefined;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (changed) {
|
|
531
|
+
await this.save();
|
|
532
|
+
this.schedule();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
acknowledge(id) {
|
|
536
|
+
return this.serialize(async () => {
|
|
537
|
+
if (this.stopped)
|
|
538
|
+
return;
|
|
539
|
+
this.queued.delete(id);
|
|
540
|
+
for (const watch of this.watches.values()) {
|
|
541
|
+
const index = watch.pending.findIndex((event) => event.id === id);
|
|
542
|
+
if (index >= 0) {
|
|
543
|
+
watch.pending.splice(index, 1);
|
|
544
|
+
watch.last_admitted = id;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
await this.save();
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
releaseDelivery(id, knownUndelivered) {
|
|
551
|
+
return this.serialize(async () => {
|
|
552
|
+
this.queued.delete(id);
|
|
553
|
+
for (const watch of this.watches.values()) {
|
|
554
|
+
const event = watch.pending.find((item) => item.id === id);
|
|
555
|
+
if (event && knownUndelivered)
|
|
556
|
+
event.uncertain = false;
|
|
557
|
+
}
|
|
558
|
+
await this.save();
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/** Called only after native request/agent settlement during clean async shutdown. */
|
|
562
|
+
async settleShutdown(undelivered) {
|
|
563
|
+
await this.flushing;
|
|
564
|
+
return this.serialize(async () => {
|
|
565
|
+
for (const watch of this.watches.values())
|
|
566
|
+
for (const event of watch.pending)
|
|
567
|
+
if (undelivered.has(event.id))
|
|
568
|
+
event.uncertain = false;
|
|
569
|
+
this.dispose();
|
|
570
|
+
await this.hooks.persist(this.snapshot());
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
async restore(value) {
|
|
574
|
+
if (!record(value) ||
|
|
575
|
+
value.version !== 1 ||
|
|
576
|
+
value.session_id !== this.hooks.sessionId ||
|
|
577
|
+
value.session_file !== this.hooks.sessionFile ||
|
|
578
|
+
!Array.isArray(value.watches) ||
|
|
579
|
+
value.watches.length > MAX_WATCHES)
|
|
580
|
+
return;
|
|
581
|
+
this.unavailable = typeof value.unavailable === "string" ? value.unavailable : undefined;
|
|
582
|
+
// Only host-persisted declarations reach here; PID/process objects are deliberately absent.
|
|
583
|
+
for (const item of value.watches) {
|
|
584
|
+
if (!record(item) ||
|
|
585
|
+
typeof item.id !== "string" ||
|
|
586
|
+
typeof item.generation !== "string" ||
|
|
587
|
+
typeof item.resource_id !== "string" ||
|
|
588
|
+
typeof item.job_id !== "string" ||
|
|
589
|
+
!["handle", "probe"].includes(item.completion_source) ||
|
|
590
|
+
!Number.isFinite(item.interval_ms) ||
|
|
591
|
+
item.interval_ms <= 0 ||
|
|
592
|
+
!Number.isFinite(item.next_check) ||
|
|
593
|
+
!Number.isFinite(item.timeout_ms) ||
|
|
594
|
+
item.timeout_ms < 1 ||
|
|
595
|
+
item.timeout_ms > 300000 ||
|
|
596
|
+
(item.report_ms !== undefined && (!Number.isFinite(item.report_ms) || item.report_ms <= 0)) ||
|
|
597
|
+
(item.probe_command !== undefined &&
|
|
598
|
+
(typeof item.probe_command !== "string" || Buffer.byteLength(item.probe_command) > 8192)) ||
|
|
599
|
+
!Array.isArray(item.fields) ||
|
|
600
|
+
item.fields.some((field) => typeof field !== "string") ||
|
|
601
|
+
!record(item.latest) ||
|
|
602
|
+
!Array.isArray(item.pending) ||
|
|
603
|
+
item.pending.some((event) => !event || typeof event.id !== "string" || typeof event.text !== "string"))
|
|
604
|
+
continue;
|
|
605
|
+
const restored = structuredClone(item);
|
|
606
|
+
for (const event of restored.pending) {
|
|
607
|
+
if (event.uncertain) {
|
|
608
|
+
event.reconcile = true;
|
|
609
|
+
event.admitted = true;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
this.watches.set(item.id, restored);
|
|
613
|
+
}
|
|
614
|
+
this.parked =
|
|
615
|
+
record(value.parked) &&
|
|
616
|
+
Array.isArray(value.parked.ids) &&
|
|
617
|
+
value.parked.ids.every((id) => typeof id === "string")
|
|
618
|
+
? value.parked
|
|
619
|
+
: undefined;
|
|
620
|
+
await this.flushPending();
|
|
621
|
+
for (const watch of this.watches.values()) {
|
|
622
|
+
if (!watch.active)
|
|
623
|
+
continue;
|
|
624
|
+
watch.generation = randomUUID();
|
|
625
|
+
if (watch.completion_source === "probe" && watch.probe_command) {
|
|
626
|
+
watch.next_check = this.now();
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
await this.observe({
|
|
630
|
+
id: watch.id,
|
|
631
|
+
generation: watch.generation,
|
|
632
|
+
error: "Kernel/session restored without an authoritative probe; handle identity is unknown",
|
|
633
|
+
});
|
|
634
|
+
watch.active = false;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
await this.save();
|
|
638
|
+
this.schedule();
|
|
639
|
+
}
|
|
640
|
+
dispose() {
|
|
641
|
+
this.stopped = true;
|
|
642
|
+
if (this.timer)
|
|
643
|
+
clearTimeout(this.timer);
|
|
644
|
+
for (const watch of this.watches.values())
|
|
645
|
+
this.hooks.cancelProbe(watch.id, watch.generation);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
//# sourceMappingURL=job-watch.js.map
|