@zq-silk/yui 0.6.0 → 0.6.2
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 +5 -5
- package/dist/agent/managedRuntimeEnvironment.js +2 -1
- package/dist/cli/commandCatalog.js +251 -13
- package/dist/cli/updateOrchestrator.js +8 -0
- package/dist/cli/updatePorts.js +76 -22
- package/dist/cli.js +264 -20
- package/dist/commands/configCommands.js +83 -9
- package/dist/commands/controllerCommands.js +103 -0
- package/dist/commands/deliveryGuardPreflight.js +30 -0
- package/dist/commands/durableJobCommands.js +231 -0
- package/dist/commands/executionAuditCommands.js +193 -0
- package/dist/commands/grantCommands.js +374 -0
- package/dist/commands/projectCommands.js +119 -81
- package/dist/commands/releaseCommands.js +444 -0
- package/dist/commands/resourcesCommands.js +274 -0
- package/dist/commands/sessionCommands.js +104 -0
- package/dist/commands/taskActor.js +117 -0
- package/dist/commands/taskChangeSetCommands.js +60 -0
- package/dist/commands/taskCommands.js +618 -202
- package/dist/commands/taskCompletionGate.js +78 -1
- package/dist/commands/taskContextCommand.js +33 -6
- package/dist/commands/taskInputCommands.js +1 -1
- package/dist/commands/taskIntegrationCommands.js +136 -33
- package/dist/commands/taskIntegrationQueueCommands.js +228 -0
- package/dist/commands/taskNextActionCommand.js +100 -0
- package/dist/commands/taskOverlapCommands.js +120 -0
- package/dist/commands/taskOverviewCommand.js +36 -8
- package/dist/commands/telemetryCommands.js +330 -0
- package/dist/commands/workflowCommands.js +415 -0
- package/dist/config/yuiConfig.js +62 -0
- package/dist/controller/clientRuntime.js +42 -1
- package/dist/controller/controller.js +402 -56
- package/dist/controller/controllerMain.js +25 -2
- package/dist/controller/domainIdentity.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +423 -31
- package/dist/controller/handoverCandidate.js +168 -0
- package/dist/controller/jobClient.js +102 -0
- package/dist/controller/jobControl.js +613 -0
- package/dist/controller/jobSupervisor.js +498 -0
- package/dist/controller/providerHookRunFence.js +34 -5
- package/dist/controller/resourceCleanupLinux.js +18 -9
- package/dist/controller/resourceInventoryLinux.js +90 -39
- package/dist/controller/runtime.js +165 -15
- package/dist/controller/runtimeEventInbox.js +234 -57
- package/dist/controller/runtimeEventProcessor.js +297 -58
- package/dist/controller/sessionOwnerReconciliation.js +321 -0
- package/dist/core/controllerServer.js +416 -27
- package/dist/core/controllerTelemetry.js +167 -0
- package/dist/doctor/doctor.js +113 -16
- package/dist/domain/validation.js +9 -0
- package/dist/execution/executionGroup.js +40 -3
- package/dist/executor/agentExecutor.js +6 -3
- package/dist/executor/effectiveLaunch.js +52 -0
- package/dist/executor/executorRegistry.js +50 -0
- package/dist/executor/fileRoleLaunchPlanner.js +61 -6
- package/dist/grant/capabilityGrant.js +282 -0
- package/dist/integration/changeSet.js +16 -3
- package/dist/integration/changeSetManifest.js +46 -0
- package/dist/integration/gitIntegrationService.js +528 -147
- package/dist/integration/integrationAttempt.js +54 -5
- package/dist/integration/integrationQueueEntry.js +221 -0
- package/dist/integration/integrationQueueService.js +955 -0
- package/dist/integration/manifestTags.js +99 -0
- package/dist/integration/overlapDiagnostics.js +211 -0
- package/dist/job/durableJob.js +449 -0
- package/dist/job/jobRunner.js +350 -0
- package/dist/lifecycle/exactRunTerminalization.js +24 -2
- package/dist/lifecycle/providerErrorClass.js +126 -0
- package/dist/message/message.js +16 -3
- package/dist/observability/executionAudit.js +545 -0
- package/dist/observability/faultClassification.js +160 -0
- package/dist/observability/runtimeIdentity.js +367 -0
- package/dist/release/fakeReleasePorts.js +55 -0
- package/dist/release/releaseHandover.js +475 -0
- package/dist/release/releaseIdempotencyStore.js +165 -0
- package/dist/release/releaseWorkflow.js +459 -0
- package/dist/release/releaseWorkflowEngine.js +688 -0
- package/dist/release/releaseWorkflowPorts.js +1720 -0
- package/dist/release/runtimeRelease.js +495 -0
- package/dist/release/workflowFileLock.js +218 -0
- package/dist/repository/gitWorkspace.js +177 -1
- package/dist/repository/projectMaintenanceLock.js +315 -0
- package/dist/repository/taskWorkspaceCoordinator.js +87 -17
- package/dist/repository/taskWorkspacePreparer.js +1091 -517
- package/dist/resources/autoResourceGc.js +116 -0
- package/dist/resources/liveReferences.js +574 -0
- package/dist/resources/resourceDiscovery.js +477 -0
- package/dist/resources/resourceGc.js +645 -0
- package/dist/resources/resourceRegistrar.js +256 -0
- package/dist/resources/resourceRegistry.js +150 -0
- package/dist/resources/resourceRegistryStore.js +41 -0
- package/dist/resources/resourceTypes.js +42 -0
- package/dist/resources/sqliteResourceRegistry.js +111 -0
- package/dist/review/reviewConfig.js +10 -0
- package/dist/review/reviewFinding.js +240 -0
- package/dist/review/reviewFindingLedger.js +545 -0
- package/dist/review/reviewOutcomeClassifier.js +61 -0
- package/dist/review/reviewRound.js +56 -4
- package/dist/run/agentRun.js +80 -4
- package/dist/run/providerRetry.js +84 -0
- package/dist/run/providerRetryConfig.js +63 -0
- package/dist/run/yieldReceipt.js +65 -0
- package/dist/runtime/exactControlPlane.js +79 -2
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/sessionOwnerIdentity.js +269 -0
- package/dist/runtime/sessionOwnerRegistry.js +132 -0
- package/dist/runtime/sessionReconciliation.js +93 -0
- package/dist/runtime/sessionTerminationGuard.js +211 -0
- package/dist/runtime/taskRuntimeIsolation.js +13 -0
- package/dist/runtime/tmuxAdapters.js +34 -1
- package/dist/scheduler/actionability.js +155 -0
- package/dist/scheduler/activeRoleRunDelivery.js +14 -5
- package/dist/scheduler/activeTaskProgress.js +60 -0
- package/dist/scheduler/leaderWakeupProcessor.js +22 -11
- package/dist/scheduler/roleRunStall.js +135 -29
- package/dist/scheduler/taskExecutionProjection.js +11 -0
- package/dist/storage/compatibleTaskStore.js +112 -5
- package/dist/storage/migration/productionRegistry.js +736 -1
- package/dist/storage/sqliteSchema.js +264 -3
- package/dist/storage/sqliteStore.js +487 -13
- package/dist/storage/storeRpc.js +21 -0
- package/dist/storage/taskStore.js +974 -21
- package/dist/storage/upgrade/homeClassification.js +120 -2
- package/dist/storage/upgrade/migrationReceipt.js +67 -0
- package/dist/storage/upgrade/pseudoLayoutRepair.js +241 -0
- package/dist/storage/upgrade/recordVersions.js +10 -1
- package/dist/storage/upgrade/sqliteMigrationTarget.js +58 -6
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +290 -0
- package/dist/storage/upgrade/sqliteStateMigration.js +258 -2
- package/dist/storage/upgrade/upgradeOrchestrator.js +482 -16
- package/dist/task/deliveryGuard.js +226 -0
- package/dist/task/nextAction.js +738 -0
- package/dist/task/repairWave.js +137 -0
- package/dist/task/taskRecordReference.js +6 -1
- package/dist/telemetry/sqliteTelemetryStore.js +387 -0
- package/dist/telemetry/telemetryCompaction.js +251 -0
- package/dist/telemetry/telemetryConfig.js +64 -0
- package/dist/telemetry/telemetryRouter.js +32 -0
- package/dist/telemetry/telemetryStore.js +19 -0
- package/dist/telemetry/telemetryWiring.js +33 -0
- package/dist/tmux/tmuxManager.js +20 -1
- package/dist/tmux/tmuxSocketEndpoint.js +20 -0
- package/dist/verification/gateArtifact.js +216 -0
- package/dist/verification/gateArtifactStore.js +87 -0
- package/dist/verification/verificationGateService.js +414 -0
- package/dist/verification/verificationPlan.js +308 -0
- package/dist/workspace/gitChangeSetCapture.js +12 -2
- package/dist/workspace/workItemChangeSetManager.js +60 -3
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -0
|
@@ -35,17 +35,22 @@
|
|
|
35
35
|
* it performs the same cheap structural checks the file store relies on
|
|
36
36
|
* (identity presence, taskId matching, referential lookups).
|
|
37
37
|
*/
|
|
38
|
-
import { mkdirSync } from "node:fs";
|
|
38
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
39
39
|
import { join } from "node:path";
|
|
40
40
|
import { isDeepStrictEqual } from "node:util";
|
|
41
41
|
import Database from "better-sqlite3";
|
|
42
42
|
import { mailboxTargetKey } from "../coordination/workMailbox.js";
|
|
43
|
+
import { validateReviewFinding } from "../review/reviewFinding.js";
|
|
43
44
|
import { generateHomeIdentity } from "../repository/homeIdentity.js";
|
|
45
|
+
import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
|
|
46
|
+
import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
|
|
44
47
|
import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
45
48
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
46
49
|
import { assertHomeWritable } from "./upgradeFence.js";
|
|
47
|
-
import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore } from "./taskStore.js";
|
|
50
|
+
import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore, storedCapabilityGrant, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, validateYuiConfig } from "./taskStore.js";
|
|
51
|
+
import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
|
|
48
52
|
import { migrateSqliteSchema, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "./sqliteSchema.js";
|
|
53
|
+
import { inspectStorageSchema } from "./storageSchema.js";
|
|
49
54
|
const DEFAULT_CONFIG = { schemaVersion: CURRENT_CONFIG_SCHEMA_VERSION };
|
|
50
55
|
function numericCompare(left, right) {
|
|
51
56
|
return left.localeCompare(right, undefined, { numeric: true });
|
|
@@ -97,6 +102,12 @@ export class SqliteTaskStore {
|
|
|
97
102
|
rootDirectory() { return this.#rootDir; }
|
|
98
103
|
/** Close the underlying database connection. */
|
|
99
104
|
close() { this.#db.close(); }
|
|
105
|
+
/**
|
|
106
|
+
* The underlying database connection, for read-only diagnostics
|
|
107
|
+
* (`PRAGMA journal_mode`, `PRAGMA quick_check`). Callers must not mutate
|
|
108
|
+
* through this handle.
|
|
109
|
+
*/
|
|
110
|
+
databaseHandle() { return this.#db; }
|
|
100
111
|
// -- transaction primitives -------------------------------------------------
|
|
101
112
|
#seedHomeMeta() {
|
|
102
113
|
const now = new Date().toISOString();
|
|
@@ -166,17 +177,13 @@ export class SqliteTaskStore {
|
|
|
166
177
|
throw error;
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
|
-
/**
|
|
170
|
-
* The file-store's `transaction(closure)`: a single BEGIN IMMEDIATE … COMMIT.
|
|
171
|
-
* Nested calls join the outer transaction. The revision is bumped once, at
|
|
172
|
-
* commit, only when the closure wrote.
|
|
173
|
-
*/
|
|
174
180
|
transaction(execute, options) {
|
|
181
|
+
const run = execute;
|
|
175
182
|
if (this.#inTransaction)
|
|
176
|
-
return
|
|
183
|
+
return run(this);
|
|
177
184
|
this.#begin();
|
|
178
185
|
try {
|
|
179
|
-
const result =
|
|
186
|
+
const result = run(this);
|
|
180
187
|
if (this.#dirty) {
|
|
181
188
|
this.#prepareWrite();
|
|
182
189
|
if (options?.requestId !== undefined) {
|
|
@@ -196,6 +203,31 @@ export class SqliteTaskStore {
|
|
|
196
203
|
throw error;
|
|
197
204
|
}
|
|
198
205
|
}
|
|
206
|
+
/** Async transaction seam used by queue operations that must inspect Git
|
|
207
|
+
* before committing the durable queue state. The SQLite write transaction
|
|
208
|
+
* remains open across the awaited callback, matching FileTaskStore's
|
|
209
|
+
* transactionAsync semantics and preserving the single-writer boundary. */
|
|
210
|
+
async transactionAsync(execute) {
|
|
211
|
+
if (this.#inTransaction)
|
|
212
|
+
return execute(this);
|
|
213
|
+
this.#prepareWrite();
|
|
214
|
+
this.#begin();
|
|
215
|
+
try {
|
|
216
|
+
const result = await execute(this);
|
|
217
|
+
if (this.#dirty)
|
|
218
|
+
this.#bumpRevision();
|
|
219
|
+
this.#commit();
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.#rollback();
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/** Bounded runtime-event folds share the same SQLite write transaction. */
|
|
228
|
+
withRuntimeEventTransaction(execute) {
|
|
229
|
+
return this.transaction(() => execute());
|
|
230
|
+
}
|
|
199
231
|
/**
|
|
200
232
|
* Revision CAS (§5.3): run `execute` only when the global revision is still
|
|
201
233
|
* `expectedRevision`; otherwise throw {@link StorageConflictError}. The check
|
|
@@ -237,6 +269,7 @@ export class SqliteTaskStore {
|
|
|
237
269
|
const row = this.#db.prepare("SELECT revision FROM home_meta WHERE id = 1").get();
|
|
238
270
|
return row.revision;
|
|
239
271
|
}
|
|
272
|
+
getStateRevision() { return this.getRevision(); }
|
|
240
273
|
/**
|
|
241
274
|
* Run an ordered command batch inside one `BEGIN IMMEDIATE … COMMIT`, yielding
|
|
242
275
|
* to the event loop between commands so a cancellation signal can interrupt
|
|
@@ -430,9 +463,14 @@ export class SqliteTaskStore {
|
|
|
430
463
|
// -- config / identity ------------------------------------------------------
|
|
431
464
|
getConfig() {
|
|
432
465
|
const row = this.#db.prepare("SELECT payload FROM config WHERE id = 1").get();
|
|
433
|
-
|
|
466
|
+
const config = this.#parse(row.payload);
|
|
467
|
+
// Fail closed on malformed durable config, matching the File store:
|
|
468
|
+
// GC mode and other settings must never silently fall back to defaults.
|
|
469
|
+
validateYuiConfig(config);
|
|
470
|
+
return config;
|
|
434
471
|
}
|
|
435
472
|
saveConfig(config) {
|
|
473
|
+
validateYuiConfig(config);
|
|
436
474
|
this.#mutate(() => {
|
|
437
475
|
this.#db.prepare("UPDATE config SET payload = ?, updated_at = ? WHERE id = 1").run(this.#json(config), this.#now());
|
|
438
476
|
});
|
|
@@ -601,6 +639,29 @@ export class SqliteTaskStore {
|
|
|
601
639
|
getTask(id) {
|
|
602
640
|
return this.#getPayload("task_records", "task_id = ?", [id]);
|
|
603
641
|
}
|
|
642
|
+
readNextActionFacts(taskId) {
|
|
643
|
+
const task = this.#getPayload("task_records", "task_id = ?", [taskId]);
|
|
644
|
+
if (task === null)
|
|
645
|
+
return null;
|
|
646
|
+
// One indexed query (idx_agent_runs_role_status) covers both the active
|
|
647
|
+
// Runs the projection waits on and the Leader Runs the budget consumes.
|
|
648
|
+
const runs = this.#sortById(this.#listPayload("agent_runs", "task_id = ? AND (status = 'active' OR role_name = 'leader')", [taskId]), (run) => run.id);
|
|
649
|
+
return {
|
|
650
|
+
task: {
|
|
651
|
+
id: task.id,
|
|
652
|
+
status: task.status,
|
|
653
|
+
projectBindings: task.projectBindings
|
|
654
|
+
},
|
|
655
|
+
workItems: this.#sortById(this.#listPayload("work_items", "task_id = ?", [taskId]), (item) => item.id),
|
|
656
|
+
changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
|
|
657
|
+
integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
|
|
658
|
+
reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
|
|
659
|
+
reviewConfig: this.getReviewConfig(),
|
|
660
|
+
openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
|
|
661
|
+
activeRuns: runs.filter((run) => run.status === "active"),
|
|
662
|
+
leaderRuns: runs.filter((run) => run.roleName === "leader")
|
|
663
|
+
};
|
|
664
|
+
}
|
|
604
665
|
/** Task ids flagged active in the catalog projection (the global active index). */
|
|
605
666
|
listActiveTaskIds() {
|
|
606
667
|
const rows = this.#db.prepare("SELECT task_id FROM tasks_catalog WHERE is_active = 1 ORDER BY task_id").all();
|
|
@@ -663,6 +724,139 @@ export class SqliteTaskStore {
|
|
|
663
724
|
getIntegrationAttempt(taskId, integrationId) {
|
|
664
725
|
return this.#getPayload("integration_attempts", "task_id = ? AND integration_id = ?", [taskId, integrationId]);
|
|
665
726
|
}
|
|
727
|
+
// -- integration queue -----------------------------------------------------
|
|
728
|
+
nextIntegrationQueueEntryId(taskId) {
|
|
729
|
+
return this.#nextTaskRecordId(taskId, "integrationQueue");
|
|
730
|
+
}
|
|
731
|
+
saveIntegrationQueueEntry(taskId, entry) {
|
|
732
|
+
if (entry.taskId !== taskId) {
|
|
733
|
+
throw new StorageRecordError(`Integration queue entry belongs to another Task: ${entry.taskId}`);
|
|
734
|
+
}
|
|
735
|
+
validateIntegrationQueueEntry(entry);
|
|
736
|
+
this.#requireTask(taskId);
|
|
737
|
+
const changeSet = this.getChangeSet(taskId, entry.changeSetId);
|
|
738
|
+
if (changeSet === null) {
|
|
739
|
+
throw new StorageRecordError(`Integration queue ChangeSet not found: ${entry.changeSetId}`);
|
|
740
|
+
}
|
|
741
|
+
if (changeSet.projectId !== entry.projectId) {
|
|
742
|
+
throw new StorageRecordError(`Integration queue ChangeSet belongs to another Project: ${entry.changeSetId}`);
|
|
743
|
+
}
|
|
744
|
+
const existing = this.getIntegrationQueueEntry(taskId, entry.id);
|
|
745
|
+
if (existing !== null) {
|
|
746
|
+
if (Date.parse(entry.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
747
|
+
throw new StorageRecordError(`Integration queue entry updatedAt cannot move backwards: ${entry.id}`);
|
|
748
|
+
}
|
|
749
|
+
if (!validIntegrationQueueTransition(existing, entry)) {
|
|
750
|
+
throw new StorageRecordError(`Integration queue entry transition is invalid: ${entry.id}`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
this.#mutate(() => {
|
|
754
|
+
this.#db.prepare(`INSERT INTO integration_queue (queue_id, task_id, project_id, change_set, status, payload, created_at, updated_at)
|
|
755
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
756
|
+
ON CONFLICT(queue_id) DO UPDATE SET task_id = excluded.task_id,
|
|
757
|
+
project_id = excluded.project_id, change_set = excluded.change_set,
|
|
758
|
+
status = excluded.status, payload = excluded.payload, updated_at = excluded.updated_at`).run(entry.id, taskId, entry.projectId, entry.changeSetId, entry.status, this.#json(entry), entry.createdAt, entry.updatedAt);
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
listIntegrationQueueEntries(taskId) {
|
|
762
|
+
return this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id);
|
|
763
|
+
}
|
|
764
|
+
getIntegrationQueueEntry(taskId, entryId) {
|
|
765
|
+
return this.#getPayload("integration_queue", "task_id = ? AND queue_id = ?", [taskId, entryId]);
|
|
766
|
+
}
|
|
767
|
+
// -- durable jobs -----------------------------------------------------------
|
|
768
|
+
nextDurableJobId(taskId) {
|
|
769
|
+
return this.#nextTaskRecordId(taskId, "durableJob");
|
|
770
|
+
}
|
|
771
|
+
saveDurableJob(taskId, job) {
|
|
772
|
+
if (job.taskId !== taskId) {
|
|
773
|
+
throw new StorageRecordError(`DurableJob belongs to another Task: ${job.taskId}`);
|
|
774
|
+
}
|
|
775
|
+
validateDurableJob(job);
|
|
776
|
+
this.#requireTask(taskId);
|
|
777
|
+
const existing = this.getDurableJob(taskId, job.id);
|
|
778
|
+
if (existing !== null) {
|
|
779
|
+
if (Date.parse(job.updatedAt) < Date.parse(existing.updatedAt)) {
|
|
780
|
+
throw new StorageRecordError(`DurableJob updatedAt cannot move backwards: ${job.id}`);
|
|
781
|
+
}
|
|
782
|
+
if (!validDurableJobTransition(existing, job)) {
|
|
783
|
+
throw new StorageRecordError(`DurableJob transition is invalid: ${job.id}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
this.#mutate(() => {
|
|
787
|
+
this.#db.prepare(`INSERT INTO durable_jobs (job_id, task_id, idempotency_key, status, payload, created_at, updated_at)
|
|
788
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
789
|
+
ON CONFLICT(task_id, job_id) DO UPDATE SET idempotency_key = excluded.idempotency_key,
|
|
790
|
+
status = excluded.status,
|
|
791
|
+
payload = excluded.payload, updated_at = excluded.updated_at`).run(job.id, taskId, job.idempotencyKey ?? null, job.status, this.#json(job), job.createdAt, job.updatedAt);
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
listDurableJobs(taskId) {
|
|
795
|
+
return this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id);
|
|
796
|
+
}
|
|
797
|
+
getDurableJob(taskId, jobId) {
|
|
798
|
+
return this.#getPayload("durable_jobs", "task_id = ? AND job_id = ?", [taskId, jobId]);
|
|
799
|
+
}
|
|
800
|
+
findDurableJobByIdempotencyKey(taskId, key) {
|
|
801
|
+
return this.#getPayload("durable_jobs", "task_id = ? AND idempotency_key = ?", [taskId, key]);
|
|
802
|
+
}
|
|
803
|
+
listAllDurableJobs() {
|
|
804
|
+
return this.#listPayload("durable_jobs", "1 = 1", []);
|
|
805
|
+
}
|
|
806
|
+
hasActiveDurableJobs() {
|
|
807
|
+
const row = this.#db.prepare("SELECT 1 FROM durable_jobs WHERE status IN ('queued', 'running') LIMIT 1").get();
|
|
808
|
+
return row !== undefined;
|
|
809
|
+
}
|
|
810
|
+
// -- job caller key hashes (rr13) -------------------------------------------
|
|
811
|
+
getJobCallerKeyHash(taskId, roleName, agentId) {
|
|
812
|
+
const row = this.#db.prepare("SELECT hash FROM job_caller_key_hashes WHERE task_id = ? AND role_name = ? AND agent_id = ?").get(taskId, roleName, agentId);
|
|
813
|
+
return row === undefined ? null : row.hash;
|
|
814
|
+
}
|
|
815
|
+
setJobCallerKeyHash(taskId, roleName, agentId, hash) {
|
|
816
|
+
this.#requireTask(taskId);
|
|
817
|
+
if (!/^[a-f0-9]{64}$/u.test(hash)) {
|
|
818
|
+
throw new StorageRecordError(`Job caller key hash is invalid: ${taskId}/${roleName}.`);
|
|
819
|
+
}
|
|
820
|
+
this.#mutate(() => {
|
|
821
|
+
this.#db.prepare(`INSERT INTO job_caller_key_hashes (task_id, role_name, agent_id, hash, updated_at)
|
|
822
|
+
VALUES (?, ?, ?, ?, ?)
|
|
823
|
+
ON CONFLICT(task_id, role_name, agent_id) DO UPDATE SET hash = excluded.hash, updated_at = excluded.updated_at`).run(taskId, roleName, agentId, hash, this.#now());
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
// -- session owners (Issue 03) ----------------------------------------------
|
|
827
|
+
saveSessionOwner(identity) {
|
|
828
|
+
const owner = identity.owner;
|
|
829
|
+
this.#mutate(() => {
|
|
830
|
+
this.#db.prepare(`INSERT INTO session_owners
|
|
831
|
+
(launch_id, scope, task_id, role_name, agent_id, native_session_id,
|
|
832
|
+
provider_root_pid, payload, recorded_at)
|
|
833
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
834
|
+
ON CONFLICT(launch_id) DO UPDATE SET
|
|
835
|
+
scope = excluded.scope, task_id = excluded.task_id,
|
|
836
|
+
role_name = excluded.role_name, agent_id = excluded.agent_id,
|
|
837
|
+
native_session_id = excluded.native_session_id,
|
|
838
|
+
provider_root_pid = excluded.provider_root_pid,
|
|
839
|
+
payload = excluded.payload, recorded_at = excluded.recorded_at`).run(identity.launchId, owner.scope, owner.scope === "task" ? owner.taskId : null, owner.roleName, identity.agentId, identity.nativeSessionId ?? null, identity.providerRoot.pid, this.#json(identity), identity.recordedAt);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
getSessionOwner(launchId) {
|
|
843
|
+
const row = this.#db.prepare("SELECT payload FROM session_owners WHERE launch_id = ?").get(launchId);
|
|
844
|
+
return row === undefined ? null : this.#parse(row.payload);
|
|
845
|
+
}
|
|
846
|
+
listSessionOwners() {
|
|
847
|
+
return this.#listPayload("session_owners", "1=1", []).sort((left, right) => left.recordedAt.localeCompare(right.recordedAt));
|
|
848
|
+
}
|
|
849
|
+
listSessionOwnersForOwner(owner) {
|
|
850
|
+
if (owner.scope === "global") {
|
|
851
|
+
return this.#listPayload("session_owners", "scope = 'global' AND role_name = ?", [owner.roleName]);
|
|
852
|
+
}
|
|
853
|
+
return this.#listPayload("session_owners", "scope = 'task' AND task_id = ? AND role_name = ?", [owner.taskId, owner.roleName]);
|
|
854
|
+
}
|
|
855
|
+
removeSessionOwner(launchId) {
|
|
856
|
+
this.#mutate(() => {
|
|
857
|
+
this.#db.prepare("DELETE FROM session_owners WHERE launch_id = ?").run(launchId);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
666
860
|
// -- task roles -------------------------------------------------------------
|
|
667
861
|
saveRole(taskId, role) {
|
|
668
862
|
this.#requireTask(taskId);
|
|
@@ -778,6 +972,167 @@ export class SqliteTaskStore {
|
|
|
778
972
|
payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, item.id, item.status, this.#json(item), this.#now());
|
|
779
973
|
});
|
|
780
974
|
}
|
|
975
|
+
// -- capability grants ------------------------------------------------------
|
|
976
|
+
nextCapabilityGrantId(taskId) { return this.#nextTaskRecordId(taskId, "capabilityGrant"); }
|
|
977
|
+
saveCapabilityGrant(taskId, grant) {
|
|
978
|
+
const stored = storedCapabilityGrant(grant);
|
|
979
|
+
if (stored.taskId !== taskId) {
|
|
980
|
+
throw new StorageRecordError(`Capability grant belongs to another Task: ${stored.taskId}`);
|
|
981
|
+
}
|
|
982
|
+
this.#requireTask(taskId);
|
|
983
|
+
this.#mutate(() => {
|
|
984
|
+
const existing = this.#getPayload("capability_grants", "task_id = ? AND grant_id = ?", [taskId, stored.id]);
|
|
985
|
+
if (existing === null) {
|
|
986
|
+
if (stored.revokedAt !== undefined) {
|
|
987
|
+
throw new StorageRecordError(`Capability grant must start unrevoked: ${stored.id}`);
|
|
988
|
+
}
|
|
989
|
+
if (stored.usesUsed !== 0) {
|
|
990
|
+
throw new StorageRecordError(`Capability grant must start unused: ${stored.id}`);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
else if (!isValidCapabilityGrantTransition(existing, stored)) {
|
|
994
|
+
throw new StorageRecordError(`Capability grant cannot be overwritten: ${taskId}/${stored.id}`);
|
|
995
|
+
}
|
|
996
|
+
this.#db.prepare(`INSERT INTO capability_grants (task_id, grant_id, payload, updated_at) VALUES (?, ?, ?, ?)
|
|
997
|
+
ON CONFLICT(task_id, grant_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, stored.id, this.#json(stored), this.#now());
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
listCapabilityGrants(taskId) {
|
|
1001
|
+
return this.#sortById(this.#listPayload("capability_grants", "task_id = ?", [taskId]), (grant) => grant.id);
|
|
1002
|
+
}
|
|
1003
|
+
getCapabilityGrant(taskId, grantId) {
|
|
1004
|
+
return this.#getPayload("capability_grants", "task_id = ? AND grant_id = ?", [taskId, grantId]);
|
|
1005
|
+
}
|
|
1006
|
+
// -- release workflows ------------------------------------------------------
|
|
1007
|
+
nextReleaseWorkflowId(taskId) { return this.#nextTaskRecordId(taskId, "releaseWorkflow"); }
|
|
1008
|
+
saveReleaseWorkflow(taskId, workflow) {
|
|
1009
|
+
const stored = storedReleaseWorkflow(workflow);
|
|
1010
|
+
if (stored.taskId !== taskId) {
|
|
1011
|
+
throw new StorageRecordError(`Release workflow belongs to another Task: ${stored.taskId}`);
|
|
1012
|
+
}
|
|
1013
|
+
this.#requireTask(taskId);
|
|
1014
|
+
this.#mutate(() => {
|
|
1015
|
+
const existing = this.#getPayload("release_workflows", "task_id = ? AND workflow_id = ?", [taskId, stored.id]);
|
|
1016
|
+
if (existing !== null && !isValidReleaseWorkflowTransition(existing, stored)) {
|
|
1017
|
+
throw new StorageRecordError(`Release workflow cannot be overwritten: ${taskId}/${stored.id}`);
|
|
1018
|
+
}
|
|
1019
|
+
this.#db.prepare(`INSERT INTO release_workflows (task_id, workflow_id, payload, updated_at) VALUES (?, ?, ?, ?)
|
|
1020
|
+
ON CONFLICT(task_id, workflow_id) DO UPDATE SET payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, stored.id, this.#json(stored), this.#now());
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
listReleaseWorkflows(taskId) {
|
|
1024
|
+
return this.#sortById(this.#listPayload("release_workflows", "task_id = ?", [taskId]), (workflow) => workflow.id);
|
|
1025
|
+
}
|
|
1026
|
+
getReleaseWorkflow(taskId, workflowId) {
|
|
1027
|
+
return this.#getPayload("release_workflows", "task_id = ? AND workflow_id = ?", [taskId, workflowId]);
|
|
1028
|
+
}
|
|
1029
|
+
// -- gate artifacts (Issue 08) ----------------------------------------------
|
|
1030
|
+
saveGateArtifact(artifact, logs) {
|
|
1031
|
+
validateGateArtifact(artifact);
|
|
1032
|
+
this.#mutate(() => {
|
|
1033
|
+
const targetRef = artifact.boundary?.targetRef ?? null;
|
|
1034
|
+
const completedAt = artifact.completedAt ?? null;
|
|
1035
|
+
this.#db.prepare(`INSERT INTO gate_artifacts
|
|
1036
|
+
(key, project_id, level, commit_sha, plan_digest, toolchain_digest,
|
|
1037
|
+
target_ref, status, outcome, payload, created_at, completed_at, last_used_at)
|
|
1038
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1039
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
1040
|
+
project_id = excluded.project_id,
|
|
1041
|
+
level = excluded.level,
|
|
1042
|
+
commit_sha = excluded.commit_sha,
|
|
1043
|
+
plan_digest = excluded.plan_digest,
|
|
1044
|
+
toolchain_digest = excluded.toolchain_digest,
|
|
1045
|
+
target_ref = excluded.target_ref,
|
|
1046
|
+
status = excluded.status,
|
|
1047
|
+
outcome = excluded.outcome,
|
|
1048
|
+
payload = excluded.payload,
|
|
1049
|
+
completed_at = excluded.completed_at,
|
|
1050
|
+
last_used_at = excluded.last_used_at`).run(artifact.key, artifact.projectId, artifact.level, artifact.commit, artifact.planDigest, artifact.toolchainDigest, targetRef, artifact.status, artifact.outcome, this.#json(artifact), artifact.createdAt, completedAt, artifact.lastUsedAt);
|
|
1051
|
+
// Replace all logs for this artifact in the same transaction.
|
|
1052
|
+
this.#db.prepare("DELETE FROM gate_artifact_logs WHERE artifact_key = ?").run(artifact.key);
|
|
1053
|
+
const insertLog = this.#db.prepare(`INSERT INTO gate_artifact_logs (artifact_key, step_name, log_content, log_digest, log_bytes)
|
|
1054
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
1055
|
+
for (const [stepName, content] of logs) {
|
|
1056
|
+
const step = artifact.steps.find((s) => s.name === stepName);
|
|
1057
|
+
if (step === undefined) {
|
|
1058
|
+
throw new StorageRecordError(`Gate artifact log has no matching step: ${artifact.key}/${stepName}`);
|
|
1059
|
+
}
|
|
1060
|
+
insertLog.run(artifact.key, stepName, content, step.logDigest, step.logBytes);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
touchGateArtifact(artifact) {
|
|
1065
|
+
validateGateArtifact(artifact);
|
|
1066
|
+
this.#mutate(() => {
|
|
1067
|
+
const result = this.#db.prepare(`UPDATE gate_artifacts SET payload = ?, last_used_at = ? WHERE key = ?`).run(this.#json(artifact), artifact.lastUsedAt, artifact.key);
|
|
1068
|
+
if (result.changes === 0) {
|
|
1069
|
+
throw new StorageRecordError(`Gate artifact not found for touch: ${artifact.key}`);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
getGateArtifact(projectId, key) {
|
|
1074
|
+
const artifact = this.#getPayload("gate_artifacts", "project_id = ? AND key = ?", [projectId, key]);
|
|
1075
|
+
return artifact === null ? null : validateGateArtifact(artifact);
|
|
1076
|
+
}
|
|
1077
|
+
findGateArtifactByIdentity(identity) {
|
|
1078
|
+
return this.getGateArtifact(identity.projectId, gateArtifactKey(identity));
|
|
1079
|
+
}
|
|
1080
|
+
findL2GateArtifactsForCommit(query) {
|
|
1081
|
+
const rows = this.#listPayload("gate_artifacts", `project_id = ? AND commit_sha = ? AND level = 'L2'
|
|
1082
|
+
AND plan_digest = ? AND toolchain_digest = ? AND target_ref = ?
|
|
1083
|
+
AND status = 'complete' AND outcome = 'succeeded'`, [query.projectId, query.commit, query.planDigest, query.toolchainDigest, query.targetRef]);
|
|
1084
|
+
const valid = [];
|
|
1085
|
+
for (const row of rows) {
|
|
1086
|
+
try {
|
|
1087
|
+
valid.push(validateGateArtifact(row));
|
|
1088
|
+
}
|
|
1089
|
+
catch {
|
|
1090
|
+
// Skip corrupt rows; a direct lookup fails closed.
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return valid;
|
|
1094
|
+
}
|
|
1095
|
+
getGateArtifactLogs(artifactKey) {
|
|
1096
|
+
const rows = this.#db.prepare("SELECT step_name, log_content FROM gate_artifact_logs WHERE artifact_key = ?").all(artifactKey);
|
|
1097
|
+
const logs = new Map();
|
|
1098
|
+
for (const row of rows) {
|
|
1099
|
+
logs.set(row.step_name, Buffer.from(row.log_content));
|
|
1100
|
+
}
|
|
1101
|
+
return logs;
|
|
1102
|
+
}
|
|
1103
|
+
pruneGateArtifacts(projectId, options) {
|
|
1104
|
+
// Snapshot candidates outside the write transaction so the isReferenced
|
|
1105
|
+
// callback never holds the SQLite write lock.
|
|
1106
|
+
const rows = this.#db.prepare("SELECT key, payload, last_used_at FROM gate_artifacts WHERE project_id = ?").all(projectId);
|
|
1107
|
+
const toDelete = [];
|
|
1108
|
+
let retained = 0;
|
|
1109
|
+
for (const row of rows) {
|
|
1110
|
+
let artifact;
|
|
1111
|
+
try {
|
|
1112
|
+
artifact = validateGateArtifact(JSON.parse(row.payload));
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
retained += 1;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
const age = options.now.getTime() - Date.parse(artifact.lastUsedAt);
|
|
1119
|
+
if (options.isReferenced(artifact.key) || age < options.ttlMs) {
|
|
1120
|
+
retained += 1;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
toDelete.push(artifact.key);
|
|
1124
|
+
}
|
|
1125
|
+
if (toDelete.length === 0) {
|
|
1126
|
+
return Object.freeze({ retained, deleted: 0 });
|
|
1127
|
+
}
|
|
1128
|
+
return this.#mutate(() => {
|
|
1129
|
+
const deleteArtifact = this.#db.prepare("DELETE FROM gate_artifacts WHERE key = ?");
|
|
1130
|
+
for (const key of toDelete) {
|
|
1131
|
+
deleteArtifact.run(key);
|
|
1132
|
+
}
|
|
1133
|
+
return Object.freeze({ retained, deleted: toDelete.length });
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
781
1136
|
// -- agent runs -------------------------------------------------------------
|
|
782
1137
|
nextAgentRunId(taskId) { return this.#nextTaskRecordId(taskId, "agentRun"); }
|
|
783
1138
|
peekNextAgentRunId(taskId) { return this.#peekTaskRecordId(taskId, "agentRun"); }
|
|
@@ -796,6 +1151,21 @@ export class SqliteTaskStore {
|
|
|
796
1151
|
payload = excluded.payload, updated_at = excluded.updated_at`).run(run.taskId, run.id, run.roleName, run.status, this.#json(run), this.#now());
|
|
797
1152
|
});
|
|
798
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Issue 04: SQLite-native pending retry query. A single indexed scan
|
|
1156
|
+
* replaces the adapter's per-Task in-memory sweep, so Controller deadline
|
|
1157
|
+
* arming no longer materializes every Task and Run in JavaScript.
|
|
1158
|
+
*/
|
|
1159
|
+
listPendingProviderRetries() {
|
|
1160
|
+
const rows = this.#db.prepare(`SELECT ar.task_id AS taskId, ar.run_id AS runId, ar.role_name AS roleName,
|
|
1161
|
+
json_extract(ar.payload, '$.providerRetry.nextAttemptAt') AS nextAttemptAt
|
|
1162
|
+
FROM agent_runs ar
|
|
1163
|
+
JOIN tasks_catalog tc ON tc.task_id = ar.task_id
|
|
1164
|
+
WHERE ar.status = 'active'
|
|
1165
|
+
AND tc.is_active = 1
|
|
1166
|
+
AND json_extract(ar.payload, '$.providerRetry.nextAttemptAt') IS NOT NULL`).all();
|
|
1167
|
+
return rows;
|
|
1168
|
+
}
|
|
799
1169
|
// -- review rounds ----------------------------------------------------------
|
|
800
1170
|
nextReviewRoundId(taskId) { return this.#nextTaskRecordId(taskId, "reviewRound"); }
|
|
801
1171
|
getReviewRound(taskId, reviewRoundId) {
|
|
@@ -814,6 +1184,27 @@ export class SqliteTaskStore {
|
|
|
814
1184
|
payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, round.id, round.status, this.#json(round), this.#now());
|
|
815
1185
|
});
|
|
816
1186
|
}
|
|
1187
|
+
// -- review findings --------------------------------------------------------
|
|
1188
|
+
nextReviewFindingId(taskId) { return this.#nextTaskRecordId(taskId, "reviewFinding"); }
|
|
1189
|
+
getReviewFinding(taskId, findingId) {
|
|
1190
|
+
return this.#getPayload("review_findings", "task_id = ? AND finding_id = ?", [taskId, findingId]);
|
|
1191
|
+
}
|
|
1192
|
+
listReviewFindings(taskId) {
|
|
1193
|
+
return this.#sortById(this.#listPayload("review_findings", "task_id = ?", [taskId]), (finding) => finding.id);
|
|
1194
|
+
}
|
|
1195
|
+
saveReviewFinding(taskId, finding) {
|
|
1196
|
+
validateReviewFinding(finding);
|
|
1197
|
+
if (finding.taskId !== taskId) {
|
|
1198
|
+
throw new StorageRecordError(`Review finding belongs to another Task: ${finding.taskId}`);
|
|
1199
|
+
}
|
|
1200
|
+
this.#requireTask(taskId);
|
|
1201
|
+
this.#mutate(() => {
|
|
1202
|
+
this.#db.prepare(`INSERT INTO review_findings (task_id, finding_id, stable_key, severity, payload, updated_at)
|
|
1203
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1204
|
+
ON CONFLICT(task_id, finding_id) DO UPDATE SET stable_key = excluded.stable_key,
|
|
1205
|
+
severity = excluded.severity, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, finding.id, finding.stableKey, finding.severity, this.#json(finding), this.#now());
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
817
1208
|
// -- active runs ------------------------------------------------------------
|
|
818
1209
|
#saveActiveRun(taskId, pointer, runId) {
|
|
819
1210
|
this.#mutate(() => {
|
|
@@ -837,10 +1228,29 @@ export class SqliteTaskStore {
|
|
|
837
1228
|
return this.#getActiveRun(taskId, roleName);
|
|
838
1229
|
}
|
|
839
1230
|
saveActiveAgentRun(run) {
|
|
1231
|
+
if (run.executionGroupId !== undefined && run.executionLaneId !== undefined) {
|
|
1232
|
+
this.saveActiveExecutionLaneRun(run);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
840
1235
|
this.#saveActiveRun(run.taskId, run.roleName, run.id);
|
|
841
1236
|
}
|
|
842
1237
|
clearActiveAgentRun(taskId, roleName) {
|
|
1238
|
+
// Older Controller paths only know the Role key. When that key points
|
|
1239
|
+
// at a lane-backed Run, remove the matching lane pointer too; preserve
|
|
1240
|
+
// every other lane for the same Role in a multi-lane group.
|
|
1241
|
+
const rolePointer = this.#getActiveRun(taskId, roleName);
|
|
843
1242
|
this.#clearActiveRun(taskId, roleName);
|
|
1243
|
+
if (rolePointer === null)
|
|
1244
|
+
return;
|
|
1245
|
+
const laneRows = this.#db.prepare("SELECT task_id, pointer FROM active_runs WHERE task_id = ?").all(taskId);
|
|
1246
|
+
for (const row of laneRows) {
|
|
1247
|
+
if (executionLaneActiveRunKeyParts(row.pointer) !== null) {
|
|
1248
|
+
const laneRun = this.#getActiveRun(taskId, row.pointer);
|
|
1249
|
+
if (laneRun !== null && laneRun.id === rolePointer.id) {
|
|
1250
|
+
this.#clearActiveRun(taskId, row.pointer);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
844
1254
|
}
|
|
845
1255
|
getActiveExecutionLaneRun(taskId, executionGroupId, executionLaneId) {
|
|
846
1256
|
return this.#getActiveRun(taskId, executionLaneActiveRunKey(executionGroupId, executionLaneId));
|
|
@@ -939,6 +1349,19 @@ export class SqliteTaskStore {
|
|
|
939
1349
|
listEvents(taskId) {
|
|
940
1350
|
return this.#sortById(this.#listPayload("events", "task_id = ?", [taskId]), (event) => event.id);
|
|
941
1351
|
}
|
|
1352
|
+
removeEvents(taskId, eventIds) {
|
|
1353
|
+
if (eventIds.length === 0)
|
|
1354
|
+
return 0;
|
|
1355
|
+
this.#requireTask(taskId);
|
|
1356
|
+
return this.#mutate(() => {
|
|
1357
|
+
const deleteEvent = this.#db.prepare("DELETE FROM events WHERE task_id = ? AND event_id = ?");
|
|
1358
|
+
let removed = 0;
|
|
1359
|
+
for (const eventId of eventIds) {
|
|
1360
|
+
removed += deleteEvent.run(taskId, eventId).changes;
|
|
1361
|
+
}
|
|
1362
|
+
return removed;
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
942
1365
|
// -- high-water maintenance ---------------------------------------------------
|
|
943
1366
|
/** Extract the numeric suffix of a `<prefix>-<n>` record id. */
|
|
944
1367
|
#idSequence(id, kind) {
|
|
@@ -1191,6 +1614,26 @@ export class SqliteTaskStore {
|
|
|
1191
1614
|
});
|
|
1192
1615
|
}
|
|
1193
1616
|
}
|
|
1617
|
+
function validIntegrationQueueTransition(before, after) {
|
|
1618
|
+
if (before.id !== after.id
|
|
1619
|
+
|| before.taskId !== after.taskId
|
|
1620
|
+
|| before.projectId !== after.projectId
|
|
1621
|
+
|| before.changeSetId !== after.changeSetId
|
|
1622
|
+
|| before.targetRef !== after.targetRef
|
|
1623
|
+
|| !isDeepStrictEqual(before.checkCommands, after.checkCommands)
|
|
1624
|
+
|| !isDeepStrictEqual(before.evidenceRefs, after.evidenceRefs)
|
|
1625
|
+
|| before.createdAt !== after.createdAt)
|
|
1626
|
+
return false;
|
|
1627
|
+
const allowed = {
|
|
1628
|
+
queued: ["queued", "running", "validated", "superseded"],
|
|
1629
|
+
running: ["running", "conflicted", "committed"],
|
|
1630
|
+
conflicted: ["conflicted", "running", "committed", "queued", "superseded"],
|
|
1631
|
+
validated: ["validated", "running", "queued", "superseded"],
|
|
1632
|
+
committed: ["committed"],
|
|
1633
|
+
superseded: ["superseded"]
|
|
1634
|
+
};
|
|
1635
|
+
return allowed[before.status].includes(after.status);
|
|
1636
|
+
}
|
|
1194
1637
|
/**
|
|
1195
1638
|
* Open a {@link TaskStore} for the given backend. `file` returns the existing
|
|
1196
1639
|
* {@link FileTaskStore}; `sqlite` returns the in-process {@link SqliteTaskStore}.
|
|
@@ -1212,11 +1655,42 @@ export function openTaskStore(home, backend, options) {
|
|
|
1212
1655
|
export function resolveTaskStoreBackend(env = process.env) {
|
|
1213
1656
|
return env.YUI_STORE_BACKEND?.toLowerCase() === "sqlite" ? "sqlite" : "file";
|
|
1214
1657
|
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Resolve the storage backend from the Home's verified manifest (Issue 01).
|
|
1660
|
+
*
|
|
1661
|
+
* The Home decides: a layout-7 Home's authoritative backend is SQLite WAL, so
|
|
1662
|
+
* ordinary CLI/Controller startup opens SQLite without requiring
|
|
1663
|
+
* `YUI_STORE_BACKEND=sqlite`. An explicit `YUI_STORE_BACKEND` env value still
|
|
1664
|
+
* wins — it is reserved for tests and explicit recovery commands — and any
|
|
1665
|
+
* other value (including unset) defers to the Home. A Home whose manifest
|
|
1666
|
+
* cannot be read falls back to the file store; the classifier/doctor surfaces
|
|
1667
|
+
* the manifest problem separately.
|
|
1668
|
+
*/
|
|
1669
|
+
export function resolveTaskStoreBackendForHome(home, env = process.env) {
|
|
1670
|
+
const explicit = env.YUI_STORE_BACKEND?.toLowerCase();
|
|
1671
|
+
if (explicit === "sqlite" || explicit === "file")
|
|
1672
|
+
return explicit;
|
|
1673
|
+
const schema = inspectStorageSchema(home);
|
|
1674
|
+
const layout = schema.status === "current" || schema.status === "unsupported"
|
|
1675
|
+
? schema.currentLayoutVersion
|
|
1676
|
+
: 0;
|
|
1677
|
+
if (layout < 7)
|
|
1678
|
+
return "file";
|
|
1679
|
+
// Issue 01: a layout-7 Home's authoritative backend is SQLite WAL, but only
|
|
1680
|
+
// when yui.db actually exists. A pseudo-layout-7 Home (manifest 7, no
|
|
1681
|
+
// yui.db) is classified NEEDS_STORAGE_REPAIR; until repair runs, the file
|
|
1682
|
+
// store remains the readable fallback. This keeps the Controller's backend
|
|
1683
|
+
// resolution consistent with openCompatibleFileTaskStore's physical check.
|
|
1684
|
+
// Uses the literal "yui.db" (not COMMITTED_DATABASE_FILENAME) to avoid a
|
|
1685
|
+
// circular import: sqliteStateMigration.ts imports SqliteTaskStore from here.
|
|
1686
|
+
return existsSync(join(home, "yui.db")) ? "sqlite" : "file";
|
|
1687
|
+
}
|
|
1215
1688
|
/**
|
|
1216
1689
|
* Convenience: open the store for the backend resolved from the environment
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1690
|
+
* and the Home's verified manifest (see {@link resolveTaskStoreBackendForHome}).
|
|
1691
|
+
* CLI/controller entry points call this instead of {@link openTaskStore}
|
|
1692
|
+
* directly so a layout-7 Home opens SQLite without an env opt-in.
|
|
1219
1693
|
*/
|
|
1220
1694
|
export function openConfiguredTaskStore(home, options, env = process.env) {
|
|
1221
|
-
return openTaskStore(home,
|
|
1695
|
+
return openTaskStore(home, resolveTaskStoreBackendForHome(home, env), options);
|
|
1222
1696
|
}
|
package/dist/storage/storeRpc.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { BoundedRpcClient, nextRequestId } from "../core/boundedRpc.js";
|
|
25
25
|
import { StorageCancelledError, StorageConflictError, StorageRecordError } from "./taskStore.js";
|
|
26
|
+
import { resolveTaskStoreBackendForHome } from "./sqliteStore.js";
|
|
26
27
|
// -- Read-only classification ------------------------------------------------
|
|
27
28
|
/**
|
|
28
29
|
* TaskStore methods that do not mutate. Reads use the worker's read pool
|
|
@@ -47,6 +48,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
47
48
|
"listGlobalRoleSessionSets",
|
|
48
49
|
"listTasks",
|
|
49
50
|
"getTask",
|
|
51
|
+
"readNextActionFacts",
|
|
50
52
|
"listActiveTaskIds",
|
|
51
53
|
"getTaskBrief",
|
|
52
54
|
"listChangeSets",
|
|
@@ -70,6 +72,7 @@ const READ_ONLY_STORE_METHODS = new Set([
|
|
|
70
72
|
"listWorkItems",
|
|
71
73
|
"getAgentRun",
|
|
72
74
|
"listAgentRuns",
|
|
75
|
+
"listPendingProviderRetries",
|
|
73
76
|
"peekNextAgentRunId",
|
|
74
77
|
"getReviewRound",
|
|
75
78
|
"listReviewRuns",
|
|
@@ -275,3 +278,21 @@ export function resolveStoreWorkerEnabled(env = process.env) {
|
|
|
275
278
|
const flag = env.YUI_STORE_WORKER;
|
|
276
279
|
return flag === "1" || flag?.toLowerCase() === "true";
|
|
277
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Resolve whether the persistence worker is enabled, from the Home's verified
|
|
283
|
+
* manifest (Issue 01). A Home-decided SQLite backend (layout 7, no explicit
|
|
284
|
+
* `YUI_STORE_BACKEND`) runs the worker by default — that is the product
|
|
285
|
+
* commitment layout 7 makes. An env-decided SQLite backend keeps the
|
|
286
|
+
* historical opt-in (`YUI_STORE_WORKER=1`). `YUI_STORE_WORKER=0/false`
|
|
287
|
+
* disables the worker in both cases (in-process SQLite still applies).
|
|
288
|
+
*/
|
|
289
|
+
export function resolveStoreWorkerEnabledForHome(home, env = process.env) {
|
|
290
|
+
if (resolveTaskStoreBackendForHome(home, env) !== "sqlite")
|
|
291
|
+
return false;
|
|
292
|
+
const flag = env.YUI_STORE_WORKER?.toLowerCase();
|
|
293
|
+
if (flag === "1" || flag === "true")
|
|
294
|
+
return true;
|
|
295
|
+
if (flag === "0" || flag === "false")
|
|
296
|
+
return false;
|
|
297
|
+
return env.YUI_STORE_BACKEND?.toLowerCase() !== "sqlite";
|
|
298
|
+
}
|