@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const SEVERITY_RANK = {
|
|
2
|
+
p1: 5,
|
|
3
|
+
critical: 4,
|
|
4
|
+
high: 3,
|
|
5
|
+
p2: 2,
|
|
6
|
+
medium: 1,
|
|
7
|
+
low: 0
|
|
8
|
+
};
|
|
9
|
+
const PATH_PATTERN = /(?:[\w.-]+\/)+[\w.-]+\.(?:ts|tsx|js|mjs|cjs|json|md|py|sh|yml|yaml|toml|css|html)(?::\d+)?/gu;
|
|
10
|
+
/**
|
|
11
|
+
* Extract open findings from a ReviewRound. Structured ExecutionFindings on
|
|
12
|
+
* the Review's ExecutionGroup lanes win; when none exist, fall back to
|
|
13
|
+
* parsing P1/P2 lines from the Review report text.
|
|
14
|
+
*/
|
|
15
|
+
export function extractReviewFindings(round) {
|
|
16
|
+
const structured = (round.executionGroup?.lanes ?? [])
|
|
17
|
+
.flatMap((lane) => lane.result?.findings ?? [])
|
|
18
|
+
.filter((finding) => finding.status === "open");
|
|
19
|
+
if (structured.length > 0) {
|
|
20
|
+
return structured.map((finding) => ({
|
|
21
|
+
id: finding.id,
|
|
22
|
+
severity: finding.severity,
|
|
23
|
+
summary: finding.summary,
|
|
24
|
+
paths: extractPaths(finding.summary),
|
|
25
|
+
source: "structured"
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
return parseReportFindings(round.report ?? "");
|
|
29
|
+
}
|
|
30
|
+
export function planRepairWave(reviewRoundId, findings) {
|
|
31
|
+
if (findings.length === 0) {
|
|
32
|
+
return { reviewRoundId, openFindingCount: 0, groups: [] };
|
|
33
|
+
}
|
|
34
|
+
const parent = new Array(findings.length).fill(0).map((_, index) => index);
|
|
35
|
+
const find = (index) => {
|
|
36
|
+
let current = index;
|
|
37
|
+
while (parent[current] !== current)
|
|
38
|
+
current = parent[current];
|
|
39
|
+
return current;
|
|
40
|
+
};
|
|
41
|
+
const union = (left, right) => {
|
|
42
|
+
const leftRoot = find(left);
|
|
43
|
+
const rightRoot = find(right);
|
|
44
|
+
if (leftRoot !== rightRoot)
|
|
45
|
+
parent[rightRoot] = leftRoot;
|
|
46
|
+
};
|
|
47
|
+
const unpathable = [];
|
|
48
|
+
for (let index = 0; index < findings.length; index += 1) {
|
|
49
|
+
if (findings[index].paths.length === 0)
|
|
50
|
+
unpathable.push(index);
|
|
51
|
+
}
|
|
52
|
+
// Unprovable findings merge together: the planner cannot prove they are
|
|
53
|
+
// disjoint from anything, so serializing them is the fail-closed choice.
|
|
54
|
+
for (let index = 1; index < unpathable.length; index += 1) {
|
|
55
|
+
union(unpathable[0], unpathable[index]);
|
|
56
|
+
}
|
|
57
|
+
for (let left = 0; left < findings.length; left += 1) {
|
|
58
|
+
for (let right = left + 1; right < findings.length; right += 1) {
|
|
59
|
+
if (sharePath(findings[left], findings[right]))
|
|
60
|
+
union(left, right);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const byRoot = new Map();
|
|
64
|
+
for (let index = 0; index < findings.length; index += 1) {
|
|
65
|
+
const root = find(index);
|
|
66
|
+
byRoot.set(root, [...(byRoot.get(root) ?? []), index]);
|
|
67
|
+
}
|
|
68
|
+
const groups = [...byRoot.values()]
|
|
69
|
+
.map((members, position) => {
|
|
70
|
+
const waveFindings = members.map((index) => findings[index]);
|
|
71
|
+
const paths = [...new Set(waveFindings.flatMap((finding) => finding.paths))]
|
|
72
|
+
.sort();
|
|
73
|
+
const reason = paths.length === 0
|
|
74
|
+
? "No parseable paths; merged conservatively"
|
|
75
|
+
: waveFindings.length === 1
|
|
76
|
+
? `Distinct path ${paths[0]}; parallel-safe`
|
|
77
|
+
: `Shared path(s) ${paths.join(", ")}; merged`;
|
|
78
|
+
return {
|
|
79
|
+
id: `repair-${position + 1}`,
|
|
80
|
+
findingIds: waveFindings.map((finding) => finding.id),
|
|
81
|
+
paths,
|
|
82
|
+
reason
|
|
83
|
+
};
|
|
84
|
+
})
|
|
85
|
+
.sort((left, right) => {
|
|
86
|
+
const leftRank = Math.max(...left.findingIds.map((id) => SEVERITY_RANK[findings.find((finding) => finding.id === id).severity]));
|
|
87
|
+
const rightRank = Math.max(...right.findingIds.map((id) => SEVERITY_RANK[findings.find((finding) => finding.id === id).severity]));
|
|
88
|
+
if (leftRank !== rightRank)
|
|
89
|
+
return rightRank - leftRank;
|
|
90
|
+
return left.id.localeCompare(right.id, undefined, { numeric: true });
|
|
91
|
+
})
|
|
92
|
+
.map((group, index) => ({ ...group, id: `repair-${index + 1}` }));
|
|
93
|
+
return {
|
|
94
|
+
reviewRoundId,
|
|
95
|
+
openFindingCount: findings.length,
|
|
96
|
+
groups
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function sharePath(left, right) {
|
|
100
|
+
if (left.paths.length === 0 || right.paths.length === 0)
|
|
101
|
+
return false;
|
|
102
|
+
const rightPaths = new Set(right.paths.map(filePart));
|
|
103
|
+
return left.paths.some((path) => rightPaths.has(filePart(path)));
|
|
104
|
+
}
|
|
105
|
+
function filePart(path) {
|
|
106
|
+
return path.split(":")[0];
|
|
107
|
+
}
|
|
108
|
+
export function extractPaths(text) {
|
|
109
|
+
const matches = text.match(PATH_PATTERN) ?? [];
|
|
110
|
+
return [...new Set(matches)].sort();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Fallback finding extraction for Reviews without structured findings: parse
|
|
114
|
+
* P1/P2 lines from the report body. A line like "P1: src/foo.ts:12 ..." or
|
|
115
|
+
* "- P2 src/bar.ts ..." becomes one finding.
|
|
116
|
+
*/
|
|
117
|
+
export function parseReportFindings(report) {
|
|
118
|
+
const findings = [];
|
|
119
|
+
const lines = report.split(/\r?\n/u);
|
|
120
|
+
let index = 0;
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const match = /^\s*(?:[-*]\s*)?(P[12])\b[:\s-]+(.+)$/u.exec(line);
|
|
123
|
+
if (match === null)
|
|
124
|
+
continue;
|
|
125
|
+
index += 1;
|
|
126
|
+
const severity = match[1].toLowerCase();
|
|
127
|
+
const summary = match[2].trim();
|
|
128
|
+
findings.push({
|
|
129
|
+
id: `report-p${severity === "p1" ? 1 : 2}-${index}`,
|
|
130
|
+
severity,
|
|
131
|
+
summary,
|
|
132
|
+
paths: extractPaths(summary),
|
|
133
|
+
source: "report"
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return findings;
|
|
137
|
+
}
|
|
@@ -3,13 +3,18 @@ export const TASK_RECORD_ID_PREFIXES = {
|
|
|
3
3
|
workItem: "work-item",
|
|
4
4
|
agentRun: "agent-run",
|
|
5
5
|
reviewRound: "review-round",
|
|
6
|
+
reviewFinding: "review-finding",
|
|
6
7
|
changeSet: "change-set",
|
|
7
8
|
integrationAttempt: "integration",
|
|
9
|
+
integrationQueue: "integration-queue",
|
|
10
|
+
durableJob: "job",
|
|
8
11
|
message: "message",
|
|
9
12
|
inputRequest: "input",
|
|
10
13
|
decision: "decision",
|
|
11
14
|
milestone: "milestone",
|
|
12
|
-
event: "event"
|
|
15
|
+
event: "event",
|
|
16
|
+
capabilityGrant: "capability-grant",
|
|
17
|
+
releaseWorkflow: "release-workflow"
|
|
13
18
|
};
|
|
14
19
|
export function formatTaskRecordReference(taskId, localId, kind) {
|
|
15
20
|
const reference = validateTaskRecordReference({ taskId, localId }, kind);
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
|
|
5
|
+
import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
|
|
6
|
+
import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigration.js";
|
|
7
|
+
/**
|
|
8
|
+
* Default sidecar implementation: the telemetry tables live inside the Home's
|
|
9
|
+
* authoritative `yui.db` (schema migrations 1 and 5), so a database-only Home
|
|
10
|
+
* has one file to manage, back up, and migrate. The store opens its own
|
|
11
|
+
* connection to that file: the write path is a single serialized writer (the
|
|
12
|
+
* "WAL worker") and stays isolated from the business store's connection.
|
|
13
|
+
*
|
|
14
|
+
* Writes are best-effort and never block the Controller event loop: `observe`
|
|
15
|
+
* only merges into a bounded in-memory queue; a background flush drains it in
|
|
16
|
+
* batches. Queue overflow and database failures increment `dropped` and record
|
|
17
|
+
* a health warning — the semantic lane is never affected.
|
|
18
|
+
*
|
|
19
|
+
* The telemetry tables are maintained by the centralized schema migrations:
|
|
20
|
+
* `telemetry` (migration 1, §4.4) holds the bounded latest-per-key window and
|
|
21
|
+
* `telemetry_aggregate` (migration 5) holds the authoritative per-Run/generation
|
|
22
|
+
* summary, maintained by triggers so it survives window pruning.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_PAGE_LIMIT = 500;
|
|
25
|
+
export class SqliteTelemetryStore {
|
|
26
|
+
mode;
|
|
27
|
+
#path;
|
|
28
|
+
#terminalKeep;
|
|
29
|
+
#runCap;
|
|
30
|
+
#maxPending;
|
|
31
|
+
#db = null;
|
|
32
|
+
#failed = false;
|
|
33
|
+
#lastError = null;
|
|
34
|
+
#dropped = 0;
|
|
35
|
+
#coalesced = 0;
|
|
36
|
+
#applied = 0;
|
|
37
|
+
#pending = new Map();
|
|
38
|
+
#flushScheduled = false;
|
|
39
|
+
#closed = false;
|
|
40
|
+
constructor(home, options = {}) {
|
|
41
|
+
this.mode = options.mode ?? "dual";
|
|
42
|
+
this.#path = join(home, COMMITTED_DATABASE_FILENAME);
|
|
43
|
+
this.#terminalKeep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
|
|
44
|
+
this.#runCap = options.runCap ?? DEFAULT_RUN_CAP;
|
|
45
|
+
this.#maxPending = options.maxPending ?? 10_000;
|
|
46
|
+
}
|
|
47
|
+
// -- TelemetrySink -------------------------------------------------------------
|
|
48
|
+
observe(entry) {
|
|
49
|
+
if (this.#closed)
|
|
50
|
+
return;
|
|
51
|
+
const db = this.#ensureDb();
|
|
52
|
+
if (db === null) {
|
|
53
|
+
this.#dropped++;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const key = pendingKey(entry);
|
|
57
|
+
const existing = this.#pending.get(key);
|
|
58
|
+
if (existing !== undefined && !isNewer(entry, existing)) {
|
|
59
|
+
// Same hook replayed with an older/equal sequence: fold onto the row
|
|
60
|
+
// already queued. Replays never add rows.
|
|
61
|
+
this.#coalesced++;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (existing !== undefined)
|
|
65
|
+
this.#coalesced++;
|
|
66
|
+
if (this.#pending.size >= this.#maxPending && existing === undefined) {
|
|
67
|
+
this.#dropped++;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
this.#pending.set(key, entry);
|
|
71
|
+
this.#scheduleFlush();
|
|
72
|
+
}
|
|
73
|
+
health() {
|
|
74
|
+
return {
|
|
75
|
+
mode: this.mode,
|
|
76
|
+
available: !this.#failed && !this.#closed,
|
|
77
|
+
dropped: this.#dropped,
|
|
78
|
+
coalesced: this.#coalesced,
|
|
79
|
+
lastError: this.#lastError,
|
|
80
|
+
rows: this.#failed ? 0 : this.count()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
async close() {
|
|
84
|
+
if (this.#closed)
|
|
85
|
+
return;
|
|
86
|
+
this.#closed = true;
|
|
87
|
+
await this.#flushPending();
|
|
88
|
+
this.#db?.close();
|
|
89
|
+
this.#db = null;
|
|
90
|
+
}
|
|
91
|
+
// -- TelemetryReader -----------------------------------------------------------
|
|
92
|
+
count(taskId, runId) {
|
|
93
|
+
const db = this.#ensureDb();
|
|
94
|
+
if (db === null)
|
|
95
|
+
return 0;
|
|
96
|
+
if (taskId === undefined) {
|
|
97
|
+
return db.prepare("SELECT COUNT(*) AS n FROM telemetry").get().n;
|
|
98
|
+
}
|
|
99
|
+
if (runId === undefined) {
|
|
100
|
+
return db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ?").get(taskId).n;
|
|
101
|
+
}
|
|
102
|
+
return db.prepare("SELECT COUNT(*) AS n FROM telemetry WHERE task_id = ? AND run_id = ?").get(taskId, runId).n;
|
|
103
|
+
}
|
|
104
|
+
list(taskId, runId, page = { limit: 100, offset: 0 }) {
|
|
105
|
+
const db = this.#ensureDb();
|
|
106
|
+
if (db === null)
|
|
107
|
+
return { items: [], nextOffset: null };
|
|
108
|
+
const limit = Math.min(Math.max(1, Math.trunc(page.limit)), MAX_PAGE_LIMIT);
|
|
109
|
+
const offset = Math.max(0, Math.trunc(page.offset));
|
|
110
|
+
const rows = runId === undefined
|
|
111
|
+
? db.prepare("SELECT task_id, role_name, run_id, generation, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? ORDER BY received_at, progress_id LIMIT ? OFFSET ?").all(taskId, limit, offset)
|
|
112
|
+
: db.prepare("SELECT task_id, role_name, run_id, generation, progress_id, sequence, payload, received_at FROM telemetry WHERE task_id = ? AND run_id = ? ORDER BY received_at, progress_id LIMIT ? OFFSET ?").all(taskId, runId, limit, offset);
|
|
113
|
+
const items = rows.map(rowToEntry);
|
|
114
|
+
const total = this.count(taskId, runId);
|
|
115
|
+
const nextOffset = offset + items.length < total ? offset + items.length : null;
|
|
116
|
+
return { items, nextOffset };
|
|
117
|
+
}
|
|
118
|
+
aggregate(taskId, runId) {
|
|
119
|
+
const db = this.#ensureDb();
|
|
120
|
+
if (db === null)
|
|
121
|
+
return null;
|
|
122
|
+
const rows = db.prepare("SELECT task_id, role_name, run_id, generation, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? AND run_id = ?").all(taskId, runId);
|
|
123
|
+
if (rows.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
return mergeAggregates(rows);
|
|
126
|
+
}
|
|
127
|
+
aggregateGeneration(taskId, roleName, runId, generation) {
|
|
128
|
+
const db = this.#ensureDb();
|
|
129
|
+
if (db === null)
|
|
130
|
+
return null;
|
|
131
|
+
const row = db.prepare("SELECT task_id, role_name, run_id, generation, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? AND role_name = ? AND run_id = ? AND generation = ?").get(taskId, roleName, runId, generation);
|
|
132
|
+
if (row === undefined)
|
|
133
|
+
return null;
|
|
134
|
+
return {
|
|
135
|
+
taskId: row.task_id,
|
|
136
|
+
roleName: row.role_name,
|
|
137
|
+
runId: row.run_id,
|
|
138
|
+
generation: row.generation,
|
|
139
|
+
firstAt: row.first_at,
|
|
140
|
+
lastAt: row.last_at,
|
|
141
|
+
count: row.count,
|
|
142
|
+
maxSequence: row.max_sequence,
|
|
143
|
+
errorCount: row.error_count
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
listRunAggregates(taskId) {
|
|
147
|
+
const db = this.#ensureDb();
|
|
148
|
+
if (db === null)
|
|
149
|
+
return [];
|
|
150
|
+
const rows = db.prepare("SELECT task_id, role_name, run_id, generation, first_at, last_at, count, max_sequence, error_count FROM telemetry_aggregate WHERE task_id = ? ORDER BY run_id, generation").all(taskId);
|
|
151
|
+
return rows.map((row) => ({
|
|
152
|
+
taskId: row.task_id,
|
|
153
|
+
roleName: row.role_name,
|
|
154
|
+
runId: row.run_id,
|
|
155
|
+
generation: row.generation,
|
|
156
|
+
firstAt: row.first_at,
|
|
157
|
+
lastAt: row.last_at,
|
|
158
|
+
count: row.count,
|
|
159
|
+
maxSequence: row.max_sequence,
|
|
160
|
+
errorCount: row.error_count
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
revision() {
|
|
164
|
+
return this.#applied;
|
|
165
|
+
}
|
|
166
|
+
latestProgressEvents(taskId) {
|
|
167
|
+
const db = this.#ensureDb();
|
|
168
|
+
if (db === null)
|
|
169
|
+
return [];
|
|
170
|
+
const rows = db.prepare(`SELECT task_id, role_name, run_id, generation, progress_id, sequence, payload, received_at FROM (
|
|
171
|
+
SELECT *, ROW_NUMBER() OVER (
|
|
172
|
+
PARTITION BY run_id
|
|
173
|
+
ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
|
|
174
|
+
) AS rn FROM telemetry WHERE task_id = ?
|
|
175
|
+
) WHERE rn = 1`).all(taskId);
|
|
176
|
+
return rows.map((row) => {
|
|
177
|
+
const entry = rowToEntry(row);
|
|
178
|
+
const payload = { ...entry.payload };
|
|
179
|
+
payload.runId = entry.runId;
|
|
180
|
+
payload.progressId = entry.progressId;
|
|
181
|
+
// The semantic progress event carries progressAt = observation time;
|
|
182
|
+
// mirror that so liveness folds see the same timestamp.
|
|
183
|
+
payload.progressAt = entry.receivedAt;
|
|
184
|
+
return {
|
|
185
|
+
schemaVersion: 2,
|
|
186
|
+
id: `telemetry-progress-${entry.runId}-${entry.progressId}`,
|
|
187
|
+
taskId: entry.taskId,
|
|
188
|
+
type: "runtime.provider-turn-progress",
|
|
189
|
+
payload,
|
|
190
|
+
createdAt: entry.receivedAt
|
|
191
|
+
};
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
// -- retention -----------------------------------------------------------------
|
|
195
|
+
pruneGeneration(taskId, roleName, runId, generation, keep = this.#terminalKeep) {
|
|
196
|
+
const db = this.#ensureDb();
|
|
197
|
+
if (db === null)
|
|
198
|
+
return 0;
|
|
199
|
+
const result = db.prepare(`DELETE FROM telemetry
|
|
200
|
+
WHERE task_id = ? AND role_name = ? AND run_id = ? AND generation = ?
|
|
201
|
+
AND (task_id, role_name, run_id, generation, progress_id) NOT IN (
|
|
202
|
+
SELECT task_id, role_name, run_id, generation, progress_id FROM telemetry
|
|
203
|
+
WHERE task_id = ? AND role_name = ? AND run_id = ? AND generation = ?
|
|
204
|
+
ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
|
|
205
|
+
LIMIT ?
|
|
206
|
+
)`).run(taskId, roleName, runId, generation, taskId, roleName, runId, generation, keep);
|
|
207
|
+
return result.changes;
|
|
208
|
+
}
|
|
209
|
+
capRun(taskId, runId, cap = this.#runCap) {
|
|
210
|
+
const db = this.#ensureDb();
|
|
211
|
+
if (db === null)
|
|
212
|
+
return 0;
|
|
213
|
+
const result = db.prepare(`DELETE FROM telemetry
|
|
214
|
+
WHERE task_id = ? AND run_id = ?
|
|
215
|
+
AND (task_id, role_name, run_id, generation, progress_id) NOT IN (
|
|
216
|
+
SELECT task_id, role_name, run_id, generation, progress_id FROM telemetry
|
|
217
|
+
WHERE task_id = ? AND run_id = ?
|
|
218
|
+
ORDER BY COALESCE(sequence, -1) DESC, received_at DESC, progress_id ASC
|
|
219
|
+
LIMIT ?
|
|
220
|
+
)`).run(taskId, runId, taskId, runId, cap);
|
|
221
|
+
return result.changes;
|
|
222
|
+
}
|
|
223
|
+
importGeneration(entries, aggregate) {
|
|
224
|
+
const db = this.#ensureDb();
|
|
225
|
+
if (db === null) {
|
|
226
|
+
this.#dropped += entries.length;
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, run_id, generation, progress_id, sequence, payload, received_at)
|
|
230
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
231
|
+
ON CONFLICT(task_id, role_name, run_id, generation, progress_id) DO UPDATE SET
|
|
232
|
+
sequence = excluded.sequence,
|
|
233
|
+
payload = excluded.payload,
|
|
234
|
+
received_at = excluded.received_at
|
|
235
|
+
WHERE COALESCE(excluded.sequence, -1) > COALESCE(telemetry.sequence, -1)
|
|
236
|
+
OR (excluded.sequence IS telemetry.sequence AND excluded.received_at >= telemetry.received_at)`);
|
|
237
|
+
const upsertAggregate = db.prepare(`INSERT INTO telemetry_aggregate
|
|
238
|
+
(task_id, role_name, run_id, generation, first_at, last_at, count, max_sequence, error_count, updated_at)
|
|
239
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
240
|
+
ON CONFLICT(task_id, role_name, run_id, generation) DO UPDATE SET
|
|
241
|
+
first_at = excluded.first_at,
|
|
242
|
+
last_at = excluded.last_at,
|
|
243
|
+
count = excluded.count,
|
|
244
|
+
max_sequence = excluded.max_sequence,
|
|
245
|
+
error_count = excluded.error_count,
|
|
246
|
+
updated_at = excluded.updated_at`);
|
|
247
|
+
db.transaction(() => {
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
upsert.run(entry.taskId, entry.roleName, entry.runId, entry.generation, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
|
|
250
|
+
}
|
|
251
|
+
upsertAggregate.run(aggregate.taskId, aggregate.roleName, aggregate.runId, aggregate.generation, aggregate.firstAt, aggregate.lastAt, aggregate.count, aggregate.maxSequence, aggregate.errorCount, aggregate.lastAt);
|
|
252
|
+
})();
|
|
253
|
+
this.#applied += entries.length;
|
|
254
|
+
}
|
|
255
|
+
async flush() {
|
|
256
|
+
await this.#flushPending();
|
|
257
|
+
}
|
|
258
|
+
// -- internals -----------------------------------------------------------------
|
|
259
|
+
#ensureDb() {
|
|
260
|
+
if (this.#db !== null)
|
|
261
|
+
return this.#db;
|
|
262
|
+
if (this.#failed || this.#closed)
|
|
263
|
+
return null;
|
|
264
|
+
try {
|
|
265
|
+
// The telemetry tables live in the Home's authoritative database; the
|
|
266
|
+
// store never creates `yui.db` itself. Wiring fails closed on Homes
|
|
267
|
+
// without a database instead of silently materializing an empty one.
|
|
268
|
+
if (!existsSync(this.#path)) {
|
|
269
|
+
throw new Error(`Telemetry database not found: ${this.#path}`);
|
|
270
|
+
}
|
|
271
|
+
const db = new Database(this.#path);
|
|
272
|
+
db.pragma("journal_mode = WAL");
|
|
273
|
+
db.pragma("synchronous = FULL");
|
|
274
|
+
db.pragma("foreign_keys = ON");
|
|
275
|
+
db.pragma("busy_timeout = 5000");
|
|
276
|
+
db.pragma("wal_autocheckpoint = 1000");
|
|
277
|
+
migrateSqliteSchema(db);
|
|
278
|
+
this.#db = db;
|
|
279
|
+
return db;
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
this.#failed = true;
|
|
283
|
+
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
#scheduleFlush() {
|
|
288
|
+
if (this.#flushScheduled)
|
|
289
|
+
return;
|
|
290
|
+
this.#flushScheduled = true;
|
|
291
|
+
const handle = setImmediate(() => {
|
|
292
|
+
this.#flushScheduled = false;
|
|
293
|
+
void this.#flushPending();
|
|
294
|
+
});
|
|
295
|
+
handle.unref?.();
|
|
296
|
+
}
|
|
297
|
+
async #flushPending() {
|
|
298
|
+
if (this.#pending.size === 0)
|
|
299
|
+
return;
|
|
300
|
+
const db = this.#ensureDb();
|
|
301
|
+
if (db === null) {
|
|
302
|
+
this.#dropped += this.#pending.size;
|
|
303
|
+
this.#pending.clear();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const batch = new Map(this.#pending);
|
|
307
|
+
this.#pending.clear();
|
|
308
|
+
const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, run_id, generation, progress_id, sequence, payload, received_at)
|
|
309
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
310
|
+
ON CONFLICT(task_id, role_name, run_id, generation, progress_id) DO UPDATE SET
|
|
311
|
+
sequence = excluded.sequence,
|
|
312
|
+
payload = excluded.payload,
|
|
313
|
+
received_at = excluded.received_at
|
|
314
|
+
WHERE COALESCE(excluded.sequence, -1) > COALESCE(telemetry.sequence, -1)
|
|
315
|
+
OR (excluded.sequence IS telemetry.sequence AND excluded.received_at >= telemetry.received_at)`);
|
|
316
|
+
try {
|
|
317
|
+
db.transaction(() => {
|
|
318
|
+
for (const entry of batch.values()) {
|
|
319
|
+
upsert.run(entry.taskId, entry.roleName, entry.runId, entry.generation, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
|
|
320
|
+
}
|
|
321
|
+
})();
|
|
322
|
+
this.#applied += batch.size;
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
326
|
+
this.#dropped += batch.size;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function pendingKey(entry) {
|
|
331
|
+
return `${entry.taskId}\u0000${entry.roleName}\u0000${entry.runId}\u0000${entry.generation}\u0000${entry.progressId}`;
|
|
332
|
+
}
|
|
333
|
+
/** True when `candidate` should replace `current` (sequence/receivedAt only move forward). */
|
|
334
|
+
function isNewer(candidate, current) {
|
|
335
|
+
if (candidate.sequence !== undefined && current.sequence !== undefined) {
|
|
336
|
+
return candidate.sequence > current.sequence;
|
|
337
|
+
}
|
|
338
|
+
if (candidate.sequence !== undefined)
|
|
339
|
+
return true;
|
|
340
|
+
if (current.sequence !== undefined)
|
|
341
|
+
return false;
|
|
342
|
+
return Date.parse(candidate.receivedAt) >= Date.parse(current.receivedAt);
|
|
343
|
+
}
|
|
344
|
+
function rowToEntry(row) {
|
|
345
|
+
return {
|
|
346
|
+
taskId: row.task_id,
|
|
347
|
+
roleName: row.role_name,
|
|
348
|
+
runId: row.run_id,
|
|
349
|
+
generation: row.generation,
|
|
350
|
+
progressId: row.progress_id,
|
|
351
|
+
...(row.sequence === null ? {} : { sequence: row.sequence }),
|
|
352
|
+
payload: JSON.parse(row.payload),
|
|
353
|
+
receivedAt: row.received_at
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function mergeAggregates(rows) {
|
|
357
|
+
let firstAt = rows[0].first_at;
|
|
358
|
+
let lastAt = rows[0].last_at;
|
|
359
|
+
let count = 0;
|
|
360
|
+
let errorCount = 0;
|
|
361
|
+
let maxSequence = null;
|
|
362
|
+
let roleName = rows[0].role_name;
|
|
363
|
+
for (const row of rows) {
|
|
364
|
+
if (row.first_at < firstAt)
|
|
365
|
+
firstAt = row.first_at;
|
|
366
|
+
if (row.last_at > lastAt) {
|
|
367
|
+
lastAt = row.last_at;
|
|
368
|
+
roleName = row.role_name;
|
|
369
|
+
}
|
|
370
|
+
count += row.count;
|
|
371
|
+
errorCount += row.error_count;
|
|
372
|
+
if (row.max_sequence !== null && (maxSequence === null || row.max_sequence > maxSequence)) {
|
|
373
|
+
maxSequence = row.max_sequence;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
taskId: rows[0].task_id,
|
|
378
|
+
roleName,
|
|
379
|
+
runId: rows[0].run_id,
|
|
380
|
+
generation: "*",
|
|
381
|
+
firstAt,
|
|
382
|
+
lastAt,
|
|
383
|
+
count,
|
|
384
|
+
maxSequence,
|
|
385
|
+
errorCount
|
|
386
|
+
};
|
|
387
|
+
}
|