@zq-silk/yui 0.15.12 → 0.16.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 +3 -2
- package/ARCHITECTURE.zh-CN.md +3 -2
- package/dist/artifacts/artifactCommitLock.js +16 -55
- package/dist/artifacts/taskArtifactRepository.js +3 -3
- package/dist/cli/agentConfigurationPicker.js +1 -1
- package/dist/cli/commandCatalog.js +57 -41
- package/dist/cli/completion.js +18 -18
- package/dist/cli/completionWizard.js +1 -1
- package/dist/cli/dynamicCompletion.js +1 -1
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/invocationAuthority.js +64 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli/parseRepeatable.js +35 -0
- package/dist/cli/updatePorts.js +26 -5
- package/dist/cli.js +1280 -1243
- package/dist/commands/agentCommands.js +4 -4
- package/dist/commands/capabilityCommands.js +1 -1
- package/dist/commands/configCommands.js +6 -57
- package/dist/commands/configOverview.js +2 -2
- package/dist/commands/durableJobCommands.js +12 -6
- package/dist/commands/globalRoleCommands.js +53 -29
- package/dist/commands/grantCommands.js +0 -3
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/operatorCommands.js +3 -3
- package/dist/commands/projectCommands.js +1 -10
- package/dist/commands/resourcesCommands.js +10 -40
- package/dist/commands/roleConfiguration.js +2 -6
- package/dist/commands/taskActivationCommands.js +4 -8
- package/dist/commands/taskAgentCapabilities.js +9 -0
- package/dist/commands/taskCommandSupport.js +155 -0
- package/dist/commands/taskCommandTypes.js +1 -0
- package/dist/commands/taskCommands.js +271 -710
- package/dist/commands/taskCompletionGate.js +1 -1
- package/dist/commands/taskExecutionCommands.js +1 -1
- package/dist/commands/taskFactCommands.js +325 -0
- package/dist/commands/taskInputCommands.js +12 -42
- package/dist/commands/taskIntegrationCommands.js +17 -57
- package/dist/commands/taskNextActionCommand.js +7 -6
- package/dist/commands/taskPublicationAdoptCommand.js +3 -3
- package/dist/commands/taskPublicationCommands.js +1 -1
- package/dist/commands/taskPublicationVerifyCommand.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -94
- package/dist/commands/taskUpstreamCommands.js +19 -8
- package/dist/commands/workflowCommands.js +6 -1
- package/dist/completion/completionInstaller.js +26 -26
- package/dist/completion/completionState.js +26 -26
- package/dist/completion/fileCompletionManager.js +6 -11
- package/dist/config/configCatalog.js +0 -2
- package/dist/config/timeZone.js +14 -0
- package/dist/config/yuiConfig.js +0 -29
- package/dist/context/runContextPack.js +2 -3
- package/dist/context/taskCatalog.js +3 -5
- package/dist/context/taskContext.js +139 -151
- package/dist/context/wakeRunReferences.js +11 -0
- package/dist/controller/agentCapabilities.js +58 -0
- package/dist/controller/agentRuntimeObserver.js +6 -11
- package/dist/controller/clientRuntime.js +22 -14
- package/dist/controller/controller.js +9 -22
- package/dist/controller/fileSchedulerStoreAdapter.js +117 -212
- package/dist/controller/jobClient.js +4 -4
- package/dist/controller/jobControl.js +60 -25
- package/dist/controller/jobSupervisor.js +4 -4
- package/dist/controller/providerRetryDelivery.js +7 -7
- package/dist/controller/runtime.js +51 -44
- package/dist/controller/runtimeEventProcessor.js +0 -5
- package/dist/controller/sessionOwnerReconciliation.js +5 -0
- package/dist/controller/taskAgentError.js +35 -0
- package/dist/coordination/workMailboxQueue.js +2 -24
- package/dist/core/controllerClient.js +2 -2
- package/dist/core/fileLockOwner.js +74 -0
- package/dist/decision/decision.js +17 -0
- package/dist/doctor/doctor.js +18 -9
- package/dist/event/taskEvent.js +12 -0
- package/dist/execution/workItemExecutionProjection.js +2 -2
- package/dist/executor/agentCapabilityConfig.js +43 -0
- package/dist/executor/agentConfigurationCatalog.js +49 -11
- package/dist/executor/agentExecutor.js +5 -8
- package/dist/executor/fileRoleLaunchPlanner.js +11 -53
- package/dist/executor/taskAgentCapabilities.js +64 -0
- package/dist/integration/deliveryObligation.js +1 -69
- package/dist/integration/gitIntegrationService.js +60 -84
- package/dist/integration/integrationAttempt.js +3 -14
- package/dist/integration/integrationSourceApplication.js +4 -12
- package/dist/integration/manifestTags.js +2 -2
- package/dist/job/durableJob.js +4 -25
- package/dist/job/jobAssignmentScope.js +22 -0
- package/dist/job/jobOperation.js +16 -0
- package/dist/job/jobRunner.js +2 -1
- package/dist/job/stepDirectory.js +15 -0
- package/dist/kernel/builtinCapabilities.js +15 -11
- package/dist/kernel/kernelPorts.js +1 -17
- package/dist/lifecycle/exactRunTerminalization.js +1 -9
- package/dist/message/inputControlResolution.js +3 -4
- package/dist/message/message.js +17 -20
- package/dist/message/messageContinuation.js +1 -1
- package/dist/milestone/milestone.js +11 -0
- package/dist/observability/orchestrationMetrics.js +24 -1
- package/dist/observability/runtimeIdentity.js +1 -17
- package/dist/output/agentConfigurationPresentation.js +2 -0
- package/dist/output/timePresentation.js +1 -14
- package/dist/plugins/pluginService.js +12 -4
- package/dist/release/releaseWorkflowPorts.js +45 -96
- package/dist/release/runtimeRelease.js +9 -15
- package/dist/repository/projectMaintenanceLock.js +23 -64
- package/dist/repository/taskBaseFreshness.js +1 -1
- package/dist/repository/taskWorkspaceCoordinator.js +13 -22
- package/dist/repository/taskWorkspacePreparer.js +29 -25
- package/dist/repository/workspaceCleanupInspection.js +5 -5
- package/dist/resources/autoResourceGc.js +4 -77
- package/dist/resources/liveReferences.js +7 -54
- package/dist/resources/resourceDiscovery.js +5 -118
- package/dist/resources/resourceGc.js +233 -181
- package/dist/resources/resourceRegistrar.js +5 -4
- package/dist/resources/resourceRegistry.js +3 -37
- package/dist/resources/resourceRegistryStore.js +13 -36
- package/dist/resources/sqliteResourceRegistry.js +43 -26
- package/dist/review/deltaRecheck.js +1 -1
- package/dist/review/reviewAcceptance.js +1 -1
- package/dist/review/reviewDecision.js +1 -1
- package/dist/review/reviewRound.js +8 -7
- package/dist/review/taskFinalReviewContractResolution.js +1 -1
- package/dist/runtime/acpProtocol.js +4 -51
- package/dist/runtime/acpSession.js +10 -75
- package/dist/runtime/acpSessionConfiguration.js +1 -8
- package/dist/runtime/agentEndpoint.js +3 -2
- package/dist/runtime/agentFailureContext.js +43 -0
- package/dist/runtime/agentHost.js +15 -15
- package/dist/runtime/codexInteractiveHost.js +2 -2
- package/dist/runtime/index.js +0 -1
- package/dist/runtime/managedCaller.js +21 -1
- package/dist/runtime/providerErrors.js +38 -0
- package/dist/runtime/runtimeObservation.js +0 -5
- package/dist/runtime/runtimeSessionCandidate.js +2 -3
- package/dist/runtime/sessionOwnerIdentity.js +1 -1
- package/dist/runtime/structuredProviderHost.js +4 -41
- package/dist/runtime/tmuxAdapters.js +6 -8
- package/dist/scheduler/activeRoleRunDelivery.js +7 -7
- package/dist/scheduler/leaderWakeupProcessor.js +19 -6
- package/dist/scheduler/operatorEvent.js +12 -20
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +5 -8
- package/dist/scheduler/roleRunLiveness.js +4 -4
- package/dist/scheduler/roleRunStall.js +16 -23
- package/dist/scheduler/taskExecutionProjection.js +25 -33
- package/dist/scheduler/taskObservabilityProjection.js +0 -1
- package/dist/scheduler/taskWake.js +5 -10
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/scheduler/wakeupQueue.js +2 -4
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/contextRecords.js +106 -0
- package/dist/storage/currentTaskStore.js +1 -1
- package/dist/storage/homeLayout.js +13 -17
- package/dist/storage/migrations/agentFailureContext.js +22 -0
- package/dist/storage/migrations/currentInputContract.js +86 -0
- package/dist/storage/migrations/currentRuntimeContract.js +228 -0
- package/dist/storage/migrations/historicalVerificationPlan.js +35 -0
- package/dist/storage/migrations/integrationContinuation.js +5 -4
- package/dist/storage/migrations/narrowAgentFailureContext.js +65 -0
- package/dist/storage/migrations/notificationOnlyWakes.js +74 -0
- package/dist/storage/migrations/verificationPlanV1.js +162 -0
- package/dist/storage/migrations/verificationPolicy.js +74 -0
- package/dist/storage/migrations/workItemHistory.js +46 -0
- package/dist/storage/persistenceWorker.js +12 -4
- package/dist/storage/recordValidation.js +87 -0
- package/dist/storage/sqliteSchema.js +142 -1
- package/dist/storage/sqliteStore.js +156 -162
- package/dist/storage/storageSchema.js +2 -14
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +15 -7
- package/dist/storage/taskStore.js +3 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +25 -37
- package/dist/task/completionReadiness.js +5 -24
- package/dist/task/draftPlan.js +0 -53
- package/dist/task/nextAction.js +7 -13
- package/dist/task/remoteDeliveryService.js +89 -0
- package/dist/task/taskActivation.js +2 -25
- package/dist/task/taskActivationService.js +0 -2
- package/dist/task/taskRecordReference.js +0 -1
- package/dist/task/taskSubmission.js +3 -14
- package/dist/telemetry/sqliteTelemetryBatch.js +29 -0
- package/dist/telemetry/sqliteTelemetryStore.js +49 -49
- package/dist/telemetry/telemetryWiring.js +4 -3
- package/dist/tmux/tmuxManager.js +29 -20
- package/dist/verification/gateArtifact.js +15 -24
- package/dist/verification/gateArtifactStore.js +14 -7
- package/dist/verification/verificationGateService.js +29 -88
- package/dist/verification/verificationPlan.js +27 -72
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/taskSurface.js +4 -4
- package/dist/web/webServer.js +4 -6
- package/dist/web/webSnapshot.js +3 -83
- package/dist/web/webTaskSurface.js +17 -46
- package/dist/workItem/workItem.js +1 -12
- package/docs/architecture/capabilities-and-resources.md +14 -2
- package/docs/architecture/capabilities-and-resources.zh-CN.md +11 -1
- package/docs/managed-turn-and-session-runtime.md +83 -10
- package/docs/managed-turn-and-session-runtime.zh-CN.md +65 -11
- package/docs/plugin-sdk.md +4 -2
- package/docs/provider-runtime.md +3 -1
- package/docs/provider-runtime.zh-CN.md +4 -2
- package/docs/release-workflow.md +190 -7
- package/docs/release-workflow.zh-CN.md +139 -5
- package/docs/roles-and-configuration.md +29 -0
- package/docs/roles-and-configuration.zh-CN.md +21 -0
- package/docs/sqlite-control-plane-design.md +16 -3
- package/docs/sqlite-control-plane-design.zh-CN.md +13 -2
- package/docs/task-dag-semantics.md +3 -3
- package/docs/task-dag-semantics.zh-CN.md +2 -2
- package/docs/task-delivery.md +91 -5
- package/docs/task-delivery.zh-CN.md +72 -4
- package/docs/task-discovery.md +10 -11
- package/docs/task-discovery.zh-CN.md +8 -9
- package/docs/testing/verification-levels.md +145 -9
- package/docs/testing/verification-levels.zh-CN.md +103 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -0
- package/skills/yui-leader/references/integration.md +9 -5
- package/skills/yui-leader/references/planning.md +9 -3
- package/skills/yui-operator/SKILL.md +15 -10
- package/skills/yui-runtime/SKILL.md +6 -6
- package/skills/yui-runtime/references/recovery.md +47 -0
- package/dist/agent/agentRegistry.js +0 -10
- package/dist/agentRun/runIdentity.js +0 -22
- package/dist/commands/deliveryGuardPreflight.js +0 -30
- package/dist/commands/taskIntegrationQueueCommands.js +0 -226
- package/dist/commands/taskOverviewCommand.js +0 -377
- package/dist/completion/completionWizard.js +0 -125
- package/dist/context/dispatchContext.js +0 -110
- package/dist/context/wakeNotification.js +0 -144
- package/dist/integration/integrationCheckEvidenceReuse.js +0 -53
- package/dist/integration/integrationQueueEntry.js +0 -191
- package/dist/integration/integrationQueueService.js +0 -810
- package/dist/release/fakeReleasePorts.js +0 -55
- package/dist/runtime/sessionOwnerRegistry.js +0 -137
- package/dist/task/deliveryGuard.js +0 -226
- /package/dist/{commands/taskActor.js → task/taskAuthority.js} +0 -0
|
@@ -131,21 +131,10 @@ export function decideSubmissionRouting(input) {
|
|
|
131
131
|
/**
|
|
132
132
|
* Normalize a possibly-absent submission intent to its effective value.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
* every old client and every message saved before the field existed keeps its
|
|
136
|
-
* historical Leader-waking behaviour. Body text is never inspected.
|
|
137
|
-
*
|
|
138
|
-
* A legacy `--wake-policy none` carried the "save only, do not wake" meaning that
|
|
139
|
-
* `record` now names, so a caller that supplied only that older signal maps to
|
|
140
|
-
* `record` rather than silently becoming a Leader-waking `discuss`. An explicit
|
|
141
|
-
* intent always wins over the wake-policy shorthand.
|
|
134
|
+
* The current default is `discuss`. Body text is never inspected.
|
|
142
135
|
*/
|
|
143
|
-
export function normalizeSubmissionIntent(intent
|
|
144
|
-
|
|
145
|
-
return intent;
|
|
146
|
-
if (wakePolicy === "none")
|
|
147
|
-
return "record";
|
|
148
|
-
return "discuss";
|
|
136
|
+
export function normalizeSubmissionIntent(intent) {
|
|
137
|
+
return intent ?? "discuss";
|
|
149
138
|
}
|
|
150
139
|
/**
|
|
151
140
|
* Whether a retry aims at the same target the key was first bound to (§2.3). An
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Runs on the existing persistence worker's connection, never the ingress
|
|
2
|
+
* thread. Telemetry keeps its existing idempotent keys and aggregate triggers;
|
|
3
|
+
* it neither bumps Task revisions nor creates durable RPC outbox records.
|
|
4
|
+
*/
|
|
5
|
+
export function writeTelemetryBatch(db, entries, runCap) {
|
|
6
|
+
if (!Number.isSafeInteger(runCap) || runCap < 1 || entries.length > 256) {
|
|
7
|
+
throw new Error("Telemetry batch exceeds its bounded write contract.");
|
|
8
|
+
}
|
|
9
|
+
const upsert = db.prepare(`INSERT INTO telemetry (task_id, role_name, turn_id, progress_id, sequence, payload, received_at)
|
|
10
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
11
|
+
ON CONFLICT(task_id, role_name, turn_id, progress_id) DO UPDATE SET
|
|
12
|
+
sequence = excluded.sequence, payload = excluded.payload, received_at = excluded.received_at
|
|
13
|
+
WHERE excluded.received_at >= telemetry.received_at`);
|
|
14
|
+
const cap = db.prepare(`DELETE FROM telemetry WHERE task_id = ? AND turn_id = ?
|
|
15
|
+
AND (task_id, role_name, turn_id, progress_id) NOT IN (
|
|
16
|
+
SELECT task_id, role_name, turn_id, progress_id FROM telemetry
|
|
17
|
+
WHERE task_id = ? AND turn_id = ?
|
|
18
|
+
ORDER BY received_at DESC, COALESCE(sequence, -1) DESC, progress_id ASC LIMIT ?
|
|
19
|
+
)`);
|
|
20
|
+
db.transaction(() => {
|
|
21
|
+
const runs = new Map();
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
upsert.run(entry.taskId, entry.roleName, entry.runId, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
|
|
24
|
+
runs.set(`${entry.taskId}\0${entry.runId}`, entry);
|
|
25
|
+
}
|
|
26
|
+
for (const { taskId, runId } of runs.values())
|
|
27
|
+
cap.run(taskId, runId, taskId, runId, runCap);
|
|
28
|
+
})();
|
|
29
|
+
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
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
4
|
import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
|
|
5
|
+
import { migrateSqliteSchema } from "../storage/sqliteSchema.js";
|
|
6
|
+
import { AsyncTaskStoreClient } from "../storage/storeRpc.js";
|
|
7
|
+
import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP } from "./telemetryConfig.js";
|
|
7
8
|
/**
|
|
8
9
|
* Default sidecar implementation: the telemetry tables live inside the Home's
|
|
9
10
|
* authoritative `yui.db`, so a Home has one durable database
|
|
10
11
|
* has one file to manage, back up, and migrate. The store opens its own
|
|
11
|
-
* connection
|
|
12
|
-
*
|
|
12
|
+
* connection for cold synchronous reads/explicit retention. Ingestion writes
|
|
13
|
+
* use the existing persistence-worker RPC, shared with the Controller when
|
|
14
|
+
* available; standalone consumers lazily own and close their own client.
|
|
13
15
|
*
|
|
14
16
|
* Writes are best-effort and never block the Controller event loop: `observe`
|
|
15
17
|
* only merges into a bounded in-memory queue; a background flush drains it in
|
|
@@ -28,6 +30,10 @@ export class SqliteTelemetryStore {
|
|
|
28
30
|
#terminalKeep;
|
|
29
31
|
#turnCap;
|
|
30
32
|
#maxPending;
|
|
33
|
+
#home;
|
|
34
|
+
#writer;
|
|
35
|
+
#ownedWriter;
|
|
36
|
+
#flushing;
|
|
31
37
|
#db = null;
|
|
32
38
|
#failed = false;
|
|
33
39
|
#lastError = null;
|
|
@@ -39,6 +45,8 @@ export class SqliteTelemetryStore {
|
|
|
39
45
|
#closed = false;
|
|
40
46
|
constructor(home, options = {}) {
|
|
41
47
|
this.mode = options.mode ?? "on";
|
|
48
|
+
this.#home = home;
|
|
49
|
+
this.#writer = options.writer;
|
|
42
50
|
this.#path = join(home, COMMITTED_DATABASE_FILENAME);
|
|
43
51
|
this.#terminalKeep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
|
|
44
52
|
this.#turnCap = options.runCap ?? DEFAULT_RUN_CAP;
|
|
@@ -48,8 +56,7 @@ export class SqliteTelemetryStore {
|
|
|
48
56
|
observe(entry) {
|
|
49
57
|
if (this.#closed)
|
|
50
58
|
return;
|
|
51
|
-
|
|
52
|
-
if (db === null) {
|
|
59
|
+
if (this.#failed) {
|
|
53
60
|
this.#dropped++;
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
@@ -84,9 +91,14 @@ export class SqliteTelemetryStore {
|
|
|
84
91
|
if (this.#closed)
|
|
85
92
|
return;
|
|
86
93
|
this.#closed = true;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
try {
|
|
95
|
+
await this.#flushPending();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
await this.#ownedWriter?.close();
|
|
99
|
+
this.#db?.close();
|
|
100
|
+
this.#db = null;
|
|
101
|
+
}
|
|
90
102
|
}
|
|
91
103
|
// -- TelemetryReader -----------------------------------------------------------
|
|
92
104
|
count(taskId, runId) {
|
|
@@ -230,6 +242,7 @@ export class SqliteTelemetryStore {
|
|
|
230
242
|
return this.#db;
|
|
231
243
|
if (this.#failed || this.#closed)
|
|
232
244
|
return null;
|
|
245
|
+
let opening;
|
|
233
246
|
try {
|
|
234
247
|
// The telemetry tables live in the Home's authoritative database; the
|
|
235
248
|
// store never creates `yui.db` itself. Wiring fails closed on Homes
|
|
@@ -237,17 +250,19 @@ export class SqliteTelemetryStore {
|
|
|
237
250
|
if (!existsSync(this.#path)) {
|
|
238
251
|
throw new Error(`Telemetry database not found: ${this.#path}`);
|
|
239
252
|
}
|
|
240
|
-
const db = new Database(this.#path);
|
|
253
|
+
const db = opening = new Database(this.#path);
|
|
241
254
|
db.pragma("journal_mode = WAL");
|
|
242
255
|
db.pragma("synchronous = FULL");
|
|
243
256
|
db.pragma("foreign_keys = ON");
|
|
244
|
-
|
|
257
|
+
// Cold diagnostic reads/retention must not wait behind a semantic writer.
|
|
258
|
+
db.pragma("busy_timeout = 0");
|
|
245
259
|
db.pragma("wal_autocheckpoint = 1000");
|
|
246
260
|
migrateSqliteSchema(db, { mode: "validate" });
|
|
247
261
|
this.#db = db;
|
|
248
262
|
return db;
|
|
249
263
|
}
|
|
250
264
|
catch (error) {
|
|
265
|
+
opening?.close();
|
|
251
266
|
this.#failed = true;
|
|
252
267
|
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
253
268
|
return null;
|
|
@@ -263,43 +278,28 @@ export class SqliteTelemetryStore {
|
|
|
263
278
|
});
|
|
264
279
|
handle.unref?.();
|
|
265
280
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
upsert.run(entry.taskId, entry.roleName, entry.runId, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
|
|
289
|
-
touchedRuns.set(`${entry.taskId}\0${entry.runId}`, {
|
|
290
|
-
taskId: entry.taskId,
|
|
291
|
-
runId: entry.runId
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
for (const { taskId, runId } of touchedRuns.values()) {
|
|
295
|
-
this.capRun(taskId, runId);
|
|
296
|
-
}
|
|
297
|
-
})();
|
|
298
|
-
this.#applied += batch.size;
|
|
299
|
-
}
|
|
300
|
-
catch (error) {
|
|
301
|
-
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
302
|
-
this.#dropped += batch.size;
|
|
281
|
+
#flushPending() {
|
|
282
|
+
this.#flushing ??= this.#drain().finally(() => { this.#flushing = undefined; });
|
|
283
|
+
return this.#flushing;
|
|
284
|
+
}
|
|
285
|
+
async #drain() {
|
|
286
|
+
while (this.#pending.size > 0) {
|
|
287
|
+
const batch = [];
|
|
288
|
+
for (const [key, entry] of this.#pending) {
|
|
289
|
+
this.#pending.delete(key);
|
|
290
|
+
batch.push(entry);
|
|
291
|
+
if (batch.length === 256)
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const writer = this.#writer ?? (this.#ownedWriter ??= new AsyncTaskStoreClient(this.#home, { readPoolSize: 1 }));
|
|
296
|
+
await writer.flushTelemetry(batch, this.#turnCap);
|
|
297
|
+
this.#applied += batch.length;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
this.#lastError = error instanceof Error ? error.message : String(error);
|
|
301
|
+
this.#dropped += batch.length;
|
|
302
|
+
}
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
|
|
4
3
|
import { resolveTelemetryEnabled } from "../config/yuiConfig.js";
|
|
5
|
-
import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
|
|
6
4
|
import { CURRENT_DATABASE_FILENAME as COMMITTED_DATABASE_FILENAME } from "../storage/currentTaskStore.js";
|
|
5
|
+
import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
|
|
6
|
+
import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
|
|
7
7
|
/**
|
|
8
8
|
* Open optional telemetry from the current Home's authoritative `yui.db`.
|
|
9
9
|
* Disabled telemetry performs no database work. Once opened, telemetry fails
|
|
10
10
|
* isolated: a broken sink increments its dropped counter without blocking the
|
|
11
11
|
* semantic lane.
|
|
12
12
|
*/
|
|
13
|
-
export function openSchedulerTelemetry(home, config) {
|
|
13
|
+
export function openSchedulerTelemetry(home, config, writer) {
|
|
14
14
|
if (!resolveTelemetryEnabled(config.telemetryEnabled))
|
|
15
15
|
return null;
|
|
16
16
|
const mode = "on";
|
|
@@ -21,6 +21,7 @@ export function openSchedulerTelemetry(home, config) {
|
|
|
21
21
|
}
|
|
22
22
|
const store = new SqliteTelemetryStore(home, {
|
|
23
23
|
mode,
|
|
24
|
+
writer,
|
|
24
25
|
terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
|
|
25
26
|
runCap: resolveRunCap(config.telemetryRunCap)
|
|
26
27
|
});
|
package/dist/tmux/tmuxManager.js
CHANGED
|
@@ -509,7 +509,9 @@ export class TmuxManager {
|
|
|
509
509
|
let output;
|
|
510
510
|
try {
|
|
511
511
|
output = this.run([
|
|
512
|
-
|
|
512
|
+
// list-panes takes a pane target: the colon keeps this an exact
|
|
513
|
+
// session lookup instead of falling back to a similarly named one.
|
|
514
|
+
"list-panes", "-s", "-t", `=${this.sessionName(taskId)}:`, "-F",
|
|
513
515
|
`#{window_name}${formatSeparator}#{pane_dead}${formatSeparator}#{pane_dead_status}${formatSeparator}#{pane_pid}${formatSeparator}#{pane_current_command}`
|
|
514
516
|
]);
|
|
515
517
|
}
|
|
@@ -865,7 +867,7 @@ export class TmuxManager {
|
|
|
865
867
|
this.recordTaskTargets(taskId);
|
|
866
868
|
for (const session of [...sessions.filter((name) => name !== taskSession), taskSession]) {
|
|
867
869
|
try {
|
|
868
|
-
this.run(["kill-session", "-t", session]);
|
|
870
|
+
this.run(["kill-session", "-t", `=${session}`]);
|
|
869
871
|
}
|
|
870
872
|
catch (error) {
|
|
871
873
|
if (!isExplicitlyAbsentTmuxSession(error))
|
|
@@ -882,7 +884,7 @@ export class TmuxManager {
|
|
|
882
884
|
await this.recordTaskTargetsAsync(taskId);
|
|
883
885
|
for (const session of [...sessions.filter((name) => name !== taskSession), taskSession]) {
|
|
884
886
|
try {
|
|
885
|
-
await this.runAsync(["kill-session", "-t", session]);
|
|
887
|
+
await this.runAsync(["kill-session", "-t", `=${session}`]);
|
|
886
888
|
}
|
|
887
889
|
catch (error) {
|
|
888
890
|
if (!isExplicitlyAbsentTmuxSession(error))
|
|
@@ -897,11 +899,23 @@ export class TmuxManager {
|
|
|
897
899
|
}
|
|
898
900
|
killRole(taskId, roleName) {
|
|
899
901
|
this.recordRoleTarget(taskId, roleName);
|
|
900
|
-
|
|
902
|
+
try {
|
|
903
|
+
this.run(["kill-window", "-t", this.exactTarget(taskId, roleName)]);
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
if (!isExplicitlyAbsentTmuxSession(error))
|
|
907
|
+
throw error;
|
|
908
|
+
}
|
|
901
909
|
}
|
|
902
910
|
async killRoleAsync(taskId, roleName) {
|
|
903
911
|
this.recordRoleTarget(taskId, roleName);
|
|
904
|
-
|
|
912
|
+
try {
|
|
913
|
+
await this.runAsync(["kill-window", "-t", this.exactTarget(taskId, roleName)]);
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
if (!isExplicitlyAbsentTmuxSession(error))
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
905
919
|
}
|
|
906
920
|
renameRole(taskId, oldRoleName, newRoleName) {
|
|
907
921
|
const target = this.target(taskId, oldRoleName);
|
|
@@ -916,7 +930,7 @@ export class TmuxManager {
|
|
|
916
930
|
}
|
|
917
931
|
hasSession(taskId) {
|
|
918
932
|
try {
|
|
919
|
-
this.run(["has-session", "-t", this.sessionName(taskId)]);
|
|
933
|
+
this.run(["has-session", "-t", `=${this.sessionName(taskId)}`]);
|
|
920
934
|
return true;
|
|
921
935
|
}
|
|
922
936
|
catch (error) {
|
|
@@ -927,7 +941,7 @@ export class TmuxManager {
|
|
|
927
941
|
}
|
|
928
942
|
async hasSessionAsync(taskId) {
|
|
929
943
|
try {
|
|
930
|
-
await this.runAsync(["has-session", "-t", this.sessionName(taskId)]);
|
|
944
|
+
await this.runAsync(["has-session", "-t", `=${this.sessionName(taskId)}`]);
|
|
931
945
|
return true;
|
|
932
946
|
}
|
|
933
947
|
catch (error) {
|
|
@@ -941,7 +955,7 @@ export class TmuxManager {
|
|
|
941
955
|
return [];
|
|
942
956
|
try {
|
|
943
957
|
return this.run([
|
|
944
|
-
"list-windows", "-t", this.sessionName(taskId)
|
|
958
|
+
"list-windows", "-t", `=${this.sessionName(taskId)}`, "-F", "#{window_name}"
|
|
945
959
|
]).split("\n").map((name) => name.trim()).filter(Boolean);
|
|
946
960
|
}
|
|
947
961
|
catch (error) {
|
|
@@ -953,7 +967,7 @@ export class TmuxManager {
|
|
|
953
967
|
async sessionWindowNamesAsync(taskId) {
|
|
954
968
|
try {
|
|
955
969
|
const names = (await this.runAsync([
|
|
956
|
-
"list-windows", "-t", this.sessionName(taskId)
|
|
970
|
+
"list-windows", "-t", `=${this.sessionName(taskId)}`, "-F", "#{window_name}"
|
|
957
971
|
])).split("\n").map((name) => name.trim()).filter(Boolean);
|
|
958
972
|
return { exists: true, names };
|
|
959
973
|
}
|
|
@@ -1141,16 +1155,12 @@ function writableClientSessionPrefix(roleName) {
|
|
|
1141
1155
|
function writableLeaseMatchesRole(sessionName, roleName) {
|
|
1142
1156
|
if (!sessionName.startsWith(WRITABLE_CLIENT_SESSION_PREFIX))
|
|
1143
1157
|
return false;
|
|
1144
|
-
if (
|
|
1145
|
-
return true;
|
|
1146
|
-
if (sessionName.startsWith(HOST_WRITABLE_CLIENT_SESSION_PREFIX))
|
|
1158
|
+
if (/^yui-writer-host-[a-f0-9]{24}$/u.test(sessionName))
|
|
1147
1159
|
return true;
|
|
1148
|
-
if (
|
|
1149
|
-
return sessionName.startsWith(writableClientSessionPrefix(roleName));
|
|
1160
|
+
if (/^yui-writer-role-[a-f0-9]{24}-[a-f0-9]{24}$/u.test(sessionName)) {
|
|
1161
|
+
return roleName === undefined || sessionName.startsWith(writableClientSessionPrefix(roleName));
|
|
1150
1162
|
}
|
|
1151
|
-
|
|
1152
|
-
// lease names existed.
|
|
1153
|
-
return true;
|
|
1163
|
+
throw runtimeError(`Unverified tmux writer lease: ${sessionName}. Preserve it and inspect its owner before retrying.`);
|
|
1154
1164
|
}
|
|
1155
1165
|
function writableClientRowsContainMatch(clients, taskSession, roleName, excludedLease, formatSeparator, encodedSeparator) {
|
|
1156
1166
|
return clients.split("\n").some((line) => {
|
|
@@ -1169,9 +1179,8 @@ function writableClientRowsContainMatch(clients, taskSession, roleName, excluded
|
|
|
1169
1179
|
|| readOnly !== "0") {
|
|
1170
1180
|
return false;
|
|
1171
1181
|
}
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
1174
|
-
// Role in that host.
|
|
1182
|
+
// A direct writable tmux client has no Role identity and fences the host.
|
|
1183
|
+
// A Yui lease must instead prove its exact current scope.
|
|
1175
1184
|
return !sessionName.startsWith(WRITABLE_CLIENT_SESSION_PREFIX)
|
|
1176
1185
|
|| writableLeaseMatchesRole(sessionName, roleName);
|
|
1177
1186
|
});
|
|
@@ -4,10 +4,11 @@ import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp
|
|
|
4
4
|
/**
|
|
5
5
|
* Issue 08: the reusable exact-SHA gate evidence record.
|
|
6
6
|
*
|
|
7
|
-
* A GateArtifact
|
|
7
|
+
* A GateArtifact is the latest cached proof for its full identity tuple: Project,
|
|
8
8
|
* candidate commit, VerificationPlan digest, toolchain digest, and (for L2)
|
|
9
|
-
* the source/base/target boundary.
|
|
10
|
-
*
|
|
9
|
+
* the source/base/target boundary. Fresh execution withdraws the previous
|
|
10
|
+
* success; a failed or incomplete latest result is not reusable. Job and
|
|
11
|
+
* Integration records own execution history. Artifacts are stored in the
|
|
11
12
|
* SQLite `gate_artifacts` / `gate_artifact_logs` tables, backend-neutral
|
|
12
13
|
* through the GateArtifactStorePort.
|
|
13
14
|
*/
|
|
@@ -40,12 +41,11 @@ export function createGateArtifact(identity, metadata, now) {
|
|
|
40
41
|
planVersion: requireText(metadata.planVersion, "GateArtifact planVersion"),
|
|
41
42
|
planDigest: identity.planDigest,
|
|
42
43
|
toolchainDigest: identity.toolchainDigest,
|
|
43
|
-
|
|
44
|
+
boundary: identity.boundary,
|
|
44
45
|
steps: Object.freeze([]),
|
|
45
46
|
generator: requireText(metadata.generator, "GateArtifact generator"),
|
|
46
47
|
status: "incomplete",
|
|
47
48
|
outcome: "unknown",
|
|
48
|
-
potentialReuseCount: 0,
|
|
49
49
|
reuseCount: 0,
|
|
50
50
|
createdAt: timestamp,
|
|
51
51
|
lastUsedAt: timestamp
|
|
@@ -67,16 +67,7 @@ export function completeGateArtifact(artifact, steps, outcome, now) {
|
|
|
67
67
|
lastUsedAt: timestamp
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
/** Record
|
|
71
|
-
export function recordGateArtifactPotentialReuse(artifact, now) {
|
|
72
|
-
validateGateArtifact(artifact);
|
|
73
|
-
return validateGateArtifact({
|
|
74
|
-
...artifact,
|
|
75
|
-
potentialReuseCount: artifact.potentialReuseCount + 1,
|
|
76
|
-
lastUsedAt: now.toISOString()
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
/** Record an actual reuse (reuse/enforce mode). */
|
|
70
|
+
/** Record an actual reuse of complete successful evidence. */
|
|
80
71
|
export function recordGateArtifactReuse(artifact, now) {
|
|
81
72
|
validateGateArtifact(artifact);
|
|
82
73
|
if (artifact.status !== "complete" || artifact.outcome !== "succeeded") {
|
|
@@ -93,7 +84,7 @@ export function validateGateArtifact(artifact) {
|
|
|
93
84
|
throw new Error(`GateArtifact must use schemaVersion ${GATE_ARTIFACT_SCHEMA_VERSION}.`);
|
|
94
85
|
}
|
|
95
86
|
requireIdentity(artifact.projectId, "GateArtifact projectId");
|
|
96
|
-
if (artifact.level !== "
|
|
87
|
+
if (artifact.level !== "L2") {
|
|
97
88
|
throw new Error(`GateArtifact level is invalid: ${String(artifact.level)}.`);
|
|
98
89
|
}
|
|
99
90
|
requireCommit(artifact.commit, "GateArtifact commit");
|
|
@@ -106,17 +97,15 @@ export function validateGateArtifact(artifact) {
|
|
|
106
97
|
commit: artifact.commit,
|
|
107
98
|
planDigest: artifact.planDigest,
|
|
108
99
|
toolchainDigest: artifact.toolchainDigest,
|
|
109
|
-
|
|
100
|
+
boundary: artifact.boundary
|
|
110
101
|
})) {
|
|
111
102
|
throw new Error("GateArtifact key does not match its identity tuple.");
|
|
112
103
|
}
|
|
113
|
-
if (artifact.
|
|
104
|
+
if (artifact.boundary === undefined) {
|
|
114
105
|
throw new Error("An L2 GateArtifact requires a target boundary.");
|
|
115
106
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
|
|
119
|
-
}
|
|
107
|
+
requireText(artifact.boundary.targetRef, "GateArtifact boundary targetRef");
|
|
108
|
+
requireCommit(artifact.boundary.baseHead, "GateArtifact boundary baseHead");
|
|
120
109
|
if (!["incomplete", "complete"].includes(artifact.status)) {
|
|
121
110
|
throw new Error(`GateArtifact status is invalid: ${String(artifact.status)}.`);
|
|
122
111
|
}
|
|
@@ -150,7 +139,8 @@ export function validateGateArtifact(artifact) {
|
|
|
150
139
|
throw new Error(`GateArtifact step ${step.name} logBytes is invalid.`);
|
|
151
140
|
}
|
|
152
141
|
}
|
|
153
|
-
|
|
142
|
+
if (Object.hasOwn(artifact, "potentialReuseCount"))
|
|
143
|
+
throw new Error("GateArtifact contains retired shadow metrics.");
|
|
154
144
|
requirePositiveInteger(artifact.reuseCount + 1, "GateArtifact reuseCount");
|
|
155
145
|
requireTimestamp(artifact.createdAt, "GateArtifact createdAt");
|
|
156
146
|
requireTimestamp(artifact.lastUsedAt, "GateArtifact lastUsedAt");
|
|
@@ -185,7 +175,8 @@ export function verifyGateArtifactLogs(artifact, logs) {
|
|
|
185
175
|
});
|
|
186
176
|
}
|
|
187
177
|
export function isReusableGateArtifact(artifact) {
|
|
188
|
-
return artifact.status === "complete" && artifact.outcome === "succeeded"
|
|
178
|
+
return artifact.status === "complete" && artifact.outcome === "succeeded"
|
|
179
|
+
&& artifact.steps.length > 0 && artifact.steps.every(step => step.outcome === "passed" && step.exitCode === 0 && step.signal === null && !step.timedOut);
|
|
189
180
|
}
|
|
190
181
|
function requireCommit(value, label) {
|
|
191
182
|
const normalized = requireText(value, label).toLowerCase();
|
|
@@ -30,19 +30,26 @@ export function findGateArtifact(store, identity) {
|
|
|
30
30
|
* commit, plan digest, toolchain digest, and target ref. Unlike
|
|
31
31
|
* {@link findGateArtifact} the base head is not part of the match: a release
|
|
32
32
|
* consumes the gate that proved the exact frozen tree, regardless of which
|
|
33
|
-
* base it integrated onto.
|
|
33
|
+
* base it integrated onto. The newest recorded matching result governs:
|
|
34
|
+
* failure/incompleteness/corruption must not fall through to an older success.
|
|
35
|
+
* Logs are verified before returning.
|
|
34
36
|
*/
|
|
35
37
|
export async function findL2ArtifactForCommit(store, query) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
+
const recordedAt = (artifact) => Date.parse(artifact.completedAt ?? artifact.createdAt);
|
|
39
|
+
const artifacts = [...store.findL2GateArtifactsForCommit(query)]
|
|
40
|
+
.sort((left, right) => recordedAt(right) - recordedAt(left));
|
|
41
|
+
const latest = artifacts[0];
|
|
42
|
+
if (latest === undefined)
|
|
43
|
+
return null;
|
|
44
|
+
for (const artifact of artifacts.filter(value => recordedAt(value) === recordedAt(latest))) {
|
|
38
45
|
if (!isReusableGateArtifact(artifact))
|
|
39
|
-
|
|
46
|
+
return null;
|
|
40
47
|
const logs = store.getGateArtifactLogs(artifact.key);
|
|
41
48
|
const verification = verifyGateArtifactLogs(artifact, logs);
|
|
42
|
-
if (verification.ok)
|
|
43
|
-
return
|
|
49
|
+
if (!verification.ok)
|
|
50
|
+
return null;
|
|
44
51
|
}
|
|
45
|
-
return
|
|
52
|
+
return latest;
|
|
46
53
|
}
|
|
47
54
|
/**
|
|
48
55
|
* Read each step's source log, hash it, and return the completed step
|