@zq-silk/yui 0.6.5 → 0.6.7

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.
@@ -849,7 +849,7 @@ export const ROOT_COMMAND = buildNode({
849
849
  sections: [{
850
850
  id: "lifecycle",
851
851
  title: "Commands",
852
- entries: ["status", "cleanup", "identity", "stop", "restart"]
852
+ entries: ["status", "cleanup", "identity", "live-identity", "stop", "restart"]
853
853
  }],
854
854
  children: [
855
855
  {
@@ -869,6 +869,11 @@ export const ROOT_COMMAND = buildNode({
869
869
  summary: "Read the stable runtime identity receipt (build, backend, worker).",
870
870
  hidden: true
871
871
  },
872
+ {
873
+ name: "live-identity",
874
+ summary: "Read the authenticated live Controller launch identity.",
875
+ hidden: true
876
+ },
872
877
  { name: "stop", summary: "Stop the Controller." },
873
878
  { name: "restart", summary: "Restart internal services without stopping tmux sessions." }
874
879
  ]
@@ -33,7 +33,8 @@ function resolveExecutionPath(args) {
33
33
  const internalExecutable = child !== undefined && ((node === ROOT_COMMAND && child.name === "internal")
34
34
  || (node.path.join(" ") === "yui completion" && child.name === "candidates")
35
35
  || (node.path.join(" ") === "yui task run" && child.name === "checkpoint")
36
- || (node.path.join(" ") === "yui controller" && child.name === "identity"));
36
+ || (node.path.join(" ") === "yui controller"
37
+ && (child.name === "identity" || child.name === "live-identity")));
37
38
  if (child === undefined || (child.hidden && !internalExecutable)) {
38
39
  if (node.kind === "hybrid" && node.acceptsArguments)
39
40
  break;
@@ -261,10 +261,12 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
261
261
  }
262
262
  },
263
263
  // `runUpdate` is intentionally synchronous because the npm/staged-binary
264
- // ports use spawnSync. Keep Controller ownership in this same owner by
265
- // using the CLI's structured lifecycle commands; tests can replace these
266
- // seams with deterministic fakes without touching a real Controller.
264
+ // ports use spawnSync. A short-lived child owns the async, exactly fenced
265
+ // reconciliation first; lifecycle capture then uses structured live
266
+ // Controller commands. Tests can replace these seams with deterministic
267
+ // fakes without touching a real Controller.
267
268
  controllerStatus(home) {
269
+ reconcileControllerResourcesForUpdate(home, environment, spawn);
268
270
  return readControllerLifecycle(home, environment, spawn);
269
271
  },
270
272
  stopController(home, expectedPid) {
@@ -285,6 +287,7 @@ export function createUpdatePorts(environment, spawn = spawnSync, stagingRoot =
285
287
  }
286
288
  const UPDATE_CLI_PATH = fileURLToPath(new URL("../cli.js", import.meta.url));
287
289
  const UPDATE_CLIENT_RUNTIME_PATH = fileURLToPath(new URL("../controller/clientRuntime.js", import.meta.url));
290
+ const UPDATE_CONTROLLER_RECONCILIATION_PATH = fileURLToPath(new URL("../controller/updateReconciliation.js", import.meta.url));
288
291
  /**
289
292
  * Capture running state plus an authenticated exact process identity before
290
293
  * stopping. Public `controller status` is an inventory and intentionally
@@ -330,18 +333,18 @@ function readControllerLifecycle(home, environment, spawn, cliBinary) {
330
333
  if (current !== undefined) {
331
334
  throw new Error("Controller inventory is current but has no process proof; treating ownership as unknown-active.");
332
335
  }
333
- return proveControllerAbsent(home, environment, spawn);
336
+ return proveControllerAbsent(home, environment, spawn, cliBinary);
334
337
  }
335
- const identity = parseControllerIdentity(runControllerCommand(home, environment, spawn, "identity", cliBinary));
338
+ const identity = parseControllerIdentity(runControllerCommand(home, environment, spawn, "live-identity", cliBinary));
336
339
  return {
337
340
  running: true,
338
341
  ...(isPositivePid(processInfo.pid) ? { pid: processInfo.pid } : {}),
339
342
  identity
340
343
  };
341
344
  }
342
- function proveControllerAbsent(home, environment, spawn) {
345
+ function proveControllerAbsent(home, environment, spawn, cliBinary) {
343
346
  try {
344
- runControllerCommand(home, environment, spawn, "identity");
347
+ runControllerCommand(home, environment, spawn, "live-identity", cliBinary);
345
348
  }
346
349
  catch (error) {
347
350
  if (controllerErrorCode(error) === "CONTROLLER_NOT_RUNNING") {
@@ -349,7 +352,7 @@ function proveControllerAbsent(home, environment, spawn) {
349
352
  }
350
353
  throw new Error(`Controller absence could not be authenticated: ${messageOf(error)}`, { cause: error });
351
354
  }
352
- throw new Error("Controller identity is reachable but inventory is currentless; treating ownership as unknown-active.");
355
+ throw new Error("A live Controller identity is reachable but inventory is currentless; treating ownership as unknown-active.");
353
356
  }
354
357
  function stopControllerForUpdate(home, expectedPid, environment, spawn) {
355
358
  // Parent update owns this lifecycle boundary. Invoke the runtime client
@@ -464,7 +467,7 @@ function restartControllerForUpdate(home, environment, spawn, activatedBinary, a
464
467
  const replacementPid = parseReplacementPid(restart.data);
465
468
  let identityFailure;
466
469
  try {
467
- const identity = parseControllerIdentity(runControllerCommand(home, environment, spawn, "identity", activatedBinary));
470
+ const identity = parseControllerIdentity(runControllerCommand(home, environment, spawn, "live-identity", activatedBinary));
468
471
  assertActivatedControllerIdentity(identity, activatedBinary, activatedVersion);
469
472
  }
470
473
  catch (error) {
@@ -586,6 +589,57 @@ function runControllerCommand(home, environment, spawn, method, cliBinary) {
586
589
  }
587
590
  return parsed.data;
588
591
  }
592
+ function reconcileControllerResourcesForUpdate(home, environment, spawn) {
593
+ const helper = [
594
+ "const values = process.argv.slice(1);",
595
+ "const home = values.pop();",
596
+ "const reconciliationModule = values.pop();",
597
+ "(async () => {",
598
+ " const { reconcileControllerResourcesForUpdate } = await import(reconciliationModule);",
599
+ " const data = await reconcileControllerResourcesForUpdate(home, process.env);",
600
+ " process.stdout.write(JSON.stringify({ ok: true, data }));",
601
+ "})().catch((error) => {",
602
+ " const message = error instanceof Error ? error.message : String(error);",
603
+ " process.stderr.write(JSON.stringify({ ok: false, code: 'RUNTIME_ERROR', message }));",
604
+ " process.exitCode = 5;",
605
+ "});"
606
+ ].join(" ");
607
+ const result = spawn(process.execPath, ["-e", helper, UPDATE_CONTROLLER_RECONCILIATION_PATH, home], { cwd: process.cwd(), env: { ...environment, YUI_HOME: home }, shell: false });
608
+ if (result.error !== undefined || result.status !== 0) {
609
+ const detail = structuredErrorMessage(result) ?? result.stderr.toString("utf8").trim();
610
+ throw new Error(`Controller reconciliation failed (exit ${result.status ?? "null"})${detail.length === 0 ? "." : `: ${detail}`}`);
611
+ }
612
+ let parsed;
613
+ try {
614
+ parsed = JSON.parse(result.stdout.toString("utf8"));
615
+ }
616
+ catch (error) {
617
+ throw new Error("Controller reconciliation returned an invalid structured result.", {
618
+ cause: error
619
+ });
620
+ }
621
+ if (!isRecord(parsed)
622
+ || parsed.ok !== true
623
+ || !isRecord(parsed.data)
624
+ || !Array.isArray(parsed.data.cleaned)
625
+ || parsed.data.cleaned.some((id) => typeof id !== "string")) {
626
+ throw new Error("Controller reconciliation returned an invalid structured result.");
627
+ }
628
+ }
629
+ function structuredErrorMessage(result) {
630
+ for (const buffer of [result.stderr, result.stdout]) {
631
+ try {
632
+ const value = JSON.parse(buffer.toString("utf8"));
633
+ if (isRecord(value) && typeof value.message === "string" && value.message.length > 0) {
634
+ return value.message;
635
+ }
636
+ }
637
+ catch {
638
+ // Fall back to the child's raw stderr below.
639
+ }
640
+ }
641
+ return undefined;
642
+ }
589
643
  function parseControllerIdentity(value) {
590
644
  if (typeof value.executablePath !== "string"
591
645
  || value.executablePath.length === 0
package/dist/cli.js CHANGED
@@ -301,6 +301,29 @@ export async function main() {
301
301
  }
302
302
  if (args[0] === "controller") {
303
303
  const method = args[1];
304
+ if (method === "live-identity" && args.length === 2) {
305
+ try {
306
+ const identity = await callController(home, "controller.identity", {});
307
+ emit("", false, identity);
308
+ }
309
+ catch (error) {
310
+ if (!(error instanceof ControllerClientError))
311
+ throw error;
312
+ if (jsonOutput) {
313
+ process.stderr.write(`${JSON.stringify({
314
+ ok: false,
315
+ code: error.code,
316
+ message: error.message,
317
+ details: {}
318
+ })}\n`);
319
+ }
320
+ else {
321
+ process.stderr.write(`RUNTIME_ERROR: ${error.message}\n`);
322
+ }
323
+ process.exitCode = 5;
324
+ }
325
+ return;
326
+ }
304
327
  if (method === "identity" && args.length === 2) {
305
328
  // Issue 02: the stable, read-only runtime identity receipt. It survives
306
329
  // a Controller stop and answers build ID, package digest, backend, and
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
3
3
  import { createRuntimeObservation, runtimeObservationFenceMatches, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
4
+ const DEFAULT_MAX_CONCURRENT_SAMPLES = 8;
5
+ const MAX_CONCURRENT_SAMPLES = 64;
4
6
  /**
5
7
  * Controller-owned, provider-independent sampler. Drivers own source parsing;
6
8
  * this component owns active-Run discovery, cursor lifetime, canonical event
@@ -11,11 +13,13 @@ export class AgentRuntimeObserver {
11
13
  inbox;
12
14
  drivers;
13
15
  #states = new Map();
16
+ #maxConcurrentSamples;
14
17
  #sequence = 0;
15
- constructor(store, inbox, drivers = builtinAgentDriverRegistry()) {
18
+ constructor(store, inbox, drivers = builtinAgentDriverRegistry(), options = {}) {
16
19
  this.store = store;
17
20
  this.inbox = inbox;
18
21
  this.drivers = drivers;
22
+ this.#maxConcurrentSamples = sampleConcurrency(options.maxConcurrentSamples);
19
23
  }
20
24
  async sample(now = new Date()) {
21
25
  const active = this.activeSources();
@@ -25,7 +29,9 @@ export class AgentRuntimeObserver {
25
29
  this.#states.delete(key);
26
30
  }
27
31
  const dirty = new Set();
28
- await Promise.all(active.map(async ({ key, fence, source, freshSession, persistedState }) => {
32
+ const sequenceBase = this.#sequence;
33
+ this.#sequence += active.length;
34
+ await forEachConcurrent(active, this.#maxConcurrentSamples, async ({ key, fence, source, freshSession, persistedState }, index) => {
29
35
  const existingState = this.#states.get(key);
30
36
  // Cursor state is intentionally process-local, but the latest canonical
31
37
  // usage/activity baseline is durable. Rehydrate it after Controller
@@ -49,7 +55,10 @@ export class AgentRuntimeObserver {
49
55
  }
50
56
  state.cursor = sample.cursor;
51
57
  const at = now.toISOString();
52
- const sequence = this.#sequence++;
58
+ // Reserve sequence numbers in source-key order before sampling. Provider
59
+ // latency can change completion order without changing observation
60
+ // identity or the canonical sequence assigned to a source.
61
+ const sequence = sequenceBase + index;
53
62
  if (existingState === undefined && freshSession && state.usage === undefined) {
54
63
  const zero = Object.freeze({ inputTokens: 0, outputTokens: 0 });
55
64
  this.inbox.enqueueObservation(createRuntimeObservation({
@@ -114,28 +123,52 @@ export class AgentRuntimeObserver {
114
123
  if (sample.activityId !== undefined)
115
124
  state.activityId = sample.activityId;
116
125
  this.#states.set(key, state);
117
- }));
118
- return Object.freeze([...dirty].sort());
126
+ });
127
+ return Object.freeze([...dirty].sort(numericCompare));
119
128
  }
120
129
  activeSources() {
121
130
  const result = [];
122
- for (const task of this.store.listTasks()) {
123
- if (task.status !== "active")
124
- continue;
125
- const observations = this.store.listEvents(task.id)
126
- .map(runtimeObservationFromTaskEvent)
127
- .filter((value) => value !== null);
131
+ const indexedTaskIds = this.store.listActiveTaskIds?.();
132
+ // FileTaskStore remains a development/compatibility fallback. The normal
133
+ // Controller store is SQLite and must discover only its indexed hot set.
134
+ const activeTasks = indexedTaskIds === undefined
135
+ ? this.store.listTasks().filter((task) => task.status === "active")
136
+ : [...new Set(indexedTaskIds)]
137
+ .sort(numericCompare)
138
+ .map((taskId) => this.store.getTask(taskId))
139
+ .filter((task) => (task !== null && task.status === "active"));
140
+ for (const task of activeTasks) {
141
+ // A Task still incurs one O(E) event projection. Group those observations
142
+ // by Run and sort each group once so every active Run can reuse the same
143
+ // ordered slice instead of repeatedly filtering/sorting all E events.
144
+ const observationsByRunId = new Map();
145
+ for (const event of this.store.listEvents(task.id)) {
146
+ const observation = runtimeObservationFromTaskEvent(event);
147
+ const runId = observation?.fence.runId;
148
+ if (observation === null || runId === undefined)
149
+ continue;
150
+ const grouped = observationsByRunId.get(runId);
151
+ if (grouped === undefined) {
152
+ observationsByRunId.set(runId, [observation]);
153
+ }
154
+ else {
155
+ grouped.push(observation);
156
+ }
157
+ }
158
+ for (const observations of observationsByRunId.values()) {
159
+ observations.sort(compareObservations);
160
+ }
128
161
  for (const run of this.store.listAgentRuns(task.id)) {
129
162
  if (run.status !== "active"
130
163
  || this.store.getActiveAgentRun(task.id, run.roleName)?.id !== run.id)
131
164
  continue;
165
+ const observations = observationsByRunId.get(run.id) ?? [];
132
166
  const accepted = observations
133
167
  .filter((observation) => observation.kind === "turn.accepted"
134
168
  && observation.fence.runId === run.id
135
169
  && observation.fence.roleName === run.roleName
136
170
  && observation.fence.agentId === run.effective.agentId
137
171
  && observation.payload.observerSource !== undefined)
138
- .sort(compareObservations)
139
172
  .at(-1);
140
173
  const source = accepted?.payload.observerSource;
141
174
  if (accepted === undefined || source === undefined
@@ -150,19 +183,23 @@ export class AgentRuntimeObserver {
150
183
  }
151
184
  const fence = accepted.fence;
152
185
  const exact = observations
153
- .filter((observation) => runtimeObservationFenceMatches(fence, observation.fence))
154
- .sort(compareObservations);
186
+ .filter((observation) => runtimeObservationFenceMatches(fence, observation.fence));
155
187
  const persistedUsage = exact.filter((observation) => (observation.kind === "activity.observed"
156
188
  && observation.payload.usage !== undefined)).at(-1);
157
189
  const persistedHealth = exact.filter((observation) => (observation.kind === "observer.health"
158
190
  && observation.payload.sourceId === source.sourceId)).at(-1);
159
191
  result.push(Object.freeze({
160
192
  key: JSON.stringify([
193
+ fence.taskId,
194
+ fence.roleName,
195
+ fence.runId,
196
+ fence.agentId,
161
197
  fence.driverId,
198
+ fence.launchId,
162
199
  fence.sessionGenerationId,
163
200
  fence.nativeSessionId,
164
201
  fence.nativeTurnId,
165
- fence.runId,
202
+ fence.receiptId,
166
203
  source.sourceId
167
204
  ]),
168
205
  fence,
@@ -187,8 +224,31 @@ export class AgentRuntimeObserver {
187
224
  }));
188
225
  }
189
226
  }
190
- return result;
227
+ return Object.freeze(result.sort((left, right) => numericCompare(left.key, right.key)));
228
+ }
229
+ }
230
+ async function forEachConcurrent(values, limit, visit) {
231
+ let nextIndex = 0;
232
+ const workers = Math.min(limit, values.length);
233
+ await Promise.all(Array.from({ length: workers }, async () => {
234
+ while (nextIndex < values.length) {
235
+ const index = nextIndex;
236
+ nextIndex += 1;
237
+ await visit(values[index], index);
238
+ }
239
+ }));
240
+ }
241
+ function sampleConcurrency(value) {
242
+ const resolved = value ?? DEFAULT_MAX_CONCURRENT_SAMPLES;
243
+ if (!Number.isSafeInteger(resolved)
244
+ || resolved < 1
245
+ || resolved > MAX_CONCURRENT_SAMPLES) {
246
+ throw new Error(`Agent runtime observer maxConcurrentSamples must be between 1 and ${MAX_CONCURRENT_SAMPLES}.`);
191
247
  }
248
+ return resolved;
249
+ }
250
+ function numericCompare(left, right) {
251
+ return left.localeCompare(right, undefined, { numeric: true });
192
252
  }
193
253
  function compareObservations(left, right) {
194
254
  return left.receivedAt.localeCompare(right.receivedAt)