@zq-silk/yui 0.13.9 → 0.14.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/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
* - Crash recovery .............. WAL rollback of uncommitted transactions;
|
|
19
19
|
* outbox replay of committed-but-unacked effects.
|
|
20
20
|
* - Record family versioning .... full record (incl. schemaVersion) in payload.
|
|
21
|
-
* - Upgrade fence ............... assertHomeWritable at the write boundary.
|
|
22
21
|
* - Evidence retention .......... events/review_rounds/change_sets/
|
|
23
22
|
* integration_attempts are never pruned.
|
|
24
23
|
*
|
|
@@ -38,9 +37,8 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
38
37
|
import { join } from "node:path";
|
|
39
38
|
import { isDeepStrictEqual } from "node:util";
|
|
40
39
|
import Database from "better-sqlite3";
|
|
41
|
-
import { consumePendingBatch, mailboxTargetKey,
|
|
40
|
+
import { consumePendingBatch, mailboxTargetKey, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
42
41
|
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
43
|
-
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT } from "../review/taskFinalReviewContractEvent.js";
|
|
44
42
|
import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
|
|
45
43
|
import { validateReviewFinding } from "../review/reviewFinding.js";
|
|
46
44
|
import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
|
|
@@ -51,12 +49,10 @@ import { validateTaskWake } from "../scheduler/taskWake.js";
|
|
|
51
49
|
import { operationalTaskRecords, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
|
|
52
50
|
import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
53
51
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
54
|
-
import {
|
|
55
|
-
import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_PENDING_WAKEUP_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveRunKey, executionLaneActiveRunKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, FileTaskStore, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, validateYuiConfig } from "./taskStore.js";
|
|
52
|
+
import { CURRENT_CONFIG_SCHEMA_VERSION, CURRENT_WORK_MAILBOX_SCHEMA_VERSION, executionLaneActiveTurnKey, executionLaneActiveTurnKeyParts, StorageConflictError, StorageCancelledError, StorageRecordError, storedCapabilityGrant, storedPublicationReference, storedReleaseWorkflow, isValidCapabilityGrantTransition, isValidReleaseWorkflowTransition, pendingWakeupProjection, validateYuiConfig } from "./taskStore.js";
|
|
56
53
|
import { publicationExternalKey } from "../task/publicationReference.js";
|
|
57
54
|
import { gateArtifactKey, validateGateArtifact } from "../verification/gateArtifact.js";
|
|
58
|
-
import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION,
|
|
59
|
-
import { inspectStorageSchema } from "./storageSchema.js";
|
|
55
|
+
import { inspectSqliteSchemaMigrations, migrateSqliteSchema, SqliteSchemaMigrationError, SQLITE_AGGREGATE_VERSION, SQLITE_LAYOUT_VERSION, TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_TURN_CAP } from "./sqliteSchema.js";
|
|
60
56
|
/** Read the immutable Home identity without opening a writable Store connection. */
|
|
61
57
|
export function readSqliteHomeIdentity(rootDir, databaseFilename = "yui.db") {
|
|
62
58
|
const database = new Database(join(rootDir, databaseFilename), {
|
|
@@ -116,39 +112,18 @@ function isUniqueConstraint(error) {
|
|
|
116
112
|
&& error.code === "SQLITE_CONSTRAINT_PRIMARYKEY"
|
|
117
113
|
|| error?.code === "SQLITE_CONSTRAINT_UNIQUE";
|
|
118
114
|
}
|
|
119
|
-
/** Project a leader-role work mailbox's pending batch to a PendingWakeup (mirrors taskStore.ts). */
|
|
120
|
-
function pendingWakeupProjection(mailbox) {
|
|
121
|
-
const pending = mailbox === null ? null : pendingLane(mailbox, "normal");
|
|
122
|
-
if (mailbox === null || mailbox.target.kind !== "role" || mailbox.target.roleName !== "leader"
|
|
123
|
-
|| pending === null) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
schemaVersion: CURRENT_PENDING_WAKEUP_SCHEMA_VERSION,
|
|
128
|
-
taskId: mailbox.target.taskId,
|
|
129
|
-
reasons: [...pending.reasons],
|
|
130
|
-
requestCount: pending.requestCount,
|
|
131
|
-
firstRequestedAt: pending.firstQueuedAt,
|
|
132
|
-
lastRequestedAt: pending.lastQueuedAt
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
115
|
export class SqliteTaskStore {
|
|
136
116
|
#db;
|
|
137
117
|
#rootDir;
|
|
138
|
-
#migration;
|
|
139
118
|
#openedSchemaHead;
|
|
140
119
|
#inTransaction = false;
|
|
141
120
|
#dirty = false;
|
|
142
121
|
constructor(rootDir, _options = {}) {
|
|
143
122
|
this.#rootDir = rootDir;
|
|
144
|
-
this.#migration = _options.migration ?? false;
|
|
145
123
|
mkdirSync(rootDir, { recursive: true, mode: 0o700 });
|
|
146
124
|
const filename = _options.databaseFilename ?? "yui.db";
|
|
147
125
|
const databasePath = join(rootDir, filename);
|
|
148
126
|
const databaseExisted = existsSync(databasePath);
|
|
149
|
-
if (this.#migration && filename === "yui.db") {
|
|
150
|
-
throw new SqliteSchemaMigrationError("offline migration mode cannot open the authoritative yui.db", "admission");
|
|
151
|
-
}
|
|
152
127
|
this.#db = new Database(databasePath);
|
|
153
128
|
try {
|
|
154
129
|
// §4.1 / §9: WAL, no fsync weakening, FKs on, busy timeout for CLI contention.
|
|
@@ -158,7 +133,7 @@ export class SqliteTaskStore {
|
|
|
158
133
|
this.#db.pragma("busy_timeout = 5000");
|
|
159
134
|
this.#db.pragma("wal_autocheckpoint = 1000");
|
|
160
135
|
migrateSqliteSchema(this.#db, {
|
|
161
|
-
mode:
|
|
136
|
+
mode: !databaseExisted ? "apply" : "validate"
|
|
162
137
|
});
|
|
163
138
|
const schema = inspectSqliteSchemaMigrations(this.#db);
|
|
164
139
|
this.#openedSchemaHead = {
|
|
@@ -213,14 +188,8 @@ export class SqliteTaskStore {
|
|
|
213
188
|
this.#inTransaction = false;
|
|
214
189
|
this.#dirty = false;
|
|
215
190
|
}
|
|
216
|
-
/**
|
|
191
|
+
/** Verify the SQLite schema head at the single write boundary. */
|
|
217
192
|
#prepareWrite() {
|
|
218
|
-
// The staged migration populates the sidecar database while the upgrade
|
|
219
|
-
// fence is active (the migration IS the upgrade), so it bypasses the
|
|
220
|
-
// per-write admission check. Production stores never set migration mode.
|
|
221
|
-
if (this.#migration)
|
|
222
|
-
return;
|
|
223
|
-
assertHomeWritable(this.#rootDir);
|
|
224
193
|
const current = this.#db.prepare("SELECT version, checksum FROM schema_migrations ORDER BY version DESC LIMIT 1").get();
|
|
225
194
|
if (current?.version !== this.#openedSchemaHead.version
|
|
226
195
|
|| current.checksum !== this.#openedSchemaHead.checksum) {
|
|
@@ -245,14 +214,9 @@ export class SqliteTaskStore {
|
|
|
245
214
|
}
|
|
246
215
|
this.#begin();
|
|
247
216
|
try {
|
|
248
|
-
// Acquire SQLite's write reservation before checking the upgrade fence.
|
|
249
|
-
// A writer that started first commits before the upgrader's final
|
|
250
|
-
// transactional inventory; a writer that starts later sees the fence.
|
|
251
217
|
this.#prepareWrite();
|
|
252
218
|
const result = fn();
|
|
253
|
-
|
|
254
|
-
this.#bumpRevision();
|
|
255
|
-
}
|
|
219
|
+
this.#bumpRevision();
|
|
256
220
|
this.#commit();
|
|
257
221
|
return result;
|
|
258
222
|
}
|
|
@@ -268,19 +232,15 @@ export class SqliteTaskStore {
|
|
|
268
232
|
this.#begin();
|
|
269
233
|
try {
|
|
270
234
|
// Pin the Store's validated schema generation before user code runs.
|
|
271
|
-
// This prevents a long-lived
|
|
272
|
-
//
|
|
235
|
+
// This prevents a long-lived Store from executing SQL after the database
|
|
236
|
+
// schema head has changed underneath it.
|
|
273
237
|
this.#prepareWrite();
|
|
274
238
|
const result = run(this);
|
|
275
239
|
if (this.#dirty) {
|
|
276
240
|
if (options?.requestId !== undefined) {
|
|
277
241
|
this.#insertOutbox(options.requestId, options.outboxCommand ?? null);
|
|
278
242
|
}
|
|
279
|
-
|
|
280
|
-
// migrationSetHomeMeta; the commit must not bump it.
|
|
281
|
-
if (!this.#migration) {
|
|
282
|
-
this.#bumpRevision();
|
|
283
|
-
}
|
|
243
|
+
this.#bumpRevision();
|
|
284
244
|
}
|
|
285
245
|
this.#commit();
|
|
286
246
|
return result;
|
|
@@ -292,8 +252,8 @@ export class SqliteTaskStore {
|
|
|
292
252
|
}
|
|
293
253
|
/** Async transaction seam used by queue operations that must inspect Git
|
|
294
254
|
* before committing the durable queue state. The SQLite write transaction
|
|
295
|
-
* remains open across the awaited callback,
|
|
296
|
-
*
|
|
255
|
+
* remains open across the awaited callback, preserving the single-writer
|
|
256
|
+
* boundary. */
|
|
297
257
|
async transactionAsync(execute) {
|
|
298
258
|
if (this.#inTransaction)
|
|
299
259
|
return execute(this);
|
|
@@ -335,11 +295,7 @@ export class SqliteTaskStore {
|
|
|
335
295
|
if (options?.requestId !== undefined) {
|
|
336
296
|
this.#insertOutbox(options.requestId, options.outboxCommand ?? null);
|
|
337
297
|
}
|
|
338
|
-
|
|
339
|
-
// migrationSetHomeMeta; the commit must not bump it.
|
|
340
|
-
if (!this.#migration) {
|
|
341
|
-
this.#bumpRevision();
|
|
342
|
-
}
|
|
298
|
+
this.#bumpRevision();
|
|
343
299
|
}
|
|
344
300
|
this.#commit();
|
|
345
301
|
return result;
|
|
@@ -405,9 +361,7 @@ export class SqliteTaskStore {
|
|
|
405
361
|
if (options.requestId !== undefined) {
|
|
406
362
|
this.#insertOutbox(options.requestId, options.outboxCommand ?? commands);
|
|
407
363
|
}
|
|
408
|
-
|
|
409
|
-
this.#bumpRevision();
|
|
410
|
-
}
|
|
364
|
+
this.#bumpRevision();
|
|
411
365
|
}
|
|
412
366
|
this.#commit();
|
|
413
367
|
return results;
|
|
@@ -510,49 +464,6 @@ export class SqliteTaskStore {
|
|
|
510
464
|
if (row === undefined)
|
|
511
465
|
throw new StorageRecordError(`Task not found: ${taskId}`);
|
|
512
466
|
}
|
|
513
|
-
// -- migration bulk-load helpers (state.json -> SQLite, task-21 §8) ----------
|
|
514
|
-
// These are used only by the staged offline migration, which runs with the
|
|
515
|
-
// `migration` option (fence bypass). They seed infrastructure tables that the
|
|
516
|
-
// document owns (home identity/revision, global and per-task ID high-water
|
|
517
|
-
// marks) so the opened store continues from the same counters.
|
|
518
|
-
/** Preserve the document's Home identity and revision in `home_meta`. */
|
|
519
|
-
migrationSetHomeMeta(identity, revision) {
|
|
520
|
-
this.#mutate(() => {
|
|
521
|
-
this.#db.prepare(`UPDATE home_meta SET home_identity = ?, revision = ?, updated_at = ? WHERE id = 1`).run(this.#json(identity), revision, this.#now());
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
/** Seed a global ID high-water mark (task/project) at least `highWater`. */
|
|
525
|
-
migrationSeedGlobalSequence(name, highWater) {
|
|
526
|
-
this.#mutate(() => {
|
|
527
|
-
this.#db.prepare(`INSERT INTO global_sequences (name, high_water) VALUES (?, ?)
|
|
528
|
-
ON CONFLICT(name) DO UPDATE SET high_water = MAX(high_water, ?)`).run(name, highWater, highWater);
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
/** Seed a per-task ID high-water mark at least `highWater`. */
|
|
532
|
-
migrationSeedIdSequence(taskId, kind, highWater) {
|
|
533
|
-
this.#mutate(() => {
|
|
534
|
-
this.#db.prepare(`INSERT INTO id_sequences (task_id, kind, high_water) VALUES (?, ?, ?)
|
|
535
|
-
ON CONFLICT(task_id, kind) DO UPDATE SET high_water = MAX(high_water, ?)`).run(taskId, kind, highWater, highWater);
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
/** Restore one validated historical grant into a disposable migration output. */
|
|
539
|
-
migrationRestoreCapabilityGrant(taskId, grant) {
|
|
540
|
-
if (!this.#migration) {
|
|
541
|
-
throw new StorageRecordError("Historical capability grants may only be restored into a migration sidecar.");
|
|
542
|
-
}
|
|
543
|
-
const stored = storedCapabilityGrant(grant);
|
|
544
|
-
if (stored.taskId !== taskId) {
|
|
545
|
-
throw new StorageRecordError(`Capability grant belongs to another Task: ${stored.taskId}`);
|
|
546
|
-
}
|
|
547
|
-
this.#requireTask(taskId);
|
|
548
|
-
this.#mutate(() => {
|
|
549
|
-
const existing = this.#getPayload("capability_grants", "task_id = ? AND grant_id = ?", [taskId, stored.id]);
|
|
550
|
-
if (existing !== null) {
|
|
551
|
-
throw new StorageRecordError(`Migration cannot overwrite capability grant: ${taskId}/${stored.id}`);
|
|
552
|
-
}
|
|
553
|
-
this.#db.prepare("INSERT INTO capability_grants (task_id, grant_id, payload, updated_at) VALUES (?, ?, ?, ?)").run(taskId, stored.id, this.#json(stored), this.#now());
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
467
|
// -- generic payload helpers ------------------------------------------------
|
|
557
468
|
#getPayload(table, where, params) {
|
|
558
469
|
const row = this.#db.prepare(`SELECT payload FROM ${table} WHERE ${where}`).get(...params);
|
|
@@ -657,7 +568,7 @@ export class SqliteTaskStore {
|
|
|
657
568
|
const boundTasks = this.listTasks().filter((task) => task.projectBindings.some((binding) => binding.projectId === projectId));
|
|
658
569
|
const activeTasks = boundTasks.filter((task) => task.status === "active");
|
|
659
570
|
const unresolvedWorkItemRefs = [];
|
|
660
|
-
const
|
|
571
|
+
const activeTurnRefs = [];
|
|
661
572
|
const unresolvedIntegrationRefs = [];
|
|
662
573
|
for (const task of activeTasks) {
|
|
663
574
|
for (const workItem of this.listWorkItems(task.id)) {
|
|
@@ -665,9 +576,9 @@ export class SqliteTaskStore {
|
|
|
665
576
|
unresolvedWorkItemRefs.push(`${task.id}/${workItem.id}`);
|
|
666
577
|
}
|
|
667
578
|
}
|
|
668
|
-
for (const run of this.
|
|
579
|
+
for (const run of this.listTurns(task.id)) {
|
|
669
580
|
if (run.status === "active")
|
|
670
|
-
|
|
581
|
+
activeTurnRefs.push(`${task.id}/${run.id}`);
|
|
671
582
|
}
|
|
672
583
|
for (const attempt of this.listIntegrationAttempts(task.id)) {
|
|
673
584
|
if (attempt.status === "running" || attempt.status === "blocked") {
|
|
@@ -680,7 +591,7 @@ export class SqliteTaskStore {
|
|
|
680
591
|
boundTaskIds: boundTasks.map(({ id }) => id),
|
|
681
592
|
activeTaskIds: activeTasks.map(({ id }) => id),
|
|
682
593
|
unresolvedWorkItemRefs,
|
|
683
|
-
|
|
594
|
+
activeTurnRefs,
|
|
684
595
|
unresolvedIntegrationRefs
|
|
685
596
|
};
|
|
686
597
|
}
|
|
@@ -794,15 +705,14 @@ export class SqliteTaskStore {
|
|
|
794
705
|
const task = this.#getPayload("task_records", "task_id = ?", [taskId]);
|
|
795
706
|
if (task === null)
|
|
796
707
|
return null;
|
|
797
|
-
// One indexed query (
|
|
798
|
-
//
|
|
799
|
-
const events = this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (?,
|
|
708
|
+
// One indexed query (idx_turns_role_status) covers both the active
|
|
709
|
+
// Turns the projection waits on and the Leader Turns the budget consumes.
|
|
710
|
+
const events = this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (?, ?)", [
|
|
800
711
|
taskId,
|
|
801
|
-
TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT,
|
|
802
712
|
TASK_RECORD_RETIRED_EVENT,
|
|
803
713
|
"review.completed"
|
|
804
714
|
]), (event) => event.id);
|
|
805
|
-
const runs = operationalTaskRecords(this.#sortById(this.#listPayload("
|
|
715
|
+
const runs = operationalTaskRecords(this.#sortById(this.#listPayload("turns", "task_id = ? AND (status = 'active' OR role_name = 'leader')", [taskId]), (run) => run.id), events, "turn");
|
|
806
716
|
return {
|
|
807
717
|
task: {
|
|
808
718
|
id: task.id,
|
|
@@ -816,14 +726,12 @@ export class SqliteTaskStore {
|
|
|
816
726
|
integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
|
|
817
727
|
integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
|
|
818
728
|
reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
|
|
819
|
-
taskFinalReviewContractEvents: events
|
|
820
|
-
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
821
729
|
reviewConfig: this.getReviewConfig(),
|
|
822
730
|
openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
|
|
823
|
-
|
|
824
|
-
|
|
731
|
+
activeTurns: runs.filter((run) => run.status === "active"),
|
|
732
|
+
leaderTurns: runs.filter((run) => run.roleName === "leader"),
|
|
825
733
|
reviewOutcomeEvidence: {
|
|
826
|
-
|
|
734
|
+
turns: this.#sortById(this.#listPayload("turns", "task_id = ?", [taskId]).filter((run) => run.purpose === "review"), (run) => run.id),
|
|
827
735
|
reviewFindings: this.listReviewFindings(taskId),
|
|
828
736
|
events: events.filter((event) => event.type === "review.completed")
|
|
829
737
|
}
|
|
@@ -835,7 +743,7 @@ export class SqliteTaskStore {
|
|
|
835
743
|
return null;
|
|
836
744
|
return {
|
|
837
745
|
...base,
|
|
838
|
-
|
|
746
|
+
turns: operationalTaskRecords(this.listTurns(taskId), this.listEvents(taskId), "turn"),
|
|
839
747
|
managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
840
748
|
durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
|
|
841
749
|
integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
|
|
@@ -1552,35 +1460,35 @@ export class SqliteTaskStore {
|
|
|
1552
1460
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(stored.taskId, stored.id, stored.scope, stored.scopeRef ?? null, stored.sequence, stored.digest, this.#json(stored), stored.frozenAt);
|
|
1553
1461
|
});
|
|
1554
1462
|
}
|
|
1555
|
-
// --
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
return this.#getPayload("
|
|
1463
|
+
// -- turns -----------------------------------------------------------------
|
|
1464
|
+
nextTurnId(taskId) { return this.#nextTaskRecordId(taskId, "turn"); }
|
|
1465
|
+
peekNextTurnId(taskId) { return this.#peekTaskRecordId(taskId, "turn"); }
|
|
1466
|
+
getTurn(taskId, turnId) {
|
|
1467
|
+
return this.#getPayload("turns", "task_id = ? AND turn_id = ?", [taskId, turnId]);
|
|
1560
1468
|
}
|
|
1561
|
-
|
|
1562
|
-
return this.#sortById(this.#listPayload("
|
|
1469
|
+
listTurns(taskId) {
|
|
1470
|
+
return this.#sortById(this.#listPayload("turns", "task_id = ?", [taskId]), (turn) => turn.id);
|
|
1563
1471
|
}
|
|
1564
|
-
|
|
1565
|
-
if (
|
|
1566
|
-
this.#requireTask(
|
|
1567
|
-
if (
|
|
1568
|
-
const round = this.getReviewRound(
|
|
1472
|
+
saveTurn(turn) {
|
|
1473
|
+
if (turn.taskId !== undefined)
|
|
1474
|
+
this.#requireTask(turn.taskId);
|
|
1475
|
+
if (turn.reviewRoundId !== undefined) {
|
|
1476
|
+
const round = this.getReviewRound(turn.taskId, turn.reviewRoundId);
|
|
1569
1477
|
if (round === null) {
|
|
1570
|
-
throw new StorageRecordError(`
|
|
1478
|
+
throw new StorageRecordError(`Turn ReviewRound not found: ${turn.reviewRoundId}.`);
|
|
1571
1479
|
}
|
|
1572
|
-
const roundWorkItemId = round.workItemId
|
|
1480
|
+
const roundWorkItemId = round.workItemId;
|
|
1573
1481
|
const laneRole = round.executionGroup?.lanes
|
|
1574
|
-
.find(({ id }) => id ===
|
|
1575
|
-
if (roundWorkItemId !==
|
|
1576
|
-
|| (round.reviewerRoleName !==
|
|
1577
|
-
throw new StorageRecordError(`
|
|
1482
|
+
.find(({ id }) => id === turn.executionLaneId)?.roleName;
|
|
1483
|
+
if (roundWorkItemId !== turn.workItemId
|
|
1484
|
+
|| (round.reviewerRoleName !== turn.roleName && laneRole !== turn.roleName)) {
|
|
1485
|
+
throw new StorageRecordError(`Turn does not match ReviewRound: ${turn.id}.`);
|
|
1578
1486
|
}
|
|
1579
1487
|
}
|
|
1580
1488
|
this.#mutate(() => {
|
|
1581
|
-
this.#db.prepare(`INSERT INTO
|
|
1582
|
-
ON CONFLICT(task_id,
|
|
1583
|
-
payload = excluded.payload, updated_at = excluded.updated_at`).run(
|
|
1489
|
+
this.#db.prepare(`INSERT INTO turns (task_id, turn_id, role_name, status, payload, updated_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
1490
|
+
ON CONFLICT(task_id, turn_id) DO UPDATE SET role_name = excluded.role_name, status = excluded.status,
|
|
1491
|
+
payload = excluded.payload, updated_at = excluded.updated_at`).run(turn.taskId, turn.id, turn.roleName, turn.status, this.#json(turn), this.#now());
|
|
1584
1492
|
});
|
|
1585
1493
|
}
|
|
1586
1494
|
// -- review rounds ----------------------------------------------------------
|
|
@@ -1622,190 +1530,181 @@ export class SqliteTaskStore {
|
|
|
1622
1530
|
severity = excluded.severity, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, finding.id, finding.stableKey, finding.severity, this.#json(finding), this.#now());
|
|
1623
1531
|
});
|
|
1624
1532
|
}
|
|
1625
|
-
// -- active
|
|
1626
|
-
#
|
|
1533
|
+
// -- active turns ----------------------------------------------------------
|
|
1534
|
+
#saveActiveTurn(taskId, pointer, turnId) {
|
|
1627
1535
|
this.#mutate(() => {
|
|
1628
|
-
const payload = this.#json({ schemaVersion: 3,
|
|
1629
|
-
this.#db.prepare(`INSERT INTO
|
|
1630
|
-
ON CONFLICT(task_id, pointer) DO UPDATE SET
|
|
1536
|
+
const payload = this.#json({ schemaVersion: 3, turnId });
|
|
1537
|
+
this.#db.prepare(`INSERT INTO active_turns (task_id, pointer, turn_id, payload, updated_at) VALUES (?, ?, ?, ?, ?)
|
|
1538
|
+
ON CONFLICT(task_id, pointer) DO UPDATE SET turn_id = excluded.turn_id, payload = excluded.payload, updated_at = excluded.updated_at`).run(taskId, pointer, turnId, payload, this.#now());
|
|
1631
1539
|
});
|
|
1632
1540
|
}
|
|
1633
|
-
#
|
|
1634
|
-
const row = this.#db.prepare("SELECT
|
|
1541
|
+
#readActiveTurnPointer(taskId, pointer) {
|
|
1542
|
+
const row = this.#db.prepare("SELECT turn_id FROM active_turns WHERE task_id = ? AND pointer = ?").get(taskId, pointer);
|
|
1635
1543
|
if (row === undefined)
|
|
1636
1544
|
return null;
|
|
1637
1545
|
return {
|
|
1638
|
-
|
|
1639
|
-
|
|
1546
|
+
turnId: row.turn_id,
|
|
1547
|
+
turn: this.getTurn(taskId, row.turn_id)
|
|
1640
1548
|
};
|
|
1641
1549
|
}
|
|
1642
|
-
#
|
|
1643
|
-
if (
|
|
1644
|
-
throw new StorageRecordError(`Active
|
|
1550
|
+
#assertActiveTurnForWrite(turn, lane) {
|
|
1551
|
+
if (turn.status !== "active") {
|
|
1552
|
+
throw new StorageRecordError(`Active Turn must have active status: ${turn.id}`);
|
|
1645
1553
|
}
|
|
1646
|
-
const hasGroup =
|
|
1647
|
-
const hasLane =
|
|
1554
|
+
const hasGroup = turn.executionGroupId !== undefined;
|
|
1555
|
+
const hasLane = turn.executionLaneId !== undefined;
|
|
1648
1556
|
if (hasGroup !== hasLane) {
|
|
1649
|
-
throw new StorageRecordError(`
|
|
1557
|
+
throw new StorageRecordError(`Turn execution lineage is incomplete: ${turn.id}.`);
|
|
1650
1558
|
}
|
|
1651
1559
|
if (lane !== undefined
|
|
1652
|
-
&& (
|
|
1653
|
-
||
|
|
1654
|
-
throw new StorageRecordError(`Active
|
|
1560
|
+
&& (turn.executionGroupId !== lane.executionGroupId
|
|
1561
|
+
|| turn.executionLaneId !== lane.executionLaneId)) {
|
|
1562
|
+
throw new StorageRecordError(`Active Turn execution lineage does not match its Lane: ${turn.id}`);
|
|
1655
1563
|
}
|
|
1656
1564
|
}
|
|
1657
|
-
#
|
|
1658
|
-
const pointer = this.#
|
|
1565
|
+
#getActiveRoleTurn(taskId, roleName) {
|
|
1566
|
+
const pointer = this.#readActiveTurnPointer(taskId, roleName);
|
|
1659
1567
|
if (pointer === null)
|
|
1660
1568
|
return null;
|
|
1661
1569
|
const task = this.getTask(taskId);
|
|
1662
1570
|
if (task === null) {
|
|
1663
|
-
throw new StorageRecordError(`Active
|
|
1571
|
+
throw new StorageRecordError(`Active Turn Task is missing: ${taskId}/${roleName}`);
|
|
1664
1572
|
}
|
|
1665
|
-
if (pointer.
|
|
1573
|
+
if (pointer.turn === null) {
|
|
1666
1574
|
// Retired Tasks are an explicit historical isolation boundary. Their
|
|
1667
|
-
// retained pointer rows may intentionally reference a missing
|
|
1575
|
+
// retained pointer rows may intentionally reference a missing Turn.
|
|
1668
1576
|
if (task.status === "retired")
|
|
1669
1577
|
return null;
|
|
1670
|
-
throw new StorageRecordError(`Active
|
|
1578
|
+
throw new StorageRecordError(`Active Turn pointer is dangling: ${taskId}/${roleName}`);
|
|
1671
1579
|
}
|
|
1672
|
-
const
|
|
1580
|
+
const turn = pointer.turn;
|
|
1673
1581
|
if (task.status === "retired")
|
|
1674
|
-
return
|
|
1675
|
-
if (
|
|
1676
|
-
||
|
|
1677
|
-
||
|
|
1678
|
-
||
|
|
1679
|
-
|| (
|
|
1680
|
-
throw new StorageRecordError(`Active
|
|
1681
|
-
}
|
|
1682
|
-
return
|
|
1683
|
-
}
|
|
1684
|
-
#
|
|
1685
|
-
const pointerKey =
|
|
1686
|
-
const pointer = this.#
|
|
1582
|
+
return turn;
|
|
1583
|
+
if (turn.id !== pointer.turnId
|
|
1584
|
+
|| turn.taskId !== taskId
|
|
1585
|
+
|| turn.roleName !== roleName
|
|
1586
|
+
|| turn.status !== "active"
|
|
1587
|
+
|| (turn.executionGroupId === undefined) !== (turn.executionLaneId === undefined)) {
|
|
1588
|
+
throw new StorageRecordError(`Active Turn pointer is invalid: ${taskId}/${roleName}`);
|
|
1589
|
+
}
|
|
1590
|
+
return turn;
|
|
1591
|
+
}
|
|
1592
|
+
#getActiveLaneTurn(taskId, executionGroupId, executionLaneId) {
|
|
1593
|
+
const pointerKey = executionLaneActiveTurnKey(executionGroupId, executionLaneId);
|
|
1594
|
+
const pointer = this.#readActiveTurnPointer(taskId, pointerKey);
|
|
1687
1595
|
if (pointer === null)
|
|
1688
1596
|
return null;
|
|
1689
1597
|
const task = this.getTask(taskId);
|
|
1690
1598
|
if (task === null) {
|
|
1691
|
-
throw new StorageRecordError(`Active
|
|
1599
|
+
throw new StorageRecordError(`Active Turn Task is missing: ${taskId}/${pointerKey}`);
|
|
1692
1600
|
}
|
|
1693
|
-
if (pointer.
|
|
1601
|
+
if (pointer.turn === null) {
|
|
1694
1602
|
if (task.status === "retired")
|
|
1695
1603
|
return null;
|
|
1696
|
-
throw new StorageRecordError(`Active
|
|
1604
|
+
throw new StorageRecordError(`Active Turn pointer is dangling: ${taskId}/${pointerKey}`);
|
|
1697
1605
|
}
|
|
1698
|
-
const
|
|
1606
|
+
const turn = pointer.turn;
|
|
1699
1607
|
if (task.status === "retired")
|
|
1700
|
-
return
|
|
1701
|
-
if (
|
|
1702
|
-
||
|
|
1703
|
-
||
|
|
1704
|
-
||
|
|
1705
|
-
||
|
|
1706
|
-
throw new StorageRecordError(`Active
|
|
1608
|
+
return turn;
|
|
1609
|
+
if (turn.id !== pointer.turnId
|
|
1610
|
+
|| turn.taskId !== taskId
|
|
1611
|
+
|| turn.status !== "active"
|
|
1612
|
+
|| turn.executionGroupId !== executionGroupId
|
|
1613
|
+
|| turn.executionLaneId !== executionLaneId) {
|
|
1614
|
+
throw new StorageRecordError(`Active Turn pointer is invalid: ${taskId}/${pointerKey}`);
|
|
1707
1615
|
}
|
|
1708
|
-
return
|
|
1616
|
+
return turn;
|
|
1709
1617
|
}
|
|
1710
|
-
#
|
|
1618
|
+
#clearActiveTurn(taskId, pointer) {
|
|
1711
1619
|
this.#mutate(() => {
|
|
1712
|
-
this.#db.prepare("DELETE FROM
|
|
1620
|
+
this.#db.prepare("DELETE FROM active_turns WHERE task_id = ? AND pointer = ?").run(taskId, pointer);
|
|
1713
1621
|
});
|
|
1714
1622
|
}
|
|
1715
|
-
|
|
1716
|
-
return this.#
|
|
1623
|
+
getActiveTurn(taskId, roleName) {
|
|
1624
|
+
return this.#getActiveRoleTurn(taskId, roleName);
|
|
1717
1625
|
}
|
|
1718
|
-
|
|
1719
|
-
if (
|
|
1720
|
-
this.
|
|
1626
|
+
saveActiveTurn(turn) {
|
|
1627
|
+
if (turn.executionGroupId !== undefined || turn.executionLaneId !== undefined) {
|
|
1628
|
+
this.saveActiveExecutionLaneTurn(turn);
|
|
1721
1629
|
return;
|
|
1722
1630
|
}
|
|
1723
1631
|
this.transaction((store) => {
|
|
1724
|
-
const task = store.getTask(
|
|
1632
|
+
const task = store.getTask(turn.taskId);
|
|
1725
1633
|
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1726
|
-
throw new StorageRecordError(`Task execution is not enabled: ${
|
|
1634
|
+
throw new StorageRecordError(`Task execution is not enabled: ${turn.taskId}.`);
|
|
1727
1635
|
}
|
|
1728
|
-
this.#
|
|
1729
|
-
const current = store.
|
|
1730
|
-
if (current !== null && current.id !==
|
|
1731
|
-
throw new StorageRecordError(`Role already has an active
|
|
1636
|
+
this.#assertActiveTurnForWrite(turn);
|
|
1637
|
+
const current = store.getActiveTurn(turn.taskId, turn.roleName);
|
|
1638
|
+
if (current !== null && current.id !== turn.id) {
|
|
1639
|
+
throw new StorageRecordError(`Role already has an active Turn: ${turn.taskId}/${turn.roleName}`);
|
|
1732
1640
|
}
|
|
1733
|
-
|
|
1734
|
-
if (sessions !== null && sessions.inFlight !== null && sessions.inFlight.runId !== run.id) {
|
|
1735
|
-
throw new StorageRecordError(`Role still has an in-flight Turn: ${run.taskId}/${run.roleName}/${sessions.inFlight.runId}`);
|
|
1736
|
-
}
|
|
1737
|
-
// Keep the Run row and its active pointer in the same transaction. The
|
|
1641
|
+
// Keep the Turn row and its active pointer in the same transaction. The
|
|
1738
1642
|
// adapter may have already written the row; this upsert remains
|
|
1739
1643
|
// intentionally idempotent for that backend-neutral path.
|
|
1740
|
-
store.
|
|
1741
|
-
this.#
|
|
1644
|
+
store.saveTurn(turn);
|
|
1645
|
+
this.#saveActiveTurn(turn.taskId, turn.roleName, turn.id);
|
|
1742
1646
|
});
|
|
1743
1647
|
}
|
|
1744
|
-
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1747
|
-
// every other lane for the same Role in a multi-lane group.
|
|
1648
|
+
clearActiveTurn(taskId, roleName) {
|
|
1649
|
+
// A Role key can point at a lane-backed Turn. Remove the matching lane
|
|
1650
|
+
// pointer too while preserving every other lane for the same Role.
|
|
1748
1651
|
this.transaction(() => {
|
|
1749
|
-
const rolePointer = this.#
|
|
1750
|
-
this.#
|
|
1652
|
+
const rolePointer = this.#readActiveTurnPointer(taskId, roleName);
|
|
1653
|
+
this.#clearActiveTurn(taskId, roleName);
|
|
1751
1654
|
if (rolePointer === null)
|
|
1752
1655
|
return;
|
|
1753
|
-
const laneRows = this.#db.prepare("SELECT pointer,
|
|
1656
|
+
const laneRows = this.#db.prepare("SELECT pointer, turn_id FROM active_turns WHERE task_id = ?").all(taskId);
|
|
1754
1657
|
for (const row of laneRows) {
|
|
1755
|
-
if (
|
|
1756
|
-
&& row.
|
|
1757
|
-
this.#
|
|
1658
|
+
if (executionLaneActiveTurnKeyParts(row.pointer) !== null
|
|
1659
|
+
&& row.turn_id === rolePointer.turnId) {
|
|
1660
|
+
this.#clearActiveTurn(taskId, row.pointer);
|
|
1758
1661
|
}
|
|
1759
1662
|
}
|
|
1760
1663
|
});
|
|
1761
1664
|
}
|
|
1762
|
-
|
|
1763
|
-
return this.#
|
|
1665
|
+
getActiveExecutionLaneTurn(taskId, executionGroupId, executionLaneId) {
|
|
1666
|
+
return this.#getActiveLaneTurn(taskId, executionGroupId, executionLaneId);
|
|
1764
1667
|
}
|
|
1765
|
-
|
|
1766
|
-
if (
|
|
1767
|
-
throw new StorageRecordError(`Active execution-lane
|
|
1668
|
+
saveActiveExecutionLaneTurn(turn) {
|
|
1669
|
+
if (turn.executionGroupId === undefined || turn.executionLaneId === undefined) {
|
|
1670
|
+
throw new StorageRecordError(`Active execution-lane Turn requires group and lane ids: ${turn.id}`);
|
|
1768
1671
|
}
|
|
1769
1672
|
this.transaction((store) => {
|
|
1770
|
-
const task = store.getTask(
|
|
1673
|
+
const task = store.getTask(turn.taskId);
|
|
1771
1674
|
if (task === null || task.status !== "active" || task.executionGate.state !== "enabled") {
|
|
1772
|
-
throw new StorageRecordError(`Task execution is not enabled: ${
|
|
1675
|
+
throw new StorageRecordError(`Task execution is not enabled: ${turn.taskId}.`);
|
|
1773
1676
|
}
|
|
1774
|
-
const key =
|
|
1775
|
-
this.#
|
|
1776
|
-
executionGroupId:
|
|
1777
|
-
executionLaneId:
|
|
1677
|
+
const key = executionLaneActiveTurnKey(turn.executionGroupId, turn.executionLaneId);
|
|
1678
|
+
this.#assertActiveTurnForWrite(turn, {
|
|
1679
|
+
executionGroupId: turn.executionGroupId,
|
|
1680
|
+
executionLaneId: turn.executionLaneId
|
|
1778
1681
|
});
|
|
1779
|
-
const current = store.
|
|
1780
|
-
if (current !== null && current.id !==
|
|
1781
|
-
throw new StorageRecordError(`Execution Lane already has an active
|
|
1782
|
-
}
|
|
1783
|
-
const sessions = store.getTaskRoleSessionSet(run.taskId, run.roleName);
|
|
1784
|
-
if (sessions !== null && sessions.inFlight !== null && sessions.inFlight.runId !== run.id) {
|
|
1785
|
-
throw new StorageRecordError(`Role still has an in-flight Turn: ${run.taskId}/${run.roleName}/${sessions.inFlight.runId}`);
|
|
1682
|
+
const current = store.getActiveExecutionLaneTurn(turn.taskId, turn.executionGroupId, turn.executionLaneId);
|
|
1683
|
+
if (current !== null && current.id !== turn.id) {
|
|
1684
|
+
throw new StorageRecordError(`Execution Lane already has an active Turn: ${turn.taskId}/${key}`);
|
|
1786
1685
|
}
|
|
1787
|
-
const rolePointer = this.#
|
|
1788
|
-
store.
|
|
1789
|
-
this.#
|
|
1790
|
-
// Keep the
|
|
1791
|
-
//
|
|
1686
|
+
const rolePointer = this.#readActiveTurnPointer(turn.taskId, turn.roleName);
|
|
1687
|
+
store.saveTurn(turn);
|
|
1688
|
+
this.#saveActiveTurn(turn.taskId, key, turn.id);
|
|
1689
|
+
// Keep the Role pointer for the single-lane delivery path, but never
|
|
1690
|
+
// replace it when another Lane already owns that Role.
|
|
1792
1691
|
if (rolePointer === null) {
|
|
1793
|
-
this.#
|
|
1692
|
+
this.#saveActiveTurn(turn.taskId, turn.roleName, turn.id);
|
|
1794
1693
|
}
|
|
1795
1694
|
});
|
|
1796
1695
|
}
|
|
1797
|
-
|
|
1696
|
+
clearActiveExecutionLaneTurn(taskId, executionGroupId, executionLaneId) {
|
|
1798
1697
|
this.transaction(() => {
|
|
1799
|
-
const key =
|
|
1800
|
-
const lanePointer = this.#
|
|
1801
|
-
this.#
|
|
1698
|
+
const key = executionLaneActiveTurnKey(executionGroupId, executionLaneId);
|
|
1699
|
+
const lanePointer = this.#readActiveTurnPointer(taskId, key);
|
|
1700
|
+
this.#clearActiveTurn(taskId, key);
|
|
1802
1701
|
if (lanePointer === null)
|
|
1803
1702
|
return;
|
|
1804
|
-
const roleRows = this.#db.prepare("SELECT pointer,
|
|
1703
|
+
const roleRows = this.#db.prepare("SELECT pointer, turn_id FROM active_turns WHERE task_id = ?").all(taskId);
|
|
1805
1704
|
for (const row of roleRows) {
|
|
1806
|
-
if (
|
|
1807
|
-
&& row.
|
|
1808
|
-
this.#
|
|
1705
|
+
if (executionLaneActiveTurnKeyParts(row.pointer) === null
|
|
1706
|
+
&& row.turn_id === lanePointer.turnId) {
|
|
1707
|
+
this.#clearActiveTurn(taskId, row.pointer);
|
|
1809
1708
|
}
|
|
1810
1709
|
}
|
|
1811
1710
|
});
|
|
@@ -1930,14 +1829,14 @@ export class SqliteTaskStore {
|
|
|
1930
1829
|
this.#mutate(() => {
|
|
1931
1830
|
const seq = this.#idSequence(wake.id, "taskWake");
|
|
1932
1831
|
this.#db.prepare(`INSERT INTO task_wakes
|
|
1933
|
-
(task_id, wake_id, seq, status,
|
|
1832
|
+
(task_id, wake_id, seq, status, turn_id, from_cursor, to_cursor, reasons, payload, created_at, consumed_at)
|
|
1934
1833
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1935
1834
|
+ ` ON CONFLICT(task_id, wake_id) DO UPDATE SET
|
|
1936
1835
|
status = excluded.status,
|
|
1937
|
-
|
|
1836
|
+
turn_id = excluded.turn_id,
|
|
1938
1837
|
reasons = excluded.reasons,
|
|
1939
1838
|
payload = excluded.payload,
|
|
1940
|
-
consumed_at = excluded.consumed_at`).run(taskId, wake.id, wake.seq, wake.status, wake.
|
|
1839
|
+
consumed_at = excluded.consumed_at`).run(taskId, wake.id, wake.seq, wake.status, wake.turnId ?? null, wake.fromCursor, wake.toCursor, this.#json([...wake.reasons]), this.#json(wake), wake.createdAt, wake.consumedAt ?? null);
|
|
1941
1840
|
this.#observeHighWater(taskId, "taskWake", seq);
|
|
1942
1841
|
});
|
|
1943
1842
|
}
|
|
@@ -1982,7 +1881,7 @@ export class SqliteTaskStore {
|
|
|
1982
1881
|
nextSequence: row.next_sequence,
|
|
1983
1882
|
processing: row.processing === null ? null : this.#parse(row.processing),
|
|
1984
1883
|
pending: this.#parse(row.pending),
|
|
1985
|
-
|
|
1884
|
+
recentDedupeKeys: this.#parse(row.recent_dedupe_keys)
|
|
1986
1885
|
});
|
|
1987
1886
|
}
|
|
1988
1887
|
#targetFromCols(kind, taskId, roleName) {
|
|
@@ -1997,20 +1896,18 @@ export class SqliteTaskStore {
|
|
|
1997
1896
|
}
|
|
1998
1897
|
getWorkMailbox(target) {
|
|
1999
1898
|
const cols = this.#mailboxCols(target);
|
|
2000
|
-
const row = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending,
|
|
1899
|
+
const row = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending, recent_dedupe_keys FROM mailboxes WHERE target_key = ?").get(cols.targetKey);
|
|
2001
1900
|
return row === undefined ? null : this.#rowToMailbox(row);
|
|
2002
1901
|
}
|
|
2003
1902
|
listWorkMailboxes() {
|
|
2004
|
-
const rows = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending,
|
|
1903
|
+
const rows = this.#db.prepare("SELECT target_kind, task_id, role_name, next_sequence, processing, pending, recent_dedupe_keys FROM mailboxes ORDER BY target_key").all();
|
|
2005
1904
|
return rows.map((row) => this.#rowToMailbox(row));
|
|
2006
1905
|
}
|
|
2007
1906
|
listReadyWorkMailboxes() {
|
|
2008
|
-
const rows = this.#db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending,
|
|
1907
|
+
const rows = this.#db.prepare(`SELECT target_kind, task_id, role_name, next_sequence, processing, pending, recent_dedupe_keys
|
|
2009
1908
|
FROM mailboxes
|
|
2010
1909
|
WHERE processing IS NOT NULL
|
|
2011
|
-
OR
|
|
2012
|
-
OR json_type(pending, '$.normal') <> 'null'
|
|
2013
|
-
OR json_type(pending, '$.userCorrection') <> 'null'
|
|
1910
|
+
OR json_type(pending) <> 'null'
|
|
2014
1911
|
ORDER BY target_key`).all();
|
|
2015
1912
|
return rows.map((row) => this.#rowToMailbox(row));
|
|
2016
1913
|
}
|
|
@@ -2024,11 +1921,11 @@ export class SqliteTaskStore {
|
|
|
2024
1921
|
}
|
|
2025
1922
|
const cols = this.#mailboxCols(validated.target);
|
|
2026
1923
|
this.#mutate(() => {
|
|
2027
|
-
this.#db.prepare(`INSERT INTO mailboxes (target_kind, task_id, role_name, target_key, next_sequence, processing, pending,
|
|
1924
|
+
this.#db.prepare(`INSERT INTO mailboxes (target_kind, task_id, role_name, target_key, next_sequence, processing, pending, recent_dedupe_keys)
|
|
2028
1925
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2029
1926
|
ON CONFLICT(target_key) DO UPDATE SET next_sequence = excluded.next_sequence,
|
|
2030
1927
|
processing = excluded.processing, pending = excluded.pending,
|
|
2031
|
-
|
|
1928
|
+
recent_dedupe_keys = excluded.recent_dedupe_keys`).run(cols.targetKind, cols.taskId, cols.roleName, cols.targetKey, validated.nextSequence, validated.processing === null ? null : this.#json(validated.processing), this.#json(validated.pending), this.#json(validated.recentDedupeKeys));
|
|
2032
1929
|
});
|
|
2033
1930
|
}
|
|
2034
1931
|
removeWorkMailbox(target) {
|
|
@@ -2089,7 +1986,7 @@ export class SqliteTaskStore {
|
|
|
2089
1986
|
const target = { kind: "role", taskId: value.taskId, roleName: "leader" };
|
|
2090
1987
|
this.transaction((store) => {
|
|
2091
1988
|
const existing = store.getWorkMailbox(target);
|
|
2092
|
-
const existingPending = existing
|
|
1989
|
+
const existingPending = existing?.pending ?? null;
|
|
2093
1990
|
if (existingPending !== null
|
|
2094
1991
|
&& value.requestCount <= existingPending.requestCount) {
|
|
2095
1992
|
throw new StorageRecordError(`Pending wakeup is stale: ${value.taskId}`);
|
|
@@ -2101,60 +1998,54 @@ export class SqliteTaskStore {
|
|
|
2101
1998
|
target,
|
|
2102
1999
|
nextSequence: Math.max(existing?.nextSequence ?? 1, toSequence + 1),
|
|
2103
2000
|
processing: existing?.processing ?? null,
|
|
2104
|
-
inputDelivery: existing?.inputDelivery ?? null,
|
|
2105
2001
|
pending: {
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
},
|
|
2120
|
-
userCorrection: existing?.pending.userCorrection ?? null,
|
|
2121
|
-
cursors: existing?.pending.cursors ?? { normal: 0, userCorrection: 0 },
|
|
2122
|
-
recentDedupeKeys: existing?.pending.recentDedupeKeys ?? []
|
|
2123
|
-
}
|
|
2002
|
+
fromSequence,
|
|
2003
|
+
toSequence,
|
|
2004
|
+
reasons: [...value.reasons],
|
|
2005
|
+
refs: existingPending?.refs ?? [],
|
|
2006
|
+
requestCount: value.requestCount,
|
|
2007
|
+
firstQueuedAt: value.firstRequestedAt,
|
|
2008
|
+
lastQueuedAt: value.lastRequestedAt,
|
|
2009
|
+
sources: existingPending?.sources ?? ["pending-wakeup-projection"],
|
|
2010
|
+
dedupeKeys: existingPending?.dedupeKeys ?? [
|
|
2011
|
+
`pending-wakeup:${value.taskId}:${fromSequence}-${toSequence}`
|
|
2012
|
+
]
|
|
2013
|
+
},
|
|
2014
|
+
recentDedupeKeys: existing?.recentDedupeKeys ?? []
|
|
2124
2015
|
});
|
|
2125
2016
|
});
|
|
2126
2017
|
}
|
|
2127
2018
|
clearPendingWakeup(taskId) {
|
|
2128
2019
|
const target = { kind: "role", taskId, roleName: "leader" };
|
|
2129
2020
|
const mailbox = this.getWorkMailbox(target);
|
|
2130
|
-
if (mailbox === null || mailbox.pending
|
|
2021
|
+
if (mailbox === null || mailbox.pending === null)
|
|
2131
2022
|
return;
|
|
2132
|
-
this.saveWorkMailbox(consumePendingBatch(mailbox
|
|
2023
|
+
this.saveWorkMailbox(consumePendingBatch(mailbox));
|
|
2133
2024
|
}
|
|
2134
2025
|
// -- telemetry (§4.4) -----------------------------------------------------------
|
|
2135
2026
|
/**
|
|
2136
|
-
* Upsert one progress row. The PK is (task_id, role_name,
|
|
2027
|
+
* Upsert one progress row. The PK is (task_id, role_name, turn_id, generation,
|
|
2137
2028
|
* progress_id): a repeated progress id updates in place, so a high-frequency
|
|
2138
2029
|
* runtime telemetry observation is a single-row write that never
|
|
2139
2030
|
* rewrites global state or another Task's rows.
|
|
2140
2031
|
*/
|
|
2141
2032
|
upsertTelemetryProgress(entry) {
|
|
2142
2033
|
this.#mutate(() => {
|
|
2143
|
-
this.#db.prepare(`INSERT INTO telemetry (task_id, role_name,
|
|
2034
|
+
this.#db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at)
|
|
2144
2035
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2145
|
-
ON CONFLICT(task_id, role_name,
|
|
2146
|
-
DO UPDATE SET sequence = excluded.sequence, payload = excluded.payload, received_at = excluded.received_at`).run(entry.taskId, entry.roleName, entry.
|
|
2036
|
+
ON CONFLICT(task_id, role_name, turn_id, generation, progress_id)
|
|
2037
|
+
DO UPDATE SET sequence = excluded.sequence, payload = excluded.payload, received_at = excluded.received_at`).run(entry.taskId, entry.roleName, entry.turnId, entry.generation, entry.progressId, entry.sequence ?? null, this.#json(entry.payload), entry.receivedAt);
|
|
2147
2038
|
});
|
|
2148
2039
|
}
|
|
2149
|
-
listTelemetry(taskId,
|
|
2150
|
-
const rows =
|
|
2151
|
-
? this.#db.prepare("SELECT task_id, role_name,
|
|
2152
|
-
: this.#db.prepare("SELECT task_id, role_name,
|
|
2040
|
+
listTelemetry(taskId, turnId) {
|
|
2041
|
+
const rows = turnId === undefined
|
|
2042
|
+
? this.#db.prepare("SELECT task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? ORDER BY received_at").all(taskId)
|
|
2043
|
+
: this.#db.prepare("SELECT task_id, role_name, turn_id, generation, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? AND turn_id = ? ORDER BY received_at").all(taskId, turnId);
|
|
2153
2044
|
return rows
|
|
2154
2045
|
.map((row) => ({
|
|
2155
2046
|
taskId: row.task_id,
|
|
2156
2047
|
roleName: row.role_name,
|
|
2157
|
-
|
|
2048
|
+
turnId: row.turn_id,
|
|
2158
2049
|
generation: row.generation,
|
|
2159
2050
|
progressId: row.progress_id,
|
|
2160
2051
|
sequence: row.sequence ?? undefined,
|
|
@@ -2162,10 +2053,10 @@ export class SqliteTaskStore {
|
|
|
2162
2053
|
receivedAt: row.received_at
|
|
2163
2054
|
}));
|
|
2164
2055
|
}
|
|
2165
|
-
countTelemetry(taskId,
|
|
2166
|
-
const row =
|
|
2056
|
+
countTelemetry(taskId, turnId) {
|
|
2057
|
+
const row = turnId === undefined
|
|
2167
2058
|
? this.#db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ?").get(taskId)
|
|
2168
|
-
: this.#db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ? AND
|
|
2059
|
+
: this.#db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ? AND turn_id = ?").get(taskId, turnId);
|
|
2169
2060
|
return row.n;
|
|
2170
2061
|
}
|
|
2171
2062
|
/**
|
|
@@ -2174,35 +2065,35 @@ export class SqliteTaskStore {
|
|
|
2174
2065
|
* by task_id; it never rewrites global rows or other Tasks. Returns the number
|
|
2175
2066
|
* of rows deleted. Terminal/semantic events go to `events` and are never pruned.
|
|
2176
2067
|
*/
|
|
2177
|
-
pruneTelemetry(taskId, roleName,
|
|
2068
|
+
pruneTelemetry(taskId, roleName, turnId, generation, keep = TELEMETRY_KEEP_PER_GENERATION) {
|
|
2178
2069
|
return this.#mutate(() => {
|
|
2179
2070
|
const result = this.#db.prepare(`DELETE FROM telemetry
|
|
2180
|
-
WHERE task_id = ? AND role_name = ? AND
|
|
2181
|
-
AND (task_id, role_name,
|
|
2182
|
-
SELECT task_id, role_name,
|
|
2071
|
+
WHERE task_id = ? AND role_name = ? AND turn_id = ? AND generation = ?
|
|
2072
|
+
AND (task_id, role_name, turn_id, generation, progress_id) NOT IN (
|
|
2073
|
+
SELECT task_id, role_name, turn_id, generation, progress_id
|
|
2183
2074
|
FROM telemetry
|
|
2184
|
-
WHERE task_id = ? AND role_name = ? AND
|
|
2075
|
+
WHERE task_id = ? AND role_name = ? AND turn_id = ? AND generation = ?
|
|
2185
2076
|
ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
|
|
2186
2077
|
LIMIT ?
|
|
2187
|
-
)`).run(taskId, roleName,
|
|
2078
|
+
)`).run(taskId, roleName, turnId, generation, taskId, roleName, turnId, generation, keep);
|
|
2188
2079
|
return result.changes;
|
|
2189
2080
|
});
|
|
2190
2081
|
}
|
|
2191
2082
|
/**
|
|
2192
|
-
* Hard cap for an active
|
|
2083
|
+
* Hard cap for an active Turn (§4.4): trim oldest rows across the Turn beyond
|
|
2193
2084
|
* `cap` (default 50k). Returns the number of rows deleted.
|
|
2194
2085
|
*/
|
|
2195
|
-
|
|
2086
|
+
capTelemetryTurn(taskId, turnId, cap = TELEMETRY_TURN_CAP) {
|
|
2196
2087
|
return this.#mutate(() => {
|
|
2197
2088
|
const result = this.#db.prepare(`DELETE FROM telemetry
|
|
2198
|
-
WHERE task_id = ? AND
|
|
2199
|
-
AND (task_id, role_name,
|
|
2200
|
-
SELECT task_id, role_name,
|
|
2089
|
+
WHERE task_id = ? AND turn_id = ?
|
|
2090
|
+
AND (task_id, role_name, turn_id, generation, progress_id) NOT IN (
|
|
2091
|
+
SELECT task_id, role_name, turn_id, generation, progress_id
|
|
2201
2092
|
FROM telemetry
|
|
2202
|
-
WHERE task_id = ? AND
|
|
2093
|
+
WHERE task_id = ? AND turn_id = ?
|
|
2203
2094
|
ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
|
|
2204
2095
|
LIMIT ?
|
|
2205
|
-
)`).run(taskId,
|
|
2096
|
+
)`).run(taskId, turnId, taskId, turnId, cap);
|
|
2206
2097
|
return result.changes;
|
|
2207
2098
|
});
|
|
2208
2099
|
}
|
|
@@ -2227,63 +2118,13 @@ function validIntegrationQueueTransition(before, after) {
|
|
|
2227
2118
|
};
|
|
2228
2119
|
return allowed[before.status].includes(after.status);
|
|
2229
2120
|
}
|
|
2230
|
-
/**
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
* Backend selection is explicit (design §6); the environment switch lives in
|
|
2234
|
-
* {@link resolveTaskStoreBackend}. The file store is not removed — rollback is
|
|
2235
|
-
* a config flip.
|
|
2236
|
-
*/
|
|
2237
|
-
export function openTaskStore(home, backend, options) {
|
|
2238
|
-
if (backend === "sqlite") {
|
|
2239
|
-
return new SqliteTaskStore(home, options);
|
|
2240
|
-
}
|
|
2241
|
-
return new FileTaskStore(home);
|
|
2242
|
-
}
|
|
2243
|
-
/**
|
|
2244
|
-
* Resolve the storage backend from `YUI_STORE_BACKEND` (default `file`,
|
|
2245
|
-
* design §6). Only the exact value `sqlite` selects the SQLite store; any
|
|
2246
|
-
* other value (including unset) keeps the file store.
|
|
2247
|
-
*/
|
|
2248
|
-
export function resolveTaskStoreBackend(env = process.env) {
|
|
2249
|
-
return env.YUI_STORE_BACKEND?.toLowerCase() === "sqlite" ? "sqlite" : "file";
|
|
2250
|
-
}
|
|
2251
|
-
/**
|
|
2252
|
-
* Resolve the storage backend from the Home's verified manifest (Issue 01).
|
|
2253
|
-
*
|
|
2254
|
-
* The Home decides: a layout-7 Home's authoritative backend is SQLite WAL, so
|
|
2255
|
-
* ordinary CLI/Controller startup opens SQLite without requiring
|
|
2256
|
-
* `YUI_STORE_BACKEND=sqlite`. An explicit `YUI_STORE_BACKEND` env value still
|
|
2257
|
-
* wins — it is reserved for tests and explicit recovery commands — and any
|
|
2258
|
-
* other value (including unset) defers to the Home. A Home whose manifest
|
|
2259
|
-
* cannot be read falls back to the file store; the classifier/doctor surfaces
|
|
2260
|
-
* the manifest problem separately.
|
|
2261
|
-
*/
|
|
2262
|
-
export function resolveTaskStoreBackendForHome(home, env = process.env) {
|
|
2263
|
-
const explicit = env.YUI_STORE_BACKEND?.toLowerCase();
|
|
2264
|
-
if (explicit === "sqlite" || explicit === "file")
|
|
2265
|
-
return explicit;
|
|
2266
|
-
const schema = inspectStorageSchema(home);
|
|
2267
|
-
const layout = schema.status === "current" || schema.status === "unsupported"
|
|
2268
|
-
? schema.currentLayoutVersion
|
|
2269
|
-
: 0;
|
|
2270
|
-
if (layout < 7)
|
|
2271
|
-
return "file";
|
|
2272
|
-
// Issue 01: a layout-7 Home's authoritative backend is SQLite WAL, but only
|
|
2273
|
-
// when yui.db actually exists. A pseudo-layout-7 Home (manifest 7, no
|
|
2274
|
-
// yui.db) is classified NEEDS_STORAGE_REPAIR; until repair runs, the file
|
|
2275
|
-
// store remains the readable fallback. This keeps the Controller's backend
|
|
2276
|
-
// resolution consistent with openCompatibleFileTaskStore's physical check.
|
|
2277
|
-
// Uses the literal "yui.db" (not COMMITTED_DATABASE_FILENAME) to avoid a
|
|
2278
|
-
// circular import: sqliteStateMigration.ts imports SqliteTaskStore from here.
|
|
2279
|
-
return existsSync(join(home, "yui.db")) ? "sqlite" : "file";
|
|
2121
|
+
/** The product storage backend is one current SQLite contract. */
|
|
2122
|
+
export function resolveTaskStoreBackendForHome(_home, _env = process.env) {
|
|
2123
|
+
return "sqlite";
|
|
2280
2124
|
}
|
|
2281
2125
|
/**
|
|
2282
|
-
* Convenience
|
|
2283
|
-
* and the Home's verified manifest (see {@link resolveTaskStoreBackendForHome}).
|
|
2284
|
-
* CLI/controller entry points call this instead of {@link openTaskStore}
|
|
2285
|
-
* directly so a layout-7 Home opens SQLite without an env opt-in.
|
|
2126
|
+
* Convenience for seams which cannot receive an already-open current store.
|
|
2286
2127
|
*/
|
|
2287
|
-
export function openConfiguredTaskStore(home, options
|
|
2288
|
-
return
|
|
2128
|
+
export function openConfiguredTaskStore(home, options) {
|
|
2129
|
+
return new SqliteTaskStore(home, options);
|
|
2289
2130
|
}
|