@zq-silk/yui 0.7.1 → 0.8.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.
Files changed (73) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +79 -71
  3. package/dist/cli/commandCatalog.js +283 -136
  4. package/dist/cli/completion.js +3 -3
  5. package/dist/cli/helpRenderer.js +3 -0
  6. package/dist/cli/interactionPolicy.js +48 -33
  7. package/dist/cli/interactiveSelection.js +1 -1
  8. package/dist/cli/invocationRouter.js +3 -2
  9. package/dist/cli/roleWizard.js +8 -8
  10. package/dist/cli.js +189 -93
  11. package/dist/commands/agentCommands.js +5 -5
  12. package/dist/commands/configCommands.js +351 -104
  13. package/dist/commands/configOverview.js +60 -0
  14. package/dist/commands/deliveryGuardPreflight.js +2 -2
  15. package/dist/commands/globalRoleCommands.js +9 -9
  16. package/dist/commands/profileCommands.js +8 -8
  17. package/dist/commands/resourcesCommands.js +6 -5
  18. package/dist/commands/taskCommands.js +111 -59
  19. package/dist/commands/taskRoleRuntimeStatus.js +3 -1
  20. package/dist/commands/telemetryCommands.js +11 -6
  21. package/dist/config/configCatalog.js +42 -0
  22. package/dist/config/yuiConfig.js +80 -35
  23. package/dist/context/sessionBootstrapManifest.js +1 -1
  24. package/dist/controller/clientRuntime.js +0 -2
  25. package/dist/controller/controller.js +21 -9
  26. package/dist/controller/fileSchedulerStoreAdapter.js +409 -79
  27. package/dist/controller/resourceInventory.js +9 -5
  28. package/dist/controller/runtime.js +112 -25
  29. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  30. package/dist/controller/structuredProviderObservation.js +273 -0
  31. package/dist/doctor/doctor.js +2 -2
  32. package/dist/executor/agentAdapter.js +40 -0
  33. package/dist/executor/agentExecutor.js +31 -7
  34. package/dist/executor/executorRegistry.js +11 -49
  35. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  36. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  37. package/dist/resources/autoResourceGc.js +3 -1
  38. package/dist/review/reviewConfig.js +0 -2
  39. package/dist/run/agentRun.js +2 -2
  40. package/dist/run/providerRetry.js +29 -16
  41. package/dist/run/providerRetryConfig.js +5 -3
  42. package/dist/runtime/agentHost.js +767 -158
  43. package/dist/runtime/builtinAgentDrivers.js +1 -5
  44. package/dist/runtime/codexAppServerRuntime.js +67 -60
  45. package/dist/runtime/exactControlPlane.js +7 -2
  46. package/dist/runtime/index.js +6 -2
  47. package/dist/runtime/launchBroker.js +30 -8
  48. package/dist/runtime/launchDiagnostics.js +1 -1
  49. package/dist/runtime/providerAuthorityFence.js +24 -0
  50. package/dist/runtime/providerControl.js +63 -0
  51. package/dist/runtime/providerRecoveryDecision.js +55 -0
  52. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  53. package/dist/runtime/runtimeBinding.js +20 -11
  54. package/dist/runtime/structuredProviderHost.js +476 -0
  55. package/dist/runtime/tmuxAdapters.js +143 -42
  56. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  57. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  58. package/dist/scheduler/roleRunStall.js +12 -9
  59. package/dist/setup/setupCommand.js +153 -492
  60. package/dist/storage/compatibleTaskStore.js +9 -5
  61. package/dist/storage/migration/productionRegistry.js +169 -0
  62. package/dist/storage/taskStore.js +22 -3
  63. package/dist/telemetry/sqliteTelemetryStore.js +9 -1
  64. package/dist/telemetry/telemetryConfig.js +1 -18
  65. package/dist/telemetry/telemetryStore.js +2 -2
  66. package/dist/telemetry/telemetryWiring.js +6 -5
  67. package/dist/tmux/tmuxManager.js +1 -1
  68. package/dist/web/webSnapshot.js +5 -3
  69. package/i18n/README.zh-CN.md +48 -40
  70. package/package.json +1 -1
  71. package/skills/yui-leader/SKILL.md +12 -5
  72. package/skills/yui-operator/SKILL.md +44 -6
  73. package/skills/yui-runtime/SKILL.md +1 -1
@@ -6,19 +6,23 @@ import { FileTaskStore, STORAGE_STATE_FILE, stateFileFingerprint, StorageRecordE
6
6
  import { readSqliteHomeIdentity, SqliteTaskStore } from "./sqliteStore.js";
7
7
  import { COMMITTED_DATABASE_FILENAME } from "./upgrade/sqliteStateMigration.js";
8
8
  import { validateHomeIdentity } from "../repository/homeIdentity.js";
9
- import { ensureStorageSchema, inspectStorageSchema, readStorageSchemaManifest, STORAGE_SCHEMA_FILE } from "./storageSchema.js";
9
+ import { CURRENT_STORAGE_LAYOUT_VERSION, ensureStorageSchema, inspectStorageSchema, readStorageSchemaManifest, STORAGE_SCHEMA_FILE } from "./storageSchema.js";
10
10
  import { classifyHome } from "./upgrade/homeClassification.js";
11
11
  import { inspectSnapshotVersionState } from "./upgrade/homeMigrationTarget.js";
12
12
  import { latestStorageVersionState } from "./upgrade/recordVersions.js";
13
13
  export { createProductionStorageRegistry } from "./migration/productionRegistry.js";
14
14
  /**
15
- * Initialize a brand-new Home, or open an existing Home through the same
16
- * compatibility classification as every ordinary command. Setup is the one
17
- * ordinary flow that is also responsible for creating the initial manifest.
15
+ * Initialize a brand-new Home with the current authoritative backend, or open
16
+ * an existing Home through the same compatibility classification as every
17
+ * ordinary command. Setup is the one ordinary flow that is also responsible
18
+ * for creating the initial manifest.
18
19
  */
19
- export function initializeCompatibleFileTaskStore(home, options = {}) {
20
+ export function initializeCompatibleTaskStore(home, options = {}) {
20
21
  if (inspectStorageSchema(home).status === "uninitialized") {
21
22
  ensureStorageSchema(home);
23
+ if (CURRENT_STORAGE_LAYOUT_VERSION >= 7) {
24
+ return new SqliteTaskStore(home);
25
+ }
22
26
  }
23
27
  return openCompatibleFileTaskStore(home, options);
24
28
  }
@@ -79,8 +79,12 @@ const WORK_MAILBOX_FROM_VERSION = 1;
79
79
  const WORK_MAILBOX_TO_VERSION = 2;
80
80
  const TASK_ROLE_SESSION_SET_FROM_VERSION = 4;
81
81
  const TASK_ROLE_SESSION_SET_TO_VERSION = 5;
82
+ const STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION = 5;
83
+ const STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION = 6;
82
84
  const PUBLICATION_REFERENCE_FROM_VERSION = 0;
83
85
  const PUBLICATION_REFERENCE_TO_VERSION = 1;
86
+ const CONFIG_FROM_VERSION = 1;
87
+ const CONFIG_TO_VERSION = 2;
84
88
  /**
85
89
  * Build the authoritative production graph. Transition intent and executable
86
90
  * transforms are registered together here; compatible loading and offline
@@ -129,6 +133,7 @@ export function createProductionStorageRegistry() {
129
133
  declaredEffects: []
130
134
  })
131
135
  .registerOfflineMigration(projectOwnershipStep())
136
+ .registerOfflineMigration(configV2Step())
132
137
  .registerCompatible(projectKnowledgeProposalsStep())
133
138
  .registerOfflineMigration(taskWorkspaceIdentityStep())
134
139
  .registerOfflineMigration(recordFamilyStep("workItem", WORK_ITEM_FROM_VERSION, WORK_ITEM_TO_VERSION, "workItems"))
@@ -158,10 +163,66 @@ export function createProductionStorageRegistry() {
158
163
  .registerOfflineMigration(releaseWorkflowIntroductionStep())
159
164
  .registerOfflineMigration(workMailboxV2Step())
160
165
  .registerOfflineMigration(taskRoleSessionSetV5Step())
166
+ .registerOfflineMigration(structuredProviderSessionSetV6Step())
161
167
  .registerOfflineMigration(publicationReferenceIntroductionStep());
162
168
  assertRegistryCoversBaselineToCurrent(registry);
163
169
  return registry;
164
170
  }
171
+ function configV2Step() {
172
+ return {
173
+ axis: "record",
174
+ recordKind: "config",
175
+ fromVersion: CONFIG_FROM_VERSION,
176
+ toVersion: CONFIG_TO_VERSION,
177
+ preconditions: requireConfigV1,
178
+ transform: migrateConfigV1ToV2,
179
+ declaredEffects: []
180
+ };
181
+ }
182
+ function requireConfigV1(snapshot) {
183
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
184
+ if (versions.config !== CONFIG_FROM_VERSION) {
185
+ throw new Error(`Record config migration requires manifest version ${CONFIG_FROM_VERSION}.`);
186
+ }
187
+ if (snapshot.state === null)
188
+ return;
189
+ const config = asObject(snapshot.state.config, "Yui config");
190
+ if (config.schemaVersion !== CONFIG_FROM_VERSION) {
191
+ throw new Error(`Yui config must use schemaVersion ${CONFIG_FROM_VERSION} before migration.`);
192
+ }
193
+ }
194
+ function migrateConfigV1ToV2(snapshot) {
195
+ requireConfigV1(snapshot);
196
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
197
+ const schemaManifest = {
198
+ ...snapshot.schemaManifest,
199
+ recordVersions: { ...versions, config: CONFIG_TO_VERSION }
200
+ };
201
+ if (snapshot.state === null)
202
+ return { schemaManifest, state: null };
203
+ const config = asObject(snapshot.state.config, "Yui config");
204
+ const { providerRetryMaxWindowMs, yieldReceiptReplay: _yieldReceiptReplay, gitBin: _gitBin, telemetryMode, schemaVersion: _schemaVersion, ...retained } = config;
205
+ const providerRetryMaxWindowSeconds = typeof providerRetryMaxWindowMs === "number"
206
+ ? Math.ceil(providerRetryMaxWindowMs / 1_000)
207
+ : undefined;
208
+ const telemetryEnabled = telemetryMode === undefined
209
+ ? undefined
210
+ : telemetryMode === "dual" || telemetryMode === "bounded";
211
+ return {
212
+ schemaManifest,
213
+ state: {
214
+ ...snapshot.state,
215
+ config: {
216
+ ...retained,
217
+ schemaVersion: CONFIG_TO_VERSION,
218
+ ...(providerRetryMaxWindowSeconds === undefined
219
+ ? {}
220
+ : { providerRetryMaxWindowSeconds }),
221
+ ...(telemetryEnabled === undefined ? {} : { telemetryEnabled })
222
+ }
223
+ }
224
+ };
225
+ }
165
226
  function workItemExecutionGroupHistoryStep() {
166
227
  return {
167
228
  axis: "record",
@@ -2167,6 +2228,114 @@ function taskRoleSessionSetV5Step() {
2167
2228
  declaredEffects: []
2168
2229
  };
2169
2230
  }
2231
+ /**
2232
+ * v6 is a deliberate runtime cutover, not an emulation layer. Existing Task,
2233
+ * Run, Conversation, and Session identities remain as audit evidence, while
2234
+ * every pre-v6 managed process is terminalized locally. A new Agent Host must
2235
+ * establish structured Provider evidence before any further write.
2236
+ */
2237
+ function structuredProviderSessionSetV6Step() {
2238
+ return {
2239
+ axis: "record",
2240
+ recordKind: "taskRoleSessionSet",
2241
+ fromVersion: STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION,
2242
+ toVersion: STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION,
2243
+ preconditions: requireTaskRoleSessionSetV5Family,
2244
+ transform: migrateTaskRoleSessionSetV5ToV6,
2245
+ declaredEffects: []
2246
+ };
2247
+ }
2248
+ function requireTaskRoleSessionSetV5Family(snapshot) {
2249
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2250
+ if (versions.taskRoleSessionSet !== STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION) {
2251
+ throw new Error(`Record taskRoleSessionSet migration requires manifest version ${STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION}.`);
2252
+ }
2253
+ if (snapshot.state === null)
2254
+ return;
2255
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
2256
+ for (const [taskId, rawAggregate] of Object.entries(tasks)) {
2257
+ const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
2258
+ const sets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
2259
+ for (const [roleName, rawSet] of Object.entries(sets)) {
2260
+ const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
2261
+ if (set.schemaVersion !== STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION) {
2262
+ throw new Error(`Task Role session set ${taskId}/${roleName} must use schemaVersion ${STRUCTURED_PROVIDER_SESSION_SET_FROM_VERSION}.`);
2263
+ }
2264
+ }
2265
+ }
2266
+ }
2267
+ function migrateTaskRoleSessionSetV5ToV6(snapshot) {
2268
+ requireTaskRoleSessionSetV5Family(snapshot);
2269
+ const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2270
+ const schemaManifest = {
2271
+ ...snapshot.schemaManifest,
2272
+ recordVersions: {
2273
+ ...versions,
2274
+ taskRoleSessionSet: STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION
2275
+ }
2276
+ };
2277
+ if (snapshot.state === null)
2278
+ return { schemaManifest, state: null };
2279
+ const tasks = asObject(snapshot.state.tasks, "state tasks");
2280
+ const nextTasks = {};
2281
+ for (const [taskId, rawAggregate] of Object.entries(tasks)) {
2282
+ const aggregate = asObject(rawAggregate, `Task aggregate ${taskId}`);
2283
+ const rawSets = asObject(aggregate.roleSessionSets, `Task Role session sets ${taskId}`);
2284
+ const nextSets = {};
2285
+ for (const [roleName, rawSet] of Object.entries(rawSets)) {
2286
+ const set = asObject(rawSet, `Task Role session set ${taskId}/${roleName}`);
2287
+ const invalidatedAt = String(set.updatedAt);
2288
+ const sessions = asObject(set.sessions, `Task Role sessions ${taskId}/${roleName}`);
2289
+ const nextSessions = Object.fromEntries(Object.entries(sessions).map(([agentId, rawSession]) => {
2290
+ const session = asObject(rawSession, `Task Role Session ${taskId}/${roleName}/${agentId}`);
2291
+ return [agentId, session.status === "stopped" || session.status === "broken"
2292
+ ? session
2293
+ : { ...session, status: "broken", updatedAt: invalidatedAt }];
2294
+ }));
2295
+ nextSets[roleName] = {
2296
+ ...set,
2297
+ schemaVersion: STRUCTURED_PROVIDER_SESSION_SET_TO_VERSION,
2298
+ sessions: nextSessions,
2299
+ providerBinding: set.providerBinding === null
2300
+ ? null
2301
+ : invalidateLegacyProviderBinding(asObject(set.providerBinding, `Provider Binding ${taskId}/${roleName}`), invalidatedAt)
2302
+ };
2303
+ }
2304
+ nextTasks[taskId] = { ...aggregate, roleSessionSets: nextSets };
2305
+ }
2306
+ return { schemaManifest, state: { ...snapshot.state, tasks: nextTasks } };
2307
+ }
2308
+ function invalidateLegacyProviderBinding(binding, invalidatedAt) {
2309
+ if (binding.schemaVersion !== 1) {
2310
+ throw new Error("Pre-v6 Provider Runtime Binding must use schemaVersion 1.");
2311
+ }
2312
+ const activations = Array.isArray(binding.activations) ? binding.activations : [];
2313
+ return {
2314
+ ...binding,
2315
+ schemaVersion: 2,
2316
+ conversations: (Array.isArray(binding.conversations) ? binding.conversations : []).map((rawConversation) => {
2317
+ const conversation = asObject(rawConversation, "Provider Conversation");
2318
+ return conversation.status === "current"
2319
+ ? { ...conversation, recoverability: "unknown" }
2320
+ : conversation;
2321
+ }),
2322
+ activations: activations.map((rawActivation) => {
2323
+ const activation = asObject(rawActivation, "Provider Activation");
2324
+ const { writerLease: _removed, ...withoutLease } = activation;
2325
+ void _removed;
2326
+ return activation.status === "active"
2327
+ ? {
2328
+ ...withoutLease,
2329
+ status: "failed",
2330
+ endedAt: invalidatedAt,
2331
+ terminalReason: "legacy-managed-runtime-invalidated"
2332
+ }
2333
+ : withoutLease;
2334
+ }),
2335
+ authority: { epoch: 1, owner: "none", changedAt: invalidatedAt },
2336
+ turn: null
2337
+ };
2338
+ }
2170
2339
  function requireTaskRoleSessionSetV4Family(snapshot) {
2171
2340
  const versions = asObject(snapshot.schemaManifest.recordVersions, "schema manifest recordVersions");
2172
2341
  if (versions.taskRoleSessionSet !== TASK_ROLE_SESSION_SET_FROM_VERSION) {
@@ -6,7 +6,7 @@ import { validateConfiguredAgent } from "../agent/agent.js";
6
6
  import { validateCapabilityGrant } from "../grant/capabilityGrant.js";
7
7
  import { validateReleaseWorkflow } from "../release/releaseWorkflow.js";
8
8
  import { publicationExternalKey, validatePublicationReference } from "../task/publicationReference.js";
9
- import { reconciliationIntervalMilliseconds, resolveLeaderNextActionMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode } from "../config/yuiConfig.js";
9
+ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveContextBudget, resolveDeliveryTimeoutSeconds, resolveLeaderNextActionMode, resolveLeaderSemanticBudgetTurns, resolveProviderRetryAdapters, resolveProviderRetryDelaysSeconds, resolveProviderRetryMaxWindowSeconds, resolveProviderRetryMode, resolveResourcesGcAutoQuarantine, resolveResourcesGcMode, resolveResourcesQuarantineTtlHours, resolveRuntimeHealth, resolveTelemetryEnabled, resolveTelemetryRunCap, resolveTelemetryTerminalKeep, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
10
10
  import { resolveTimeZone } from "../output/timePresentation.js";
11
11
  import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
12
12
  import { validateContextSnapshot } from "../context/contextSnapshot.js";
@@ -44,7 +44,7 @@ import { CURRENT_AGGREGATE_SCHEMA_VERSION, requireCompatibleStorageSchema, requi
44
44
  export const STORAGE_STATE_FILE = "state.json";
45
45
  /** The root StorageState schema is the persisted aggregate document version. */
46
46
  export const CURRENT_STORAGE_STATE_SCHEMA_VERSION = CURRENT_AGGREGATE_SCHEMA_VERSION;
47
- export const CURRENT_CONFIG_SCHEMA_VERSION = 1;
47
+ export const CURRENT_CONFIG_SCHEMA_VERSION = 2;
48
48
  export const CURRENT_HOME_IDENTITY_SCHEMA_VERSION = 1;
49
49
  export const CURRENT_ACTIVE_RUN_POINTER_SCHEMA_VERSION = 3;
50
50
  /**
@@ -117,7 +117,7 @@ export const CURRENT_STORED_TASK_SCHEMA_VERSION = 17;
117
117
  * Keep these named at the storage boundary so the upgrade record-axis map can
118
118
  * assert it is classifying the same bytes the store reads and writes.
119
119
  */
120
- export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 5;
120
+ export const CURRENT_TASK_ROLE_SESSION_SET_SCHEMA_VERSION = 6;
121
121
  /**
122
122
  * v7 combines optional Issue 04 retry/receipt fields and Issue 05 Leader
123
123
  * actionability fields. All are optional, so the v6→v7 migration is a
@@ -2449,13 +2449,32 @@ function observeTaskRecordId(aggregate, kind, id) {
2449
2449
  }
2450
2450
  export function validateYuiConfig(config) {
2451
2451
  try {
2452
+ if (config.schemaVersion !== CURRENT_CONFIG_SCHEMA_VERSION) {
2453
+ throw new TypeError(`Yui config must use schemaVersion ${CURRENT_CONFIG_SCHEMA_VERSION}.`);
2454
+ }
2452
2455
  reconciliationIntervalMilliseconds(config.reconciliationIntervalSeconds);
2453
2456
  resolveTimeZone(config.timeZone);
2454
2457
  if (config.review !== undefined)
2455
2458
  validateReviewConfig(config.review);
2456
2459
  resolveLeaderNextActionMode(config.leaderNextActionMode);
2460
+ resolveContextBudget(config.contextBudget);
2457
2461
  resolveResourcesGcMode(config.resourcesGcMode);
2458
2462
  resolveResourcesGcAutoQuarantine(config.resourcesGcAutoQuarantine);
2463
+ resolveResourcesQuarantineTtlHours(config.resourcesQuarantineTtlHours);
2464
+ resolveProviderRetryMode(config.providerRetryMode);
2465
+ resolveProviderRetryAdapters(config.providerRetryAdapters);
2466
+ resolveProviderRetryDelaysSeconds(config.providerRetryDelaysSeconds);
2467
+ resolveProviderRetryMaxWindowSeconds(config.providerRetryMaxWindowSeconds);
2468
+ resolveRuntimeHealth(config.runtimeHealth);
2469
+ resolveControllerTaskConcurrency(config.controllerTaskConcurrency);
2470
+ resolveAgentLaunchInactivityTimeoutSeconds(config.agentLaunchInactivityTimeoutSeconds);
2471
+ resolveDeliveryTimeoutSeconds(config.deliveryTimeoutSeconds);
2472
+ resolveLeaderSemanticBudgetTurns(config.leaderSemanticBudgetTurns);
2473
+ resolveTmuxBin(config.tmuxBin);
2474
+ resolveTmuxHistoryLimit(config.tmuxHistoryLimit);
2475
+ resolveTelemetryEnabled(config.telemetryEnabled);
2476
+ resolveTelemetryTerminalKeep(config.telemetryTerminalKeep);
2477
+ resolveTelemetryRunCap(config.telemetryRunCap);
2459
2478
  }
2460
2479
  catch (error) {
2461
2480
  throw new StorageRecordError(error instanceof Error ? error.message : "Yui reconciliation interval is invalid.");
@@ -38,7 +38,7 @@ export class SqliteTelemetryStore {
38
38
  #flushScheduled = false;
39
39
  #closed = false;
40
40
  constructor(home, options = {}) {
41
- this.mode = options.mode ?? "dual";
41
+ this.mode = options.mode ?? "on";
42
42
  this.#path = join(home, COMMITTED_DATABASE_FILENAME);
43
43
  this.#terminalKeep = options.terminalKeep ?? DEFAULT_TERMINAL_KEEP;
44
44
  this.#runCap = options.runCap ?? DEFAULT_RUN_CAP;
@@ -286,9 +286,17 @@ export class SqliteTelemetryStore {
286
286
  WHERE COALESCE(excluded.sequence, -1) > COALESCE(telemetry.sequence, -1)
287
287
  OR (excluded.sequence IS telemetry.sequence AND excluded.received_at >= telemetry.received_at)`);
288
288
  try {
289
+ const touchedRuns = new Map();
289
290
  db.transaction(() => {
290
291
  for (const entry of batch.values()) {
291
292
  upsert.run(entry.taskId, entry.roleName, entry.runId, entry.generation, entry.progressId, entry.sequence ?? null, JSON.stringify(entry.payload), entry.receivedAt);
293
+ touchedRuns.set(`${entry.taskId}\0${entry.runId}`, {
294
+ taskId: entry.taskId,
295
+ runId: entry.runId
296
+ });
297
+ }
298
+ for (const { taskId, runId } of touchedRuns.values()) {
299
+ this.capRun(taskId, runId);
292
300
  }
293
301
  })();
294
302
  this.#applied += batch.size;
@@ -7,7 +7,7 @@
7
7
  * the schema's own constants (§4.4); the environment only overrides them.
8
8
  */
9
9
  import { TELEMETRY_KEEP_PER_GENERATION, TELEMETRY_RUN_CAP } from "../storage/sqliteSchema.js";
10
- export const DEFAULT_TELEMETRY_MODE = "legacy";
10
+ export const DEFAULT_TELEMETRY_MODE = "off";
11
11
  /** Terminal Run/generation progress rows retained after prune. */
12
12
  export const DEFAULT_TERMINAL_KEEP = TELEMETRY_KEEP_PER_GENERATION;
13
13
  /** Hard cap of progress rows per Run while it is still active. */
@@ -18,23 +18,6 @@ export const DEFAULT_RUN_CAP = TELEMETRY_RUN_CAP;
18
18
  * rows (Tasks × Runs × cap).
19
19
  */
20
20
  export const MAX_RUN_CAP = 10_000_000;
21
- const TELEMETRY_MODES = ["legacy", "dual", "bounded"];
22
- /**
23
- * Resolve the telemetry mode from the durable config value (default `legacy`).
24
- * Only the three exact values (case-insensitive) are accepted; anything else
25
- * fails closed at startup instead of silently changing diagnostic retention.
26
- */
27
- export function resolveTelemetryMode(value) {
28
- if (typeof value !== "string")
29
- return DEFAULT_TELEMETRY_MODE;
30
- const raw = value.trim().toLowerCase();
31
- if (raw === undefined || raw === "")
32
- return DEFAULT_TELEMETRY_MODE;
33
- if (!TELEMETRY_MODES.includes(raw)) {
34
- throw new TypeError(`telemetryMode must be one of ${TELEMETRY_MODES.join(", ")}; got ${JSON.stringify(raw)}.`);
35
- }
36
- return raw;
37
- }
38
21
  /**
39
22
  * Resolve the terminal-Run retention window from the durable config value
40
23
  * (default 200). Must be a positive integer.
@@ -1,7 +1,7 @@
1
- /** No-op sink for `legacy` mode and for callers without a sidecar. */
1
+ /** No-op sink for disabled telemetry and callers without a sidecar. */
2
2
  export class NullTelemetrySink {
3
3
  mode;
4
- constructor(mode = "legacy") {
4
+ constructor(mode = "off") {
5
5
  this.mode = mode;
6
6
  }
7
7
  observe(_entry) { }
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { resolveRunCap, resolveTelemetryMode, resolveTerminalKeep } from "./telemetryConfig.js";
3
+ import { resolveRunCap, resolveTerminalKeep } from "./telemetryConfig.js";
4
+ import { resolveTelemetryEnabled } from "../config/yuiConfig.js";
4
5
  import { SqliteTelemetryStore } from "./sqliteTelemetryStore.js";
5
6
  import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigration.js";
6
7
  /**
@@ -16,12 +17,12 @@ import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigra
16
17
  * and never blocks the semantic lane.
17
18
  */
18
19
  export function openSchedulerTelemetry(home, config) {
19
- const mode = resolveTelemetryMode(config.telemetryMode);
20
- if (mode === "legacy")
20
+ if (!resolveTelemetryEnabled(config.telemetryEnabled))
21
21
  return null;
22
+ const mode = "on";
22
23
  const dbPath = join(home, COMMITTED_DATABASE_FILENAME);
23
24
  if (!existsSync(dbPath)) {
24
- throw new Error(`telemetryMode=${mode} requires SQLite storage, but ${dbPath} does not exist. `
25
+ throw new Error(`telemetryEnabled=true requires SQLite storage, but ${dbPath} does not exist. `
25
26
  + "Migrate this Home to the database backend first (yui upgrade).");
26
27
  }
27
28
  const store = new SqliteTelemetryStore(home, {
@@ -29,5 +30,5 @@ export function openSchedulerTelemetry(home, config) {
29
30
  terminalKeep: resolveTerminalKeep(config.telemetryTerminalKeep),
30
31
  runCap: resolveRunCap(config.telemetryRunCap)
31
32
  });
32
- return { mode, sink: store, reader: store };
33
+ return { mode, sink: store, reader: store, retention: store };
33
34
  }
@@ -1025,7 +1025,7 @@ export class TmuxManager {
1025
1025
  /**
1026
1026
  * Pin the Role window to the largest attached client. The tmux default
1027
1027
  * `window-size latest` lets a later-attaching smaller client (e.g. the Web
1028
- * terminal or a second `task enter` from a smaller pane) shrink the shared
1028
+ * terminal or a second Role viewer from a smaller pane) shrink the shared
1029
1029
  * Role window, leaving the primary viewer with a TUI pinned to the top of a
1030
1030
  * large terminal and no scrollback. `largest` keeps the window at the
1031
1031
  * biggest attached client so a compact viewer cannot compress it.
@@ -6,6 +6,7 @@ import { projectRunRecovery, readRunRecoveryFacts } from "../run/recoveryProject
6
6
  import { classifyRuntimeHealth, projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
7
7
  import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
8
8
  import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
9
+ import { resolveRuntimeHealth } from "../config/yuiConfig.js";
9
10
  export function buildWebDashboardSnapshot(store, now = new Date()) {
10
11
  return store.transaction((reader) => {
11
12
  const statusCounts = {
@@ -88,7 +89,7 @@ export function buildWebTaskDetail(store, taskId, now = new Date()) {
88
89
  .map((run) => [run.roleName, run]));
89
90
  const activeRunHealth = runs
90
91
  .filter((run) => run.status === "active")
91
- .map((run) => projectWebRunRuntimeHealth(reader, taskId, run, events, now));
92
+ .map((run) => projectWebRunRuntimeHealth(reader, taskId, run, events, now, resolveRuntimeHealth(reader.getConfig().runtimeHealth)));
92
93
  const roles = reader.listRoles(taskId).map((role) => {
93
94
  const activeRun = activeRuns.get(role.name);
94
95
  const sessions = reader.getTaskRoleSessionSet(taskId, role.name);
@@ -137,7 +138,7 @@ function latestStallProgress(events, runId) {
137
138
  * scheduler's durable `run.stalled` episode is surfaced as
138
139
  * `stalled-candidate`.
139
140
  */
140
- function projectWebRunRuntimeHealth(reader, taskId, run, events, now) {
141
+ function projectWebRunRuntimeHealth(reader, taskId, run, events, now, policy) {
141
142
  const stalled = isRoleRunStalled(events, run.id);
142
143
  const sessions = reader.getTaskRoleSessionSet(taskId, run.roleName);
143
144
  const session = sessions?.sessions[run.effective.agentId];
@@ -193,7 +194,8 @@ function projectWebRunRuntimeHealth(reader, taskId, run, events, now) {
193
194
  const classification = classifyRuntimeHealth({
194
195
  projection,
195
196
  semanticProgressAt: semanticProgress.progressAt,
196
- now
197
+ now,
198
+ policy
197
199
  });
198
200
  return {
199
201
  runId: run.id,
@@ -45,31 +45,34 @@ yui setup
45
45
  yui doctor
46
46
  ```
47
47
 
48
- `setup` 是交互式的:检测已安装的 Agent CLI,选择要配置的 Agent、默认
49
- Agent Operator Agent,并实时探测所选 CLI 当前支持的模型。它先配置
50
- Leader Operator,再说明全局 Worker 配置会复制到新建的 Task Role
51
- 让用户选择 Worker 复用 Leader 配置还是单独配置。模型选择后只展示该模型
52
- 支持的思考强度。随后 setup 会确认位于 Yui home 外部的 Project workspace,
53
- 并询问 shell completion。选择器同时提供原生 CLI 默认值和自定义值入口。
54
- 再次运行不会删除已有 Task/Role,也不会改变当前安装的 Project workspace,
55
- 可用于安全地调整配置。setup 成功返回前会确保当前 Home 的后台 Controller
56
- 已经启动。
57
-
58
- 模型与思考强度属于 Agent binding 设置,因此 Operator、Leader 和全局
59
- Worker 即使使用同一个 Agent CLI,也可以采用不同配置。Profile 中的
60
- model/effort 只是 native child 的可移植 hint。
61
-
62
- Setup 会为每个受管 Agent binding 显式设置 `bypass` permission strategy。
63
- 后续 Role 更新可选择 `default`、`bypass` `configured`;`configured`
64
- 保留对应 adapter 的原生权限枚举与工具规则。
65
-
66
- 运行时能力目录会在每次命令中刷新,并缓存在 Yui home。实时探测超时或失败时,Yui 会展示同一 Agent 启动上下文最近一次成功的缓存并明确提示数据可能过期;没有匹配缓存时,则提供 CLI 默认值和自定义入口。`yui agent capabilities <id>` 可一次性读取同一份目录,包括模型、逐模型思考强度,以及权限、搜索可用性、profile、settings source、service tier 等其他运行时选项。
48
+ `setup` 被刻意缩减为最小流程:检查 tmux,复用或创建一个可用 Agent,在
49
+ Yui home 外创建默认 workspace,并配置 Operator Leader,使用户可以启动
50
+ Yui 并执行 Task。它不会创建 Worker、Reviewer、Profile review policy
51
+ 也不会询问 model/effort、permission shell completion。Operator 与 Leader
52
+ 的必需 binding 使用 Yui adapter 默认 permission strategy(`bypass`);
53
+ 后续调整统一通过 `config role` 完成。再次运行会原样保留已经可用的 Operator
54
+ Leader;setup 成功返回前会启动当前 Home 的后台 Controller。
55
+
56
+ 所有持久配置都位于 `yui config` 下。`config show` 展示完整有效状态,
57
+ `config --help` 介绍各配置域并给出示例。Operator 可通过结构化的
58
+ `config describe` 读取配置目录,向用户说明当前值、具体影响、可选值和生效
59
+ 方式,并只执行用户确认的修改。
60
+
61
+ 持久设置按职责分组:`config system` 管理 Home 默认值和展示方式,
62
+ `config runtime` 管理 Controller 健康阈值、并发、启动、投递和 Provider
63
+ 重试,`config workflow` 管理 Leader、context review policy,
64
+ `config resources` 管理隔离区和 GC,`config tools` 管理 tmux 与诊断
65
+ telemetry。Agent、全局 Role、Profile 和 shell completion 则继续位于同级的
66
+ `config agent|role|profile|completion` 域。每个持久设置域统一使用
67
+ `show`、`set`、`clear`。
68
+
69
+ 运行时能力目录会在每次命令中刷新,并缓存在 Yui home。实时探测超时或失败时,Yui 会展示同一 Agent 启动上下文最近一次成功的缓存并明确提示数据可能过期;没有匹配缓存时,则提供 CLI 默认值和自定义入口。`yui config agent capabilities <id>` 可一次性读取同一份目录,包括模型、逐模型思考强度,以及权限、搜索可用性、profile、settings source、service tier 等其他运行时选项。
67
70
 
68
71
  `completion` 无论是否指定 shell,都会进入确认流程:
69
72
 
70
73
  ```sh
71
- yui completion
72
- yui completion zsh
74
+ yui config completion
75
+ yui config completion zsh
73
76
  ```
74
77
 
75
78
  流程会确认生成脚本、安装路径和 shell 启动文件修改。补全脚本直接由命令目录生成,支持二级及更深层子命令。
@@ -116,16 +119,16 @@ yui task activate <task-id>
116
119
 
117
120
  ```sh
118
121
  yui config show
119
- yui config set time-zone Europe/London
122
+ yui config system set time-zone Europe/London
120
123
  ```
121
124
 
122
125
  WorkItem 审查只有一条可选的全局规则,并直接复用已有 Global Role 的
123
126
  Agent、model、权限、prompt 和 Skills:
124
127
 
125
128
  ```sh
126
- yui config set review --role reviewer --trigger always
129
+ yui config workflow set review --role reviewer --trigger always
127
130
  yui config show
128
- yui config clear review
131
+ yui config workflow clear review
129
132
  ```
130
133
 
131
134
  对带 Project 的软件交付,可使用 `--trigger final`:WorkItem 验收与
@@ -133,7 +136,7 @@ Integration 保持独立,在 Task 完成前只对所有已集成 Project 的
133
136
  一次 Task 级 ReviewRound:
134
137
 
135
138
  ```sh
136
- yui config set review --role reviewer --trigger final
139
+ yui config workflow set review --role reviewer --trigger final
137
140
  ```
138
141
 
139
142
  每个进入 Leader 验收阶段的结果,都会成为原 WorkItem 上一个明确的候选。
@@ -261,7 +264,7 @@ Task/WorkItem 模型,不增加额外任务类型。
261
264
  从已配置的全局 Worker 创建 Task Role,应用 Profile 并派发 WorkItem:
262
265
 
263
266
  ```sh
264
- yui role show worker
267
+ yui config role show worker
265
268
  yui task role add <task-id> implementer --profile implementer
266
269
  yui task role show <task-id> implementer
267
270
 
@@ -327,7 +330,7 @@ yui task work create <task-id> "审查实现" \
327
330
  --objective "返回有源码依据的问题" \
328
331
  --accept "每个问题都标明受影响路径"
329
332
  yui task work update <task-id>/<work-item-id> running
330
- yui profile show reviewer
333
+ yui config profile show reviewer
331
334
  ```
332
335
 
333
336
  subagent 的创建与结果返回完全由 Leader 当前 Agent 的 native child 能力
@@ -432,20 +435,23 @@ Task 生命周期的交互选择只展示有效来源状态:activate 只展示
432
435
 
433
436
  ## Session 与 tmux
434
437
 
435
- tmux 负责 Agent 进程生命周期及其可观察输出。Global Operatorglobal Role 仍使用原生交互式 CLI;受管理的 Task Claude Run 则为每个 Run 启动一个有限生命周期进程,使用 `--print`、stream-json 输入和 stream-json 输出。Yui 通过 stdin 写入一条以换行结尾的精确 Run JSON user frame,并发排空 stdout/stderr,并通过 Claude session ID 保持原生上下文连续性。因此启动和投递不再依赖 TUI composer、ready 字符、粘贴延迟或模拟 Enter 键;Codex 保留其 adapter 原生的启动 prompt 与结构化 callback 路径。
438
+ 受管理的 Task Agent 统一使用混合 Provider Runtime:Controller Agent Host 通过 Provider 原生结构化协议提交和确认输入;tmux/PTY 只负责保持 Host 存活、展示输出,以及在显式人工接管后提供输入网关。受管理输入绝不会作为终端按键、粘贴文本或启动 argv 发送。Codex 使用持久 App Server JSON-RPC 进程;Claude 使用持久 stream-json 进程,并以精确回放的 user message 作为接收确认。
436
439
 
437
- `task enter` `task role enter` 只是附着到已存在的 Task Role pane:不会启动 Controller、准备 workspace、创建或恢复 Agent、唤醒 Role,也不会投递输入。Task attach 默认为 `--read-only`;只有显式指定 `--read-write` 才可交互,并且 Role 存在 active managed Run、受管理的 Claude 进程仍在退出,或同一 pane 已有 writer 时会被拒绝。读写 attach 会先发布 Role 级 tmux writer lease,再复核持久化 Run 状态,从而闭合与 Controller 启动之间的竞态。lease 存续期间只暂停该 Role 的受管理投递且不消耗有界投递重试;detach 会释放 lease,并且只通知已经存在的持久化 Role 工作重新评估,同一 Task 的其他 Role 不受影响。attach 前,Yui 会关闭 readline、退出 raw mode、暂停自身 stdin,再同步把终端交给 tmuxattach 会继承外层终端的真实能力并进入干净的 alternate screen;鼠标滚动只查看 Agent pane 的 100,000 行 tmux 历史,不再混入此前的 shell 或 IDE Terminal 历史。读写 attach 可以使用现有 pane 本身支持的原生交互,但它不参与受管理会话的启动或投递。
440
+ Run、Conversation、ActivationTurn 是四个独立身份。Conversation 可以跨多个 Run 和进程;Activation 只代表一次 Provider 进程存活期;Turn 在写入前先持久化。写入超时或结果不明确会进入 `delivery-unknown`,不会自动重发。Provider 进程退出后结束当前 Activation;恢复同一 Conversation 会创建新 Activation 并推进 authority epoch
438
441
 
439
- tmux 会在 pane 创建时固定其历史容量。配置该限制之前创建的 Role 会保留原容量;Yui 会在 Terminal attach 和 Web 中提示用户退出并重新进入一次,从而在保留 Agent 原生对话的同时创建具有 100,000 行历史的新 pane。
440
-
441
- Global 交互入口在不存在 writer 时保持可写;已有 writer 时自动降级为只读。global Web 对每个 tmux session 只允许一个 writer;Task Web 始终只读。Task CLI 入口除非显式请求 `--read-write`,否则始终只读,避免观察动作改变 Agent 执行。
442
+ Task Role 使用以下显式入口:
442
443
 
443
444
  ```sh
444
- yui role enter <global-role>
445
- yui task enter <task-id> [role] [--read-only | --read-write]
446
- yui task role enter <task-id> <role> [--read-only | --read-write]
445
+ yui session enter <global-role>
446
+ yui task role view <task-id> <role>
447
+ yui task role takeover <task-id> <role>
448
+ yui task role release <task-id> <role>
447
449
  ```
448
450
 
451
+ `view` 始终只读。`takeover` 要求存在 active managed Run 且没有未决 Turn;它先以持久 CAS 把唯一 writer authority 转给人工 holder,再把相同 epoch 同步给 Agent Host,最后开放 PTY 输入网关。人工输入仍由 Host 转换为结构化 Provider Turn,而不是直接注入 Provider 终端。detach 会自动归还 authority;`release` 即使没有 active Run 也可执行,用于幂等修复中断或未完全同步的接管。Global Operator 与 global Role 继续使用原生交互式 CLI,不属于受管理 Task Provider 协议。
452
+
453
+ tmux 会在 pane 创建时固定其历史容量。配置该限制之前创建的 Role 会保留原容量;Yui 会在 Terminal attach 和 Web 中提示用户退出并重新进入一次,从而创建具有 100,000 行历史的新 pane。
454
+
449
455
  每个 Role 可绑定多个 Agent,但任一时刻只有一个 active Agent,并为每个
450
456
  Agent binding 独立保存 native session。Operator 进一步限制为同一种
451
457
  adapter 最多绑定一个,例如可同时绑定一个 Codex 和一个 Claude;这些
@@ -455,9 +461,9 @@ binding 是预先保存、可随时切换的配置,而不是并行身份。Ope
455
461
  并切换。跨 Agent 切换默认复用已保存的 model/effort,只有用户明确选择
456
462
  更新时才进入现有配置选择流程。
457
463
 
458
- 使用 `yui role unbind <global-role> <agent-id>` 或 `yui task role unbind <task-id> <role> <agent-id>` 可移除休眠 binding。active binding 或任何未 stopped 的 native session 都会被拒绝;stopped session 记录会和 binding 在同一事务中删除。
464
+ 使用 `yui config role unbind <global-role> <agent-id>` 或 `yui task role unbind <task-id> <role> <agent-id>` 可移除休眠 binding。active binding 或任何未 stopped 的 native session 都会被拒绝;stopped session 记录会和 binding 在同一事务中删除。
459
465
 
460
- Claude 的 session ID 在启动前分配。每个受管理的 Task Claude Run 都使用新的有限生命周期进程;resume 会针对固定 native session 启动新进程,而不是复用交互式 pane。受管理的 Codex 启动使用 Codex 结构化 `notify` 回调,在 turn 完成后记录 thread ID,不再向模型对话注入 session-bind prompt。
466
+ Claude 的 session ID 在启动前分配,并由持久 stream-json Provider 进程承载多个 Turn;Codex 使用持久 App Server thread。两者都复用同一套 Conversation、Activation、Turn authority fence,不再向模型对话注入 session-bind prompt。
461
467
 
462
468
  自动生命周期与投递判断只使用结构化 Hook payload、持久身份、tmux process
463
469
  state、receipt 与 pane fence。Yui 不会解析 prompt glyph、进度文本、trust dialog
@@ -527,9 +533,11 @@ Web 端可以通过与 Terminal 相同的持久化 CLI 路径回答 open InputRe
527
533
 
528
534
  ```sh
529
535
  yui update
530
- yui agent add|list|show|capabilities|update|remove
531
- yui role add|list|show|update|remove|bind|enter
532
- yui role session record|replace
536
+ yui config agent add|list|show|capabilities|update|remove
537
+ yui config role add|list|show|update|remove|bind|unbind
538
+ yui config profile add|list|show|update|remove|reset
539
+ yui config completion [bash|zsh|fish]
540
+ yui session enter|record|replace|reconcile
533
541
  yui project add|clone|update|discover|list|show|knowledge
534
542
  ```
535
543
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zq-silk/yui",
3
- "version": "0.7.1",
3
+ "version": "0.8.2",
4
4
  "description": "Local control plane for long-running native agent CLI sessions backed by tmux.",
5
5
  "license": "MIT",
6
6
  "private": false,