@zq-silk/yui 2.17.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commandCatalog.js +2 -1
- package/dist/commands/controllerCommands.js +14 -4
- package/dist/commands/durableJobCommands.js +50 -3
- package/dist/commands/globalRoleCommands.js +18 -12
- package/dist/commands/inboxCommands.js +1 -0
- package/dist/commands/operatorCommands.js +10 -2
- package/dist/controlPlaneCli.js +21 -4
- package/dist/controller/clientRuntime.js +34 -13
- package/dist/controller/controller.js +45 -22
- package/dist/controller/diagnosticJournal.js +214 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +143 -101
- package/dist/controller/globalInputDelivery.js +6 -2
- package/dist/controller/globalRuntimeAttention.js +11 -3
- package/dist/controller/jobControl.js +1 -1
- package/dist/controller/jobDefinitionControl.js +24 -13
- package/dist/controller/jobSupervisor.js +4 -0
- package/dist/controller/messageHealth.js +354 -0
- package/dist/controller/runtime.js +75 -5
- package/dist/controller/taskHostTerminalRecovery.js +11 -0
- package/dist/controller/workerSchedulerStore.js +24 -0
- package/dist/core/boundedRpc.js +4 -0
- package/dist/core/controllerServer.js +1 -1
- package/dist/core/controllerTelemetry.js +92 -19
- package/dist/job/globalShell.js +51 -0
- package/dist/job/jobBusinessResult.js +4 -3
- package/dist/job/jobDefinition.js +6 -1
- package/dist/job/jobRunner.js +9 -0
- package/dist/job/scheduleService.js +2 -0
- package/dist/message/globalInboxRecovery.js +30 -19
- package/dist/message/globalProviderRetry.js +7 -1
- package/dist/runtime/builtinAgentErrorMappers.js +4 -0
- package/dist/runtime/globalSessionHealth.js +39 -0
- package/dist/scheduler/roleRunStall.js +10 -10
- package/dist/storage/contextRecords.js +16 -0
- package/dist/storage/persistenceWorker.js +41 -19
- package/dist/storage/sqliteSchema.js +35 -11
- package/dist/storage/sqliteStore.js +102 -8
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +25 -11
- package/dist/storage/taskReadRevisionSchema.js +39 -0
- package/dist/web/assets/client/app/catalog.js +0 -29
- package/dist/web/assets/client/app/jobs.js +115 -0
- package/dist/web/assets/client/app/main.js +17 -6
- package/dist/web/assets/client/app/manage.js +1 -0
- package/dist/web/assets/client/app/settings.js +1 -5
- package/dist/web/assets/client/i18n/en.js +203 -18
- package/dist/web/assets/client/i18n/zh-CN.js +202 -17
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/layout/manageShell.js +61 -15
- package/dist/web/assets/client/layout/router.js +2 -1
- package/dist/web/assets/client/lib/prefs.js +1 -1
- package/dist/web/assets/client/views/jobs.js +1042 -160
- package/dist/web/assets/client/views/sidebar.js +3 -0
- package/dist/web/assets/shared/manageNav.js +3 -1
- package/dist/web/assets/shell/appRail.js +2 -1
- package/dist/web/assets/shell/manage.js +7 -2
- package/dist/web/assets/shell/models.js +4 -3
- package/dist/web/assets/shell/settingsContent.js +0 -3
- package/dist/web/assets/shell/sidebar.js +0 -1
- package/dist/web/assets/styles/views/jobs.js +301 -12
- package/dist/web/assets/styles/views/sidebar.js +1 -2
- package/dist/web/webServer.js +28 -1
- package/docs/global-shell-jobs.md +78 -0
- package/docs/job-runs.md +6 -3
- package/docs/managed-turn-and-session-runtime.md +170 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +116 -0
- package/docs/release-workflow.md +63 -0
- package/docs/release-workflow.zh-CN.md +44 -0
- package/docs/storage-baseline.md +14 -1
- package/docs/storage-baseline.zh-CN.md +6 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
import { runRpcWorker } from "../core/boundedRpc.js";
|
|
34
34
|
import { SqliteTaskStore } from "./sqliteStore.js";
|
|
35
35
|
import { StorageRecordError } from "./taskStore.js";
|
|
36
|
+
import { randomUUID } from "node:crypto";
|
|
37
|
+
import { ControllerWorkObserver, compactWorkObservation } from "../core/controllerTelemetry.js";
|
|
36
38
|
// -- method invocation -------------------------------------------------------
|
|
37
39
|
function invokeMethod(target, method, args) {
|
|
38
40
|
const fn = target[method];
|
|
@@ -41,6 +43,13 @@ function invokeMethod(target, method, args) {
|
|
|
41
43
|
}
|
|
42
44
|
return fn.apply(target, args);
|
|
43
45
|
}
|
|
46
|
+
const workerInstance = randomUUID();
|
|
47
|
+
const work = new ControllerWorkObserver();
|
|
48
|
+
function observation() {
|
|
49
|
+
return { instance: workerInstance, work: compactWorkObservation(work),
|
|
50
|
+
database: state.writer?.storageObservationMetrics() ?? null,
|
|
51
|
+
reads: state.observer?.compactReadObservationMetrics?.() ?? null };
|
|
52
|
+
}
|
|
44
53
|
/**
|
|
45
54
|
* Sentinel returned by a handler when the durable outbox already records the
|
|
46
55
|
* request's effect (§5.4). The host's `result` builder maps it to an
|
|
@@ -80,7 +89,10 @@ async function handleInit(request) {
|
|
|
80
89
|
state.observer = {
|
|
81
90
|
invoke(method, args) {
|
|
82
91
|
return invokeMethod(host, method, args);
|
|
83
|
-
}
|
|
92
|
+
},
|
|
93
|
+
...(host.compactReadObservationMetrics === undefined ? {} : {
|
|
94
|
+
compactReadObservationMetrics: () => host.compactReadObservationMetrics()
|
|
95
|
+
})
|
|
84
96
|
};
|
|
85
97
|
}
|
|
86
98
|
}
|
|
@@ -162,21 +174,31 @@ runRpcWorker({
|
|
|
162
174
|
},
|
|
163
175
|
init: (request) => handleInit(request),
|
|
164
176
|
handle: async (request) => {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
const finish = work.start(request.kind, "requestId" in request ? request.requestId : undefined);
|
|
178
|
+
try {
|
|
179
|
+
switch (request.kind) {
|
|
180
|
+
case "call":
|
|
181
|
+
return await handleCall(request);
|
|
182
|
+
case "transaction":
|
|
183
|
+
return await handleTransaction(request);
|
|
184
|
+
case "observer":
|
|
185
|
+
return await handleObserver(request);
|
|
186
|
+
case "telemetry":
|
|
187
|
+
if (state.writer === undefined)
|
|
188
|
+
throw new Error("Worker not initialized.");
|
|
189
|
+
return state.writer.flushTelemetry(request.entries, request.runCap);
|
|
190
|
+
case "init":
|
|
191
|
+
case "cancel":
|
|
192
|
+
case "shutdown":
|
|
193
|
+
throw new Error(`Unexpected request kind for handle: ${request.kind}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
finish(true);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
finish();
|
|
180
202
|
}
|
|
181
203
|
},
|
|
182
204
|
cancel: (requestId) => {
|
|
@@ -196,7 +218,7 @@ runRpcWorker({
|
|
|
196
218
|
},
|
|
197
219
|
ready: () => ({ kind: "ready" }),
|
|
198
220
|
result: (requestId, value) => (value === ALREADY_APPLIED
|
|
199
|
-
? { kind: "already-applied", requestId }
|
|
200
|
-
: { kind: "result", requestId, result: value }),
|
|
201
|
-
error: (requestId, error) => ({ kind: "error", requestId, error })
|
|
221
|
+
? { kind: "already-applied", requestId, observation: observation() }
|
|
222
|
+
: { kind: "result", requestId, result: value, observation: observation() }),
|
|
223
|
+
error: (requestId, error) => ({ kind: "error", requestId, error, observation: observation() })
|
|
202
224
|
});
|
|
@@ -5,6 +5,7 @@ import { migrateTaskMainWorkspaces } from "./upgrade/taskMainWorkspaceMigration.
|
|
|
5
5
|
import { migrateProjectKnowledge } from "./upgrade/projectKnowledgeMigration.js";
|
|
6
6
|
import { JOB_SCHEMA_SQL } from "./jobSchema.js";
|
|
7
7
|
import { AGENT_JOB_SCHEMA_SQL } from "./agentJobSchema.js";
|
|
8
|
+
import { TASK_READ_REVISION_SCHEMA_SQL } from "./taskReadRevisionSchema.js";
|
|
8
9
|
import { SCHEDULE_SCHEMA_SQL } from "./scheduleSchema.js";
|
|
9
10
|
import { GLOBAL_SESSION_TABLE_SQL, SESSION_HISTORY_UPGRADE_SQL } from "./sessionHistorySchema.js";
|
|
10
11
|
import { migrateJobDefinitions } from "./upgrade/jobDefinitionMigration.js";
|
|
@@ -13,7 +14,7 @@ import { migrateScheduleDispatch } from "./upgrade/scheduleDispatchMigration.js"
|
|
|
13
14
|
import { migrateProjectSyncClock } from "./upgrade/projectSyncClockMigration.js";
|
|
14
15
|
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION, STORAGE_FORMAT, isMinorStorageUpgrade, isStorageVersion, storageVersionParts } from "./storageVersions.js";
|
|
15
16
|
export { BASELINE_SCHEMA_SQL };
|
|
16
|
-
export const SQLITE_SCHEMA_TABLES = [...BASELINE_SCHEMA_TABLES, "job_definitions", "global_job_runs", "schedules", "agent_job_runs"];
|
|
17
|
+
export const SQLITE_SCHEMA_TABLES = [...BASELINE_SCHEMA_TABLES, "job_definitions", "global_job_runs", "schedules", "agent_job_runs", "task_read_revisions"];
|
|
17
18
|
export const TELEMETRY_KEEP_PER_RUN = 200;
|
|
18
19
|
export const TELEMETRY_RUN_CAP = 50_000;
|
|
19
20
|
export const BASELINE_SCHEMA_CHECKSUM = createHash("sha256").update(BASELINE_SCHEMA_SQL).digest("hex");
|
|
@@ -23,7 +24,9 @@ export const SCHEDULE_SCHEMA_CHECKSUM = createHash("sha256").update(SCHEDULE_SCH
|
|
|
23
24
|
// Fresh Homes create the final layout directly; published baseline SQL stays immutable.
|
|
24
25
|
const SESSION_SCHEMA_SQL = SCHEDULE_SCHEMA_SQL_COMPLETE.replace(/CREATE TABLE global_role_session_sets \([\s\S]*?\n\);/, GLOBAL_SESSION_TABLE_SQL);
|
|
25
26
|
const SESSION_SCHEMA_CHECKSUM = createHash("sha256").update(SESSION_SCHEMA_SQL).digest("hex");
|
|
26
|
-
export const
|
|
27
|
+
export const PRE_READ_REVISION_SCHEMA_SQL = SESSION_SCHEMA_SQL + AGENT_JOB_SCHEMA_SQL;
|
|
28
|
+
const PRE_READ_REVISION_SCHEMA_CHECKSUM = createHash("sha256").update(PRE_READ_REVISION_SCHEMA_SQL).digest("hex");
|
|
29
|
+
export const CURRENT_SCHEMA_SQL = PRE_READ_REVISION_SCHEMA_SQL + TASK_READ_REVISION_SCHEMA_SQL;
|
|
27
30
|
export const CURRENT_SCHEMA_CHECKSUM = createHash("sha256").update(CURRENT_SCHEMA_SQL).digest("hex");
|
|
28
31
|
export class SqliteSchemaError extends Error {
|
|
29
32
|
code;
|
|
@@ -230,7 +233,7 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
230
233
|
name: "job-agent-execution",
|
|
231
234
|
introducedIn: "next",
|
|
232
235
|
sourceChecksum: SESSION_SCHEMA_CHECKSUM,
|
|
233
|
-
targetChecksum:
|
|
236
|
+
targetChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
234
237
|
// Existing records and immutable artifacts remain byte-for-byte unchanged.
|
|
235
238
|
// Declares Agent definitions/Runs and optional Job ownership of a non-Task Role.
|
|
236
239
|
sql: AGENT_JOB_SCHEMA_SQL
|
|
@@ -239,8 +242,8 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
239
242
|
toVersion: "1.21",
|
|
240
243
|
name: "project-sync-clock",
|
|
241
244
|
introducedIn: "next",
|
|
242
|
-
sourceChecksum:
|
|
243
|
-
targetChecksum:
|
|
245
|
+
sourceChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
246
|
+
targetChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
244
247
|
sql: "",
|
|
245
248
|
dataMigration: "project-sync-clock"
|
|
246
249
|
}, {
|
|
@@ -248,11 +251,31 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
248
251
|
toVersion: "1.22",
|
|
249
252
|
name: "role-profile-availability",
|
|
250
253
|
introducedIn: "next",
|
|
251
|
-
sourceChecksum:
|
|
252
|
-
targetChecksum:
|
|
254
|
+
sourceChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
255
|
+
targetChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
253
256
|
// Optional enabled flag; absence means enabled. Preserve all historical
|
|
254
257
|
// Role/Profile bytes, frozen contexts and Session snapshots.
|
|
255
258
|
sql: ""
|
|
259
|
+
}, {
|
|
260
|
+
fromVersion: "1.22",
|
|
261
|
+
toVersion: "1.23",
|
|
262
|
+
name: "global-shell-scripts",
|
|
263
|
+
introducedIn: "next",
|
|
264
|
+
sourceChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
265
|
+
targetChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
266
|
+
// Additive global Shell definitions with revisioned script text and exact
|
|
267
|
+
// Run snapshots. Historical definitions/Runs stay byte-for-byte unchanged.
|
|
268
|
+
sql: ""
|
|
269
|
+
}, {
|
|
270
|
+
fromVersion: "1.23",
|
|
271
|
+
toVersion: "1.24",
|
|
272
|
+
name: "task-read-revisions",
|
|
273
|
+
introducedIn: "next",
|
|
274
|
+
sourceChecksum: PRE_READ_REVISION_SCHEMA_CHECKSUM,
|
|
275
|
+
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
276
|
+
// No payload rewriting or historical revision invention. A newly opened
|
|
277
|
+
// reader starts empty; zero is the initial token until the first mutation.
|
|
278
|
+
sql: TASK_READ_REVISION_SCHEMA_SQL
|
|
256
279
|
}]);
|
|
257
280
|
export function storageMinorUpgradePlan(from) {
|
|
258
281
|
if (from === CURRENT_STORAGE_VERSION)
|
|
@@ -282,10 +305,11 @@ export function validateSqliteSchemaObjects(db, version = CURRENT_STORAGE_VERSIO
|
|
|
282
305
|
const checksum = version === CURRENT_STORAGE_VERSION
|
|
283
306
|
? CURRENT_SCHEMA_CHECKSUM : storageMinorUpgradePlan(version)?.[0]?.sourceChecksum;
|
|
284
307
|
const sql = checksum === CURRENT_SCHEMA_CHECKSUM ? CURRENT_SCHEMA_SQL
|
|
285
|
-
: checksum ===
|
|
286
|
-
: checksum ===
|
|
287
|
-
: checksum ===
|
|
288
|
-
: checksum ===
|
|
308
|
+
: checksum === PRE_READ_REVISION_SCHEMA_CHECKSUM ? PRE_READ_REVISION_SCHEMA_SQL
|
|
309
|
+
: checksum === SESSION_SCHEMA_CHECKSUM ? SESSION_SCHEMA_SQL
|
|
310
|
+
: checksum === SCHEDULE_SCHEMA_CHECKSUM ? SCHEDULE_SCHEMA_SQL_COMPLETE
|
|
311
|
+
: checksum === JOB_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL + JOB_SCHEMA_SQL
|
|
312
|
+
: checksum === BASELINE_SCHEMA_CHECKSUM ? BASELINE_SCHEMA_SQL : undefined;
|
|
289
313
|
if (sql === undefined || checksum === undefined)
|
|
290
314
|
throw new SqliteSchemaError(`unsupported schema definition for storage ${version}.`);
|
|
291
315
|
let expectedObjects = schemaObjectsByChecksum.get(checksum);
|
|
@@ -34,6 +34,7 @@ import { validateAgentJobRun, assertAgentJobRunTransition } from "../job/agentJo
|
|
|
34
34
|
* in-process contract or deferred validation phase.
|
|
35
35
|
*/
|
|
36
36
|
import Database from "../sqlite/driver.js";
|
|
37
|
+
import { ControllerWorkObserver, compactWorkObservation } from "../core/controllerTelemetry.js";
|
|
37
38
|
import { findNativeSessionSets } from "./nativeSessionLookup.js";
|
|
38
39
|
import { existsSync, mkdirSync } from "node:fs";
|
|
39
40
|
import { join } from "node:path";
|
|
@@ -141,10 +142,37 @@ function assertRunAdmission(task, run) {
|
|
|
141
142
|
}
|
|
142
143
|
}
|
|
143
144
|
export class SqliteTaskStore {
|
|
145
|
+
#payloadReads = new ControllerWorkObserver();
|
|
146
|
+
#transactions = {
|
|
147
|
+
"write-lock": new ControllerWorkObserver(),
|
|
148
|
+
"write-body": new ControllerWorkObserver(),
|
|
149
|
+
"transaction-commit": new ControllerWorkObserver()
|
|
150
|
+
};
|
|
151
|
+
storageObservationMetrics() {
|
|
152
|
+
return {
|
|
153
|
+
lock: compactWorkObservation(this.#transactions["write-lock"]),
|
|
154
|
+
body: compactWorkObservation(this.#transactions["write-body"]),
|
|
155
|
+
commit: compactWorkObservation(this.#transactions["transaction-commit"]),
|
|
156
|
+
payloadReads: compactWorkObservation(this.#payloadReads)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
#observeTransaction(phase, action) {
|
|
160
|
+
const finish = this.#transactions[phase].start(phase);
|
|
161
|
+
try {
|
|
162
|
+
const result = action();
|
|
163
|
+
finish();
|
|
164
|
+
return result;
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
finish(true);
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
144
171
|
#db;
|
|
145
172
|
#rootDir;
|
|
146
173
|
#openedSchemaHead;
|
|
147
174
|
#inTransaction = false;
|
|
175
|
+
#writeTransaction = false;
|
|
148
176
|
#dirty = false;
|
|
149
177
|
constructor(rootDir, _options = {}) {
|
|
150
178
|
this.#rootDir = rootDir;
|
|
@@ -261,6 +289,13 @@ export class SqliteTaskStore {
|
|
|
261
289
|
listEventsByType(taskId, types) {
|
|
262
290
|
return this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (SELECT value FROM json_each(?))", [taskId, JSON.stringify(types)]), event => event.id);
|
|
263
291
|
}
|
|
292
|
+
hasRuntimeObservation(taskId, identity) {
|
|
293
|
+
const byEvent = "eventId" in identity;
|
|
294
|
+
return this.#db.prepare(`SELECT 1 FROM events WHERE task_id = ?
|
|
295
|
+
AND type = 'runtime.observation'
|
|
296
|
+
AND json_extract(payload, '${byEvent ? "$.payload.eventId" : "$.payload.semanticKey"}') = ?
|
|
297
|
+
LIMIT 1`).get(taskId, byEvent ? identity.eventId : identity.semanticKey) !== undefined;
|
|
298
|
+
}
|
|
264
299
|
readTaskUsageFacts(taskId) {
|
|
265
300
|
// Bounded source selection, not a scan/deserialization of Task history.
|
|
266
301
|
// Strip result bodies before they cross the SQLite boundary.
|
|
@@ -348,13 +383,15 @@ export class SqliteTaskStore {
|
|
|
348
383
|
writeTelemetryBatch(this.#db, entries, runCap);
|
|
349
384
|
}
|
|
350
385
|
#begin() {
|
|
351
|
-
this.#db.exec("BEGIN IMMEDIATE");
|
|
386
|
+
this.#observeTransaction("write-lock", () => this.#db.exec("BEGIN IMMEDIATE"));
|
|
352
387
|
this.#inTransaction = true;
|
|
388
|
+
this.#writeTransaction = true;
|
|
353
389
|
this.#dirty = false;
|
|
354
390
|
}
|
|
355
391
|
#commit() {
|
|
356
|
-
this.#db.exec("COMMIT");
|
|
392
|
+
this.#observeTransaction("transaction-commit", () => this.#db.exec("COMMIT"));
|
|
357
393
|
this.#inTransaction = false;
|
|
394
|
+
this.#writeTransaction = false;
|
|
358
395
|
this.#dirty = false;
|
|
359
396
|
}
|
|
360
397
|
#rollback() {
|
|
@@ -363,6 +400,7 @@ export class SqliteTaskStore {
|
|
|
363
400
|
}
|
|
364
401
|
catch { /* already closed */ }
|
|
365
402
|
this.#inTransaction = false;
|
|
403
|
+
this.#writeTransaction = false;
|
|
366
404
|
this.#dirty = false;
|
|
367
405
|
}
|
|
368
406
|
/** Verify the SQLite schema head at the single write boundary. */
|
|
@@ -393,7 +431,7 @@ export class SqliteTaskStore {
|
|
|
393
431
|
this.#begin();
|
|
394
432
|
try {
|
|
395
433
|
this.#prepareWrite();
|
|
396
|
-
const result = fn
|
|
434
|
+
const result = this.#observeTransaction("write-body", fn);
|
|
397
435
|
this.#bumpRevision();
|
|
398
436
|
this.#commit();
|
|
399
437
|
return result;
|
|
@@ -413,7 +451,7 @@ export class SqliteTaskStore {
|
|
|
413
451
|
// This prevents a long-lived Store from executing SQL after the database
|
|
414
452
|
// schema head has changed underneath it.
|
|
415
453
|
this.#prepareWrite();
|
|
416
|
-
const result = run(this);
|
|
454
|
+
const result = this.#observeTransaction("write-body", () => run(this));
|
|
417
455
|
if (this.#dirty) {
|
|
418
456
|
if (options?.requestId !== undefined) {
|
|
419
457
|
this.#insertOutbox(options.requestId, options.outboxCommand ?? null);
|
|
@@ -438,7 +476,7 @@ export class SqliteTaskStore {
|
|
|
438
476
|
this.#begin();
|
|
439
477
|
try {
|
|
440
478
|
this.#prepareWrite();
|
|
441
|
-
const result = await execute(this);
|
|
479
|
+
const result = await this.#transactions["write-body"].observe("write-body", undefined, () => execute(this));
|
|
442
480
|
if (this.#dirty)
|
|
443
481
|
this.#bumpRevision();
|
|
444
482
|
this.#commit();
|
|
@@ -472,7 +510,7 @@ export class SqliteTaskStore {
|
|
|
472
510
|
if (current !== expectedRevision) {
|
|
473
511
|
throw new StorageConflictError(`Storage revision conflict (expected ${expectedRevision}, found ${current}).`, current);
|
|
474
512
|
}
|
|
475
|
-
const result = execute(this);
|
|
513
|
+
const result = this.#observeTransaction("write-body", () => execute(this));
|
|
476
514
|
if (this.#dirty) {
|
|
477
515
|
this.#prepareWrite();
|
|
478
516
|
if (options?.requestId !== undefined) {
|
|
@@ -496,6 +534,13 @@ export class SqliteTaskStore {
|
|
|
496
534
|
return row.revision;
|
|
497
535
|
}
|
|
498
536
|
getStateRevision() { return this.getRevision(); }
|
|
537
|
+
getTaskReadRevision(taskId) {
|
|
538
|
+
// Uncommitted writes have no durable cache identity, even if rolled back.
|
|
539
|
+
if (this.#writeTransaction)
|
|
540
|
+
return Number.NaN;
|
|
541
|
+
return this.#db.prepare("SELECT revision FROM task_read_revisions WHERE task_id = ?")
|
|
542
|
+
.get(taskId)?.revision ?? 0;
|
|
543
|
+
}
|
|
499
544
|
/**
|
|
500
545
|
* Run an ordered command batch inside one `BEGIN IMMEDIATE … COMMIT`, yielding
|
|
501
546
|
* to the event loop between commands so a cancellation signal can interrupt
|
|
@@ -656,8 +701,19 @@ export class SqliteTaskStore {
|
|
|
656
701
|
return row === undefined ? null : this.#validateTaskPayload(table, row.payload);
|
|
657
702
|
}
|
|
658
703
|
#listPayload(table, where, params) {
|
|
659
|
-
|
|
660
|
-
|
|
704
|
+
// Table names are fixed internal callers; no SQL, parameters or payload
|
|
705
|
+
// text enters diagnostics. Include parsing/validation, not only SQLite IO.
|
|
706
|
+
const finish = this.#payloadReads.start(`list-${table}`);
|
|
707
|
+
try {
|
|
708
|
+
const rows = this.#db.prepare(`SELECT payload FROM ${table} WHERE ${where}`).all(...params);
|
|
709
|
+
const values = rows.map((row) => this.#validateTaskPayload(table, row.payload));
|
|
710
|
+
finish();
|
|
711
|
+
return values;
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
finish(true);
|
|
715
|
+
throw error;
|
|
716
|
+
}
|
|
661
717
|
}
|
|
662
718
|
#validateTaskPayload(table, payload) {
|
|
663
719
|
const record = this.#parse(payload);
|
|
@@ -1019,8 +1075,35 @@ export class SqliteTaskStore {
|
|
|
1019
1075
|
OR json_extract(payload, '$.interruptThen') IS NOT NULL)`);
|
|
1020
1076
|
if (query.interruptThen)
|
|
1021
1077
|
where.push("json_extract(payload, '$.interruptThen') IS NOT NULL");
|
|
1078
|
+
if (query.reservedThen !== undefined) {
|
|
1079
|
+
where.push(`json_extract(payload, '$.delivery') IS NULL
|
|
1080
|
+
AND json_extract(payload, '$.notDelivered') IS NULL
|
|
1081
|
+
AND json_extract(payload, '$.interruptThen.targetNativeSessionId') = @thenSession
|
|
1082
|
+
AND json_extract(payload, '$.interruptThen.targetAgentId') = @thenAgent
|
|
1083
|
+
AND json_extract(payload, '$.interruptThen.targetAuthorityEpoch') = @thenEpoch
|
|
1084
|
+
AND json_extract(payload, '$.interruptThen.targetAuthorityHolderId') = @thenHolder`);
|
|
1085
|
+
Object.assign(params, { thenSession: query.reservedThen.nativeSessionId,
|
|
1086
|
+
thenAgent: query.reservedThen.agentId, thenEpoch: query.reservedThen.authorityEpoch,
|
|
1087
|
+
thenHolder: query.reservedThen.holderId });
|
|
1088
|
+
}
|
|
1022
1089
|
if (query.operatorNotice)
|
|
1023
1090
|
where.push("json_extract(payload, '$.inputControl.requestId') LIKE 'operator-notice:%'");
|
|
1091
|
+
if (query.awaitingDelivery)
|
|
1092
|
+
where.push(`(json_extract(payload, '$.delivery') IS NULL
|
|
1093
|
+
OR json_extract(payload, '$.delivery.via') = 'transport')
|
|
1094
|
+
AND json_extract(payload, '$.inbox.ack') IS NULL
|
|
1095
|
+
AND json_extract(payload, '$.inbox.retirement') IS NULL
|
|
1096
|
+
AND (json_extract(payload, '$.inputControl.action') = 'queue'
|
|
1097
|
+
OR json_extract(payload, '$.interruptThen') IS NOT NULL
|
|
1098
|
+
OR json_extract(payload, '$.control.outcome') IN ('pending','delivery-unknown'))`);
|
|
1099
|
+
if (query.excludeHealthNotices)
|
|
1100
|
+
where.push("COALESCE(json_extract(payload, '$.inputControl.requestId'), '') NOT LIKE 'operator-notice:message-health:%'");
|
|
1101
|
+
if (query.systemBodyPrefix !== undefined) {
|
|
1102
|
+
where.push(`json_extract(payload, '$.kind') = 'system'
|
|
1103
|
+
AND json_extract(payload, '$.author.type') = 'system'
|
|
1104
|
+
AND substr(json_extract(payload, '$.body'), 1, length(@bodyPrefix)) = @bodyPrefix`);
|
|
1105
|
+
params.bodyPrefix = query.systemBodyPrefix;
|
|
1106
|
+
}
|
|
1024
1107
|
if (query.afterSequence !== undefined) {
|
|
1025
1108
|
where.push("seq > @after");
|
|
1026
1109
|
params.after = query.afterSequence;
|
|
@@ -1335,6 +1418,17 @@ export class SqliteTaskStore {
|
|
|
1335
1418
|
listJobDefinitions(scope) {
|
|
1336
1419
|
return this.#listPayload("job_definitions", "scope_key=?", [jobScopeKey(scope)]);
|
|
1337
1420
|
}
|
|
1421
|
+
listJobScopeSummaries() {
|
|
1422
|
+
const rows = this.#db.prepare(`SELECT scope_key, SUM(definitions) AS definitions, SUM(schedules) AS schedules,
|
|
1423
|
+
SUM(enabled) AS enabled FROM (
|
|
1424
|
+
SELECT scope_key, 1 AS definitions, 0 AS schedules, 0 AS enabled FROM job_definitions
|
|
1425
|
+
UNION ALL SELECT scope_key, 0, 1, CASE WHEN state='enabled' THEN 1 ELSE 0 END FROM schedules)
|
|
1426
|
+
GROUP BY scope_key`).all();
|
|
1427
|
+
return rows.map(row => ({
|
|
1428
|
+
scope: row.scope_key === "global" ? { kind: "global" } : { kind: "task", taskId: row.scope_key },
|
|
1429
|
+
definitions: Number(row.definitions), schedules: Number(row.schedules), enabledSchedules: Number(row.enabled)
|
|
1430
|
+
}));
|
|
1431
|
+
}
|
|
1338
1432
|
saveGlobalJobRun(run) {
|
|
1339
1433
|
validateDurableJob(run);
|
|
1340
1434
|
if ("taskId" in run || run.scope !== "global")
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* SQLite is the only product Store. `YUI_STORE_WORKER` selects the observer
|
|
21
21
|
* execution boundary; it does not select another authority.
|
|
22
22
|
*/
|
|
23
|
-
import { BoundedRpcClient, nextRequestId } from "../core/boundedRpc.js";
|
|
23
|
+
import { BoundedRpcClient, deserializeError as deserializeRpcError, nextRequestId } from "../core/boundedRpc.js";
|
|
24
|
+
import { ControllerWorkObserver } from "../core/controllerTelemetry.js";
|
|
24
25
|
import { StorageCancelledError, StorageConflictError, StorageRecordError } from "./taskStore.js";
|
|
25
26
|
// -- Read-only classification ------------------------------------------------
|
|
26
27
|
/**
|
|
@@ -37,6 +38,8 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
37
38
|
"getHomeIdentity",
|
|
38
39
|
"getReviewConfig",
|
|
39
40
|
"getRevision",
|
|
41
|
+
"getStateRevision",
|
|
42
|
+
"getTaskReadRevision",
|
|
40
43
|
"listConfiguredAgents",
|
|
41
44
|
"getConfiguredAgent",
|
|
42
45
|
"listProjects",
|
|
@@ -57,6 +60,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
57
60
|
"queryTaskSearch",
|
|
58
61
|
"latestEventSequence",
|
|
59
62
|
"listEventsByType",
|
|
63
|
+
"hasRuntimeObservation",
|
|
60
64
|
"readTaskUsageFacts",
|
|
61
65
|
"listTaskRunWorkspaceBases",
|
|
62
66
|
"contextInputReferences",
|
|
@@ -72,6 +76,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
72
76
|
"listActiveDurableJobs",
|
|
73
77
|
"getJobDefinition",
|
|
74
78
|
"listJobDefinitions",
|
|
79
|
+
"listJobScopeSummaries",
|
|
75
80
|
"getGlobalJobRun",
|
|
76
81
|
"getAgentJobRun",
|
|
77
82
|
"listAgentJobRuns",
|
|
@@ -137,12 +142,10 @@ function deserializeError(serialized) {
|
|
|
137
142
|
if (name === "StorageCancelledError" || name === "AbortError") {
|
|
138
143
|
return new StorageCancelledError(message);
|
|
139
144
|
}
|
|
140
|
-
|
|
141
|
-
error.name = name;
|
|
142
|
-
return error;
|
|
145
|
+
return deserializeRpcError(serialized);
|
|
143
146
|
}
|
|
144
147
|
// -- Storage protocol adapter -------------------------------------------------
|
|
145
|
-
function storageProtocol(home, options) {
|
|
148
|
+
function storageProtocol(home, options, observe) {
|
|
146
149
|
return {
|
|
147
150
|
initRequest: () => ({
|
|
148
151
|
kind: "init",
|
|
@@ -162,6 +165,8 @@ function storageProtocol(home, options) {
|
|
|
162
165
|
return response.requestId;
|
|
163
166
|
},
|
|
164
167
|
settle: (response, settlement) => {
|
|
168
|
+
if (response.observation !== undefined)
|
|
169
|
+
observe(response.observation);
|
|
165
170
|
if (response.kind === "result") {
|
|
166
171
|
settlement.resolve(response.result);
|
|
167
172
|
return;
|
|
@@ -188,8 +193,17 @@ function storageProtocol(home, options) {
|
|
|
188
193
|
*/
|
|
189
194
|
export class AsyncTaskStoreClient {
|
|
190
195
|
#rpc;
|
|
196
|
+
#waits = new ControllerWorkObserver();
|
|
197
|
+
#worker = null;
|
|
198
|
+
/** Cached observations only: status never waits on a busy persistence worker. */
|
|
199
|
+
observationMetrics() {
|
|
200
|
+
return { roundTrips: this.#waits.snapshot(), worker: this.#worker,
|
|
201
|
+
inFlight: this.inFlight, queueDepth: this.queueDepth };
|
|
202
|
+
}
|
|
191
203
|
constructor(home, options = {}) {
|
|
192
|
-
this.#rpc = new BoundedRpcClient(storageProtocol(home, options
|
|
204
|
+
this.#rpc = new BoundedRpcClient(storageProtocol(home, options, observation => {
|
|
205
|
+
this.#worker = observation;
|
|
206
|
+
}), {
|
|
193
207
|
maxInFlight: options.maxInFlight,
|
|
194
208
|
maxQueue: options.maxQueue,
|
|
195
209
|
workerScript: options.workerScript ?? new URL("./persistenceWorker.js", import.meta.url)
|
|
@@ -198,13 +212,13 @@ export class AsyncTaskStoreClient {
|
|
|
198
212
|
/** Invoke a TaskStore method over the RPC (used by the Proxy). */
|
|
199
213
|
callStore(method, args, options) {
|
|
200
214
|
const requestId = options?.requestId ?? nextRequestId();
|
|
201
|
-
return this.#rpc.send(requestId, {
|
|
215
|
+
return this.#waits.observe("store-call", requestId, () => this.#rpc.send(requestId, {
|
|
202
216
|
kind: "call",
|
|
203
217
|
requestId,
|
|
204
218
|
method,
|
|
205
219
|
args,
|
|
206
220
|
readOnly: isReadOnlyMethod(method)
|
|
207
|
-
}, { signal: options?.signal });
|
|
221
|
+
}, { signal: options?.signal }));
|
|
208
222
|
}
|
|
209
223
|
/** Run a command batch atomically in the worker (§3.2). */
|
|
210
224
|
transactionAsync(commands, options) {
|
|
@@ -213,12 +227,12 @@ export class AsyncTaskStoreClient {
|
|
|
213
227
|
op: command.op,
|
|
214
228
|
args: command.args
|
|
215
229
|
}));
|
|
216
|
-
return this.#rpc.send(requestId, {
|
|
230
|
+
return this.#waits.observe("store-transaction", requestId, () => this.#rpc.send(requestId, {
|
|
217
231
|
kind: "transaction",
|
|
218
232
|
requestId,
|
|
219
233
|
commands: wireCommands,
|
|
220
234
|
...(options?.expectedRevision === undefined ? {} : { expectedRevision: options.expectedRevision })
|
|
221
|
-
}, { signal: options?.signal });
|
|
235
|
+
}, { signal: options?.signal }));
|
|
222
236
|
}
|
|
223
237
|
/**
|
|
224
238
|
* Invoke a controller observer method hosted by the worker (the
|
|
@@ -227,7 +241,7 @@ export class AsyncTaskStoreClient {
|
|
|
227
241
|
*/
|
|
228
242
|
invokeObserver(method, args, options) {
|
|
229
243
|
const requestId = options?.requestId ?? nextRequestId();
|
|
230
|
-
return this.#rpc.send(requestId, { kind: "observer", requestId, method, args }, { signal: options?.signal });
|
|
244
|
+
return this.#waits.observe("store-observer", requestId, () => this.#rpc.send(requestId, { kind: "observer", requestId, method, args }, { signal: options?.signal }));
|
|
231
245
|
}
|
|
232
246
|
/** Close the worker and release its connections. */
|
|
233
247
|
close() {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Storage 1.24 definition: immutable once published. These are cache tokens,
|
|
2
|
+
* not a second Task state or a CAS/authorization source. Triggers cover every
|
|
3
|
+
* source family, including direct SQL, deletions and rolled-back transactions.
|
|
4
|
+
*/
|
|
5
|
+
export const TASK_READ_REVISION_SOURCES = [
|
|
6
|
+
"events", "turns", "work_items", "review_rounds", "change_sets",
|
|
7
|
+
"integration_attempts", "input_requests", "durable_jobs", "messages"
|
|
8
|
+
];
|
|
9
|
+
export const TASK_READ_REVISION_SCHEMA_SQL = `
|
|
10
|
+
CREATE TABLE task_read_revisions (
|
|
11
|
+
task_id TEXT PRIMARY KEY,
|
|
12
|
+
revision INTEGER NOT NULL
|
|
13
|
+
);
|
|
14
|
+
CREATE INDEX idx_runtime_observation_semantic
|
|
15
|
+
ON events(task_id, json_extract(payload, '$.payload.semanticKey'))
|
|
16
|
+
WHERE type = 'runtime.observation';
|
|
17
|
+
CREATE INDEX idx_runtime_observation_event
|
|
18
|
+
ON events(task_id, json_extract(payload, '$.payload.eventId'))
|
|
19
|
+
WHERE type = 'runtime.observation';
|
|
20
|
+
${TASK_READ_REVISION_SOURCES.map(table => `
|
|
21
|
+
CREATE TRIGGER ${table}_read_ai AFTER INSERT ON ${table}
|
|
22
|
+
BEGIN
|
|
23
|
+
INSERT INTO task_read_revisions VALUES (NEW.task_id, 1)
|
|
24
|
+
ON CONFLICT(task_id) DO UPDATE SET revision = revision + 1;
|
|
25
|
+
END;
|
|
26
|
+
CREATE TRIGGER ${table}_read_ad AFTER DELETE ON ${table}
|
|
27
|
+
BEGIN
|
|
28
|
+
INSERT INTO task_read_revisions VALUES (OLD.task_id, 1)
|
|
29
|
+
ON CONFLICT(task_id) DO UPDATE SET revision = revision + 1;
|
|
30
|
+
END;
|
|
31
|
+
CREATE TRIGGER ${table}_read_au AFTER UPDATE ON ${table}
|
|
32
|
+
BEGIN
|
|
33
|
+
INSERT INTO task_read_revisions VALUES (NEW.task_id, 1)
|
|
34
|
+
ON CONFLICT(task_id) DO UPDATE SET revision = revision + 1;
|
|
35
|
+
INSERT INTO task_read_revisions
|
|
36
|
+
SELECT OLD.task_id, 1 WHERE OLD.task_id != NEW.task_id
|
|
37
|
+
ON CONFLICT(task_id) DO UPDATE SET revision = revision + 1;
|
|
38
|
+
END;
|
|
39
|
+
`).join("")}`;
|
|
@@ -111,10 +111,6 @@ function renderSidebar(deps, progress) {
|
|
|
111
111
|
reset(deps, progress);
|
|
112
112
|
});
|
|
113
113
|
el.catalogCount.textContent = fill(t("catalog.count"), { shown: state.tasks.length, total: state.catalogTotal });
|
|
114
|
-
renderSource(el.source, state, t, function (value) {
|
|
115
|
-
state.source = value; state.attentionFilter = null; state.query = ""; el.search.value = "";
|
|
116
|
-
syncUrl(state, false); reset(deps, progress);
|
|
117
|
-
});
|
|
118
114
|
el.catalogReset.hidden = !state.catalogCursor;
|
|
119
115
|
el.catalogNext.hidden = !state.nextCursor && !state.catalogCursor;
|
|
120
116
|
el.catalogAttentionReset.hidden = !state.attentionFilter;
|
|
@@ -122,27 +118,6 @@ function renderSidebar(deps, progress) {
|
|
|
122
118
|
el.lastSync.textContent = state.generatedAt ? formatClock(state.generatedAt, deps.locale()) : "—";
|
|
123
119
|
}
|
|
124
120
|
|
|
125
|
-
// Task category: search and attention are cross-category, so they show "all";
|
|
126
|
-
// picking a category clears them, as the server filter then applies again.
|
|
127
|
-
function renderSource(container, state, t, onPick) {
|
|
128
|
-
const current = state.query.trim() || state.attentionFilter ? "all" : state.source || "ordinary";
|
|
129
|
-
// Bound once; the handler reads the live state, not this render's snapshot.
|
|
130
|
-
if (!container.childElementCount) {
|
|
131
|
-
["ordinary", "job", "all"].forEach(function (value) {
|
|
132
|
-
container.append(h("button.seg-btn", { type: "button", role: "radio", dataset: { source: value },
|
|
133
|
-
onclick: function () {
|
|
134
|
-
if (value !== (state.source || "ordinary") || state.query.trim() || state.attentionFilter) onPick(value);
|
|
135
|
-
} }));
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
container.querySelectorAll("[data-source]").forEach(function (button) {
|
|
139
|
-
const value = button.dataset.source;
|
|
140
|
-
button.textContent = t("jobs.categoryShort." + value);
|
|
141
|
-
button.title = t("jobs.category." + value);
|
|
142
|
-
button.setAttribute("aria-checked", String(value === current));
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
121
|
// Redraw the list in place, keeping its scroll position.
|
|
147
122
|
function renderTaskList(deps) {
|
|
148
123
|
const scroll = deps.el.tasks.scrollTop;
|
|
@@ -175,9 +150,6 @@ function catalogQuery(state) {
|
|
|
175
150
|
if (state.filter !== "all") query.set("status", state.filter);
|
|
176
151
|
if (state.query.trim()) query.set("search", state.query.trim());
|
|
177
152
|
if (state.attentionFilter) query.set("attention", state.attentionFilter);
|
|
178
|
-
// Search and attention remain cross-category discovery; the server owns filtering.
|
|
179
|
-
if (!state.query.trim() && !state.attentionFilter && state.source !== "all")
|
|
180
|
-
query.set("source", state.source || "ordinary");
|
|
181
153
|
if (state.catalogCursor) query.set("cursor", state.catalogCursor);
|
|
182
154
|
return query.toString();
|
|
183
155
|
}
|
|
@@ -233,7 +205,6 @@ function applyFilters(deps, params) {
|
|
|
233
205
|
state.query = params.get("q") || "";
|
|
234
206
|
state.attentionFilter = ATTENTION_KINDS.includes(params.get("attention")) ? params.get("attention") : null;
|
|
235
207
|
state.catalogAll = params.get("all") === "true";
|
|
236
|
-
state.source = ["ordinary", "job", "all"].includes(params.get("source")) ? params.get("source") : "ordinary";
|
|
237
208
|
deps.el.search.value = state.query;
|
|
238
209
|
}
|
|
239
210
|
|