@zq-silk/yui 2.2.0 → 2.4.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.
Files changed (206) hide show
  1. package/README.md +19 -3
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +25 -11
  7. package/dist/cli/interactionPolicy.js +4 -0
  8. package/dist/cli/invocationAuthority.js +1 -1
  9. package/dist/commands/executionAuditCommands.js +5 -0
  10. package/dist/commands/globalRoleCommands.js +41 -3
  11. package/dist/commands/projectCommands.js +89 -94
  12. package/dist/commands/roleSkillValidation.js +4 -8
  13. package/dist/commands/taskCommands.js +30 -9
  14. package/dist/commands/taskIntegrationCommands.js +5 -16
  15. package/dist/commands/taskUsageCommand.js +38 -0
  16. package/dist/context/roleSessionContext.js +18 -2
  17. package/dist/context/runContextPack.js +5 -1
  18. package/dist/context/sessionBootstrapManifest.js +10 -1
  19. package/dist/context/skillCatalog.js +108 -0
  20. package/dist/context/skillPackage.js +161 -0
  21. package/dist/context/taskContext.js +3 -2
  22. package/dist/context/taskSearch.js +127 -0
  23. package/dist/controlPlaneCli.js +51 -39
  24. package/dist/controller/clientRuntime.js +3 -2
  25. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  26. package/dist/controller/jobControl.js +2 -0
  27. package/dist/controller/nativeHostInput.js +37 -0
  28. package/dist/controller/runtime.js +93 -2
  29. package/dist/controller/runtimeEventInbox.js +2 -2
  30. package/dist/controller/runtimeEventProcessor.js +3 -3
  31. package/dist/doctor/doctor.js +10 -0
  32. package/dist/execution/reviewMainRun.js +1 -0
  33. package/dist/execution/workItemMainRun.js +1 -0
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  37. package/dist/integration/gitIntegrationService.js +15 -79
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/job/httpPreview.js +61 -0
  40. package/dist/job/jobCleanupInspection.js +67 -0
  41. package/dist/job/jobRunner.js +12 -2
  42. package/dist/kernel/builtinCapabilities.js +3 -1
  43. package/dist/message/message.js +15 -1
  44. package/dist/output/rolePresentation.js +4 -1
  45. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  46. package/dist/repository/gitCompatibility.js +62 -0
  47. package/dist/repository/gitWorkspace.js +86 -80
  48. package/dist/repository/gitWorktreeInventory.js +121 -0
  49. package/dist/repository/project.js +74 -25
  50. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  51. package/dist/repository/taskWorkspacePreparer.js +39 -43
  52. package/dist/repository/workspaceCleanupInspection.js +88 -7
  53. package/dist/resources/resourceDiscovery.js +2 -2
  54. package/dist/resources/resourceGc.js +7 -4
  55. package/dist/resources/resourceRegistrar.js +18 -11
  56. package/dist/runtime/agentEndpoint.js +8 -0
  57. package/dist/runtime/agentHost.js +170 -3
  58. package/dist/runtime/codexAppServerRuntime.js +1 -1
  59. package/dist/runtime/codexNativeAccess.js +175 -0
  60. package/dist/runtime/controlledCodexTui.js +116 -0
  61. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  62. package/dist/runtime/publicReply.js +23 -0
  63. package/dist/runtime/runtimeObservation.js +48 -0
  64. package/dist/runtime/structuredProviderHost.js +16 -7
  65. package/dist/runtime/taskCostMetrics.js +133 -0
  66. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  67. package/dist/runtime/taskUsageMetrics.js +11 -2
  68. package/dist/runtime/taskUsageQuery.js +38 -0
  69. package/dist/scheduler/taskExecutionProjection.js +2 -0
  70. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  71. package/dist/storage/homeLayout.js +13 -0
  72. package/dist/storage/sqliteSchema.js +44 -0
  73. package/dist/storage/sqliteStore.js +53 -0
  74. package/dist/storage/storageVersions.js +1 -1
  75. package/dist/storage/storeRpc.js +2 -0
  76. package/dist/storage/taskSearch.js +35 -0
  77. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  78. package/dist/task/archiveDiagnostics.js +26 -8
  79. package/dist/task/archivePreflight.js +26 -9
  80. package/dist/task/leaderArchive.js +9 -15
  81. package/dist/task/ordinaryArchive.js +42 -0
  82. package/dist/tmux/tmuxManager.js +8 -8
  83. package/dist/web/assets/assetManifest.js +11 -51
  84. package/dist/web/assets/client/app/catalog.js +215 -0
  85. package/dist/web/assets/client/app/main.js +148 -0
  86. package/dist/web/assets/client/app/selection.js +139 -0
  87. package/dist/web/assets/client/app/settings.js +326 -0
  88. package/dist/web/assets/client/app/taskView.js +280 -0
  89. package/dist/web/assets/client/domain/context.js +96 -0
  90. package/dist/web/assets/client/domain/records.js +126 -0
  91. package/dist/web/assets/client/domain/runs.js +119 -0
  92. package/dist/web/assets/client/domain/taskForms.js +197 -0
  93. package/dist/web/assets/client/domain/vocab.js +69 -0
  94. package/dist/web/assets/client/domain/work.js +116 -0
  95. package/dist/web/assets/client/i18n/en.js +900 -0
  96. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  97. package/dist/web/assets/client/index.js +114 -0
  98. package/dist/web/assets/client/layout/resizer.js +37 -0
  99. package/dist/web/assets/client/layout/router.js +35 -0
  100. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  101. package/dist/web/assets/client/layout/sizing.js +95 -0
  102. package/dist/web/assets/client/layout/workspace.js +247 -0
  103. package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
  104. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  105. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  106. package/dist/web/assets/client/lib/i18n.js +54 -0
  107. package/dist/web/assets/client/lib/markdown.js +167 -0
  108. package/dist/web/assets/client/lib/prefs.js +28 -0
  109. package/dist/web/assets/client/lib/theme.js +68 -0
  110. package/dist/web/assets/client/ui/containers.js +101 -0
  111. package/dist/web/assets/client/ui/controls.js +51 -0
  112. package/dist/web/assets/client/ui/forms.js +48 -0
  113. package/dist/web/assets/client/ui/metrics.js +137 -0
  114. package/dist/web/assets/client/ui/primitives.js +92 -0
  115. package/dist/web/assets/client/ui/text.js +33 -0
  116. package/dist/web/assets/client/ui/toast.js +12 -0
  117. package/dist/web/assets/client/views/bodySearch.js +172 -0
  118. package/dist/web/assets/client/views/createTask.js +85 -0
  119. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  120. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  121. package/dist/web/assets/client/views/dock/preview.js +129 -0
  122. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  123. package/dist/web/assets/client/views/globalInput.js +67 -0
  124. package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
  125. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  126. package/dist/web/assets/client/views/skills.js +152 -0
  127. package/dist/web/assets/client/views/task/actions.js +36 -0
  128. package/dist/web/assets/client/views/task/delivery.js +185 -0
  129. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  130. package/dist/web/assets/client/views/task/files.js +272 -0
  131. package/dist/web/assets/client/views/task/observation.js +153 -0
  132. package/dist/web/assets/client/views/task/overview.js +161 -0
  133. package/dist/web/assets/client/views/task/page.js +131 -0
  134. package/dist/web/assets/client/views/task/records.js +163 -0
  135. package/dist/web/assets/client/views/task/runtime.js +130 -0
  136. package/dist/web/assets/client/views/task/timeline.js +190 -0
  137. package/dist/web/assets/shared/geometry.js +19 -0
  138. package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
  139. package/dist/web/assets/shared/taskTabs.js +8 -0
  140. package/dist/web/assets/shared/themes.js +93 -0
  141. package/dist/web/assets/shell/dialogs.js +63 -0
  142. package/dist/web/assets/shell/dock.js +40 -0
  143. package/dist/web/assets/shell/index.js +40 -0
  144. package/dist/web/assets/shell/settings.js +34 -0
  145. package/dist/web/assets/shell/sidebar.js +45 -0
  146. package/dist/web/assets/styles/index.js +41 -0
  147. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
  148. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  149. package/dist/web/assets/styles/markdown.js +22 -3
  150. package/dist/web/assets/styles/tokens.js +10 -63
  151. package/dist/web/assets/styles/ui/controls.js +76 -0
  152. package/dist/web/assets/styles/ui/forms.js +46 -0
  153. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  154. package/dist/web/assets/styles/views/delivery.js +29 -0
  155. package/dist/web/assets/styles/views/dock.js +71 -0
  156. package/dist/web/assets/styles/views/page.js +32 -0
  157. package/dist/web/assets/styles/views/records.js +70 -0
  158. package/dist/web/assets/styles/views/settings.js +26 -0
  159. package/dist/web/assets/styles/views/sidebar.js +74 -0
  160. package/dist/web/assets/styles/views/task.js +80 -0
  161. package/dist/web/assets/styles/views/terminal.js +38 -0
  162. package/dist/web/taskPreviews.js +205 -0
  163. package/dist/web/tmuxWebTerminal.js +16 -9
  164. package/dist/web/webConversation.js +373 -0
  165. package/dist/web/webMaterials.js +155 -0
  166. package/dist/web/webServer.js +306 -18
  167. package/dist/web/webSettings.js +331 -0
  168. package/dist/web/webSkills.js +86 -0
  169. package/dist/web/webSnapshot.js +40 -8
  170. package/dist/web/webTaskSurface.js +114 -9
  171. package/dist/workspace/gitChangeSetCapture.js +3 -1
  172. package/docs/managed-turn-and-session-runtime.md +9 -0
  173. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  174. package/docs/observability/README.md +13 -3
  175. package/docs/observability/README.zh-CN.md +10 -3
  176. package/docs/release-workflow.md +53 -0
  177. package/docs/release-workflow.zh-CN.md +40 -0
  178. package/docs/storage-baseline.md +38 -1
  179. package/docs/storage-baseline.zh-CN.md +22 -0
  180. package/docs/task-delivery.md +36 -8
  181. package/docs/task-delivery.zh-CN.md +24 -7
  182. package/docs/task-discovery.md +28 -0
  183. package/docs/testing/verification-levels.md +2 -2
  184. package/i18n/README.zh-CN.md +4 -3
  185. package/package.json +1 -1
  186. package/skills/yui-leader/references/execution.md +2 -1
  187. package/skills/yui-leader/references/integration.md +20 -0
  188. package/skills/yui-operator/SKILL.md +4 -0
  189. package/skills/yui-reviewer/SKILL.md +3 -0
  190. package/skills/yui-runtime/SKILL.md +4 -0
  191. package/skills/yui-runtime/references/publication.md +20 -4
  192. package/skills/yui-worker/SKILL.md +3 -0
  193. package/dist/web/assets/client/app.js +0 -804
  194. package/dist/web/assets/client/components.js +0 -418
  195. package/dist/web/assets/client/detail.js +0 -333
  196. package/dist/web/assets/client/dock.js +0 -225
  197. package/dist/web/assets/client/evidence.js +0 -172
  198. package/dist/web/assets/client/forms.js +0 -230
  199. package/dist/web/assets/client/i18n.js +0 -1161
  200. package/dist/web/assets/client/markdown.js +0 -114
  201. package/dist/web/assets/client/records.js +0 -63
  202. package/dist/web/assets/client/sections.js +0 -313
  203. package/dist/web/assets/client/theme.js +0 -71
  204. package/dist/web/assets/shell.js +0 -161
  205. package/dist/web/assets/styles/components.js +0 -169
  206. package/dist/web/assets/styles/views.js +0 -292
@@ -15,6 +15,11 @@ export function isRuntimeTokenEvidence(observation) {
15
15
  && (observation.payload.observationQuality === "partial"
16
16
  || observation.payload.observationQuality === "unavailable")));
17
17
  }
18
+ /** Both money and tokens survive activity compaction; only tokens feed token metrics. */
19
+ export function isRuntimeUsageEvidence(observation) {
20
+ return isRuntimeTokenEvidence(observation)
21
+ || (observation.kind === "activity.observed" && observation.payload.cost !== undefined);
22
+ }
18
23
  const KINDS = [
19
24
  "host.observed",
20
25
  "session.started",
@@ -296,6 +301,11 @@ function normalizePayload(kind, input) {
296
301
  const observerSource = input.observerSource === undefined
297
302
  ? undefined
298
303
  : normalizeObserverSource(input.observerSource);
304
+ const cost = input.cost === undefined ? undefined : normalizeCost(input.cost);
305
+ if (cost !== undefined && (kind !== "activity.observed"
306
+ || (cost.semantics === "request" && input.activityId === undefined))) {
307
+ throw new Error("Cost requires activity.observed and a stable request activityId.");
308
+ }
299
309
  if (kind === "turn.failed" && input.failure === undefined) {
300
310
  throw new Error("run.failed requires normalized failure evidence.");
301
311
  }
@@ -376,6 +386,7 @@ function normalizePayload(kind, input) {
376
386
  ...input.usage,
377
387
  semantics: input.usage.semantics ?? "cumulative-session"
378
388
  }) }),
389
+ ...(cost === undefined ? {} : { cost }),
379
390
  ...(observerSource === undefined ? {} : { observerSource }),
380
391
  ...(input.sourceId === undefined
381
392
  ? {}
@@ -434,6 +445,43 @@ function normalizePayload(kind, input) {
434
445
  ...(input.goalTokenBudget === undefined ? {} : { goalTokenBudget: input.goalTokenBudget })
435
446
  });
436
447
  }
448
+ function normalizeCost(input) {
449
+ if (input === null || typeof input !== "object"
450
+ || !["actual", "estimated"].includes(input.kind)
451
+ || !["request", "cumulative-session"].includes(input.semantics)) {
452
+ throw new Error("Runtime cost kind or semantics is invalid.");
453
+ }
454
+ if (!Number.isFinite(input.amount) || input.amount < 0 || input.amount > Number.MAX_SAFE_INTEGER) {
455
+ throw new Error("Runtime cost amount must be finite and non-negative.");
456
+ }
457
+ if (typeof input.currency !== "string" || !/^[A-Z]{3}$/u.test(input.currency)) {
458
+ throw new Error("Runtime cost currency requires a three-letter currency code.");
459
+ }
460
+ if (input.kind === "estimated" && input.basis === undefined) {
461
+ throw new Error("Estimated cost requires a price basis.");
462
+ }
463
+ const basis = input.basis;
464
+ if (basis !== undefined && (!Array.isArray(basis.excluded) || basis.excluded.length > 16)) {
465
+ throw new Error("Runtime cost basis excluded items must be a bounded list.");
466
+ }
467
+ return Object.freeze({
468
+ kind: input.kind, semantics: input.semantics, amount: input.amount,
469
+ currency: input.currency, source: costText(input.source, "Cost source"),
470
+ ...(basis === undefined ? {} : { basis: Object.freeze({
471
+ model: costText(basis.model, "Cost model"),
472
+ source: costText(basis.source, "Price source"),
473
+ version: costText(basis.version, "Price version or date"),
474
+ scope: costText(basis.scope, "Estimate scope"),
475
+ excluded: Object.freeze(basis.excluded.map(value => costText(value, "Excluded cost", 128)))
476
+ }) })
477
+ });
478
+ }
479
+ function costText(value, label, limit = 512) {
480
+ const text = requireText(value, label);
481
+ if (text.length > limit)
482
+ throw new Error(`${label} exceeds ${limit} characters.`);
483
+ return text;
484
+ }
437
485
  export function runtimeObservationSemanticKey(input) {
438
486
  const fence = input.fence;
439
487
  const continuationIdentity = [
@@ -13,6 +13,8 @@ import { unsupportedAgentRunConfiguration } from "./agentRunConfiguration.js";
13
13
  import { codexAppServerErrorIsMissing, CodexAppServerRequestError, CodexAppServerRuntime, codexClientInitialization, codexGoalNotification, codexTurnInput, codexTurnOutput } from "./codexAppServerRuntime.js";
14
14
  import { JsonLineChannel, PROVIDER_MESSAGE_MAX_BYTES, terminateProcessGroup } from "./jsonLineChannel.js";
15
15
  import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
16
+ import { foldPublicReply } from "./publicReply.js";
17
+ import { createCodexNativeAccess } from "./codexNativeAccess.js";
16
18
  const CODEX_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000;
17
19
  /**
18
20
  * These Session implementations do not expose run-configuration observations.
@@ -297,7 +299,7 @@ class CodexProxyWebSocketChannel {
297
299
  return;
298
300
  }
299
301
  const id = requestId(message.id);
300
- const pending = id === undefined ? undefined : this.#pending.get(id);
302
+ const pending = id === undefined || message.method !== undefined ? undefined : this.#pending.get(id);
301
303
  if (pending !== undefined) {
302
304
  clearTimeout(pending.timer);
303
305
  this.#pending.delete(id);
@@ -382,6 +384,7 @@ class CodexStructuredProviderSession {
382
384
  onActivity;
383
385
  mirror;
384
386
  nativeAccountHome;
387
+ nativeAccess;
385
388
  #turnAttempts = new Map();
386
389
  adapterId = "codex";
387
390
  #activeTurnId;
@@ -391,6 +394,7 @@ class CodexStructuredProviderSession {
391
394
  #bufferedStarts = [];
392
395
  #bufferedTerminals = [];
393
396
  #bufferedActivities = [];
397
+ publicReply;
394
398
  constructor(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, nativeAccountHome) {
395
399
  this.child = child;
396
400
  this.exit = exit;
@@ -412,13 +416,16 @@ class CodexStructuredProviderSession {
412
416
  await channel.notify("initialized");
413
417
  const runtime = new CodexAppServerRuntime(channel);
414
418
  let conversationId;
419
+ let nativeConfiguration;
415
420
  let resumedActiveTurnId;
416
421
  let resumedTurns = [];
417
422
  if (control.mode === "new") {
418
- conversationId = (await runtime.openConversation({
423
+ const opened = await runtime.openConversation({
419
424
  cwd: payload.cwd,
420
425
  ...control.codexThread
421
- })).conversationId;
426
+ });
427
+ conversationId = opened.conversationId;
428
+ nativeConfiguration = opened.raw;
422
429
  }
423
430
  else {
424
431
  try {
@@ -427,6 +434,7 @@ class CodexStructuredProviderSession {
427
434
  ...control.codexThread
428
435
  });
429
436
  conversationId = resumed.threadId;
437
+ nativeConfiguration = resumed.raw;
430
438
  resumedActiveTurnId = resumed.activeTurnId;
431
439
  resumedTurns = resumed.turns;
432
440
  }
@@ -456,6 +464,7 @@ class CodexStructuredProviderSession {
456
464
  }
457
465
  }
458
466
  const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime, onStarted, onTerminal, onGoal, onInput, onActivity, mirror, optionalId(initialized.codexHome));
467
+ session.nativeAccess = createCodexNativeAccess(conversationId, initialized, channel, openingMessages, nativeConfiguration);
459
468
  session.#activeTurnId = resumedActiveTurnId;
460
469
  const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
461
470
  session.#clientOwnedTurnId = ownedTurn?.turnId;
@@ -514,6 +523,8 @@ class CodexStructuredProviderSession {
514
523
  const method = typeof message.method === "string" ? message.method : "";
515
524
  const params = object(message.params) ?? {};
516
525
  const threadId = optionalId(params.threadId);
526
+ if (emit)
527
+ this.publicReply = foldPublicReply(this.publicReply, this.conversationId, method, params);
517
528
  if (threadId !== this.conversationId) {
518
529
  if (threadId === undefined && (method === "turn/started" || method === "turn/completed")) {
519
530
  this.mirror("stderr", `Codex protocol: ${method} has no thread identity; ignored.\n`);
@@ -630,11 +641,9 @@ class CodexStructuredProviderSession {
630
641
  get activeTurnId() {
631
642
  return this.#activeTurnId;
632
643
  }
633
- /**
634
- * This implementation has no run-configuration observation reader.
635
- */
644
+ /** Last model confirmation from native start/resume, with its observation time. */
636
645
  get runConfiguration() {
637
- return CODEX_RUN_CONFIGURATION;
646
+ return this.nativeAccess?.runConfiguration ?? CODEX_RUN_CONFIGURATION;
638
647
  }
639
648
  async submitTurn(turn) {
640
649
  if (this.#activeTurnId !== undefined) {
@@ -0,0 +1,133 @@
1
+ /** No price catalog, currency conversion, token-to-money guess, or billing claim. */
2
+ export function projectTaskCosts(observations, inScope, workItem, missingEvidence = []) {
3
+ const main = observations.filter(o => o.fence.nativeSessionId !== undefined
4
+ && o.fence.continuationId === undefined && o.fence.parentContinuationId === undefined
5
+ && (o.authority === "provider-structured" || o.authority === "driver-inferred"));
6
+ const resource = (o) => JSON.stringify([
7
+ o.fence.driverId, o.fence.nativeSessionId
8
+ ]);
9
+ const sessions = new Map();
10
+ for (const o of main)
11
+ sessions.set(resource(o), [...(sessions.get(resource(o)) ?? []), o]);
12
+ const project = (kind) => {
13
+ const evidence = [];
14
+ const reasons = [...missingEvidence];
15
+ if (observations.some(o => inScope(o) && o.payload.cost?.kind === kind
16
+ && (o.fence.continuationId !== undefined || o.fence.parentContinuationId !== undefined))) {
17
+ reasons.push("child-usage-overlap-unproven");
18
+ }
19
+ for (const all of sessions.values()) {
20
+ if (!all.some(inScope))
21
+ continue;
22
+ const costs = all.filter(o => o.payload.cost?.kind === kind);
23
+ if (costs.length === 0) {
24
+ reasons.push("cost-unobserved");
25
+ continue;
26
+ }
27
+ // Different native Roles and sources cannot safely claim independent charges.
28
+ const overlap = new Set(all.map(o => JSON.stringify([o.fence.roleName, o.fence.agentId]))).size > 1;
29
+ const latest = costs.at(-1);
30
+ const cost = latest.payload.cost;
31
+ // Missing receipts can carry only a coverage boundary, with no cost.
32
+ const local = qualityReasons(all.filter(o => inScope(o)
33
+ && (o.payload.cost === undefined || o.payload.cost.kind === kind)));
34
+ const semantics = new Set(costs.map(o => o.payload.cost.semantics));
35
+ if (overlap || semantics.size > 1) {
36
+ evidence.push(detail(latest, null, [...local,
37
+ overlap ? "native-session-role-overlap" : "cost-source-overlap"]));
38
+ continue;
39
+ }
40
+ if (cost.semantics === "cumulative-session") {
41
+ let value = null;
42
+ if (new Set(costs.map(o => JSON.stringify([o.payload.cost.source, o.payload.cost.currency]))).size > 1) {
43
+ local.push("cost-source-overlap");
44
+ }
45
+ else if (workItem)
46
+ local.push("cumulative-not-allocatable-to-work-item");
47
+ else if (costs.some((o, i) => i > 0
48
+ && o.payload.cost.amount < costs[i - 1].payload.cost.amount)) {
49
+ local.push("cost-counter-rollback");
50
+ }
51
+ else if (new Set(costs.map(o => JSON.stringify(o.payload.cost.basis))).size > 1) {
52
+ local.push("cost-basis-changed");
53
+ }
54
+ else {
55
+ const baseline = costs[0].payload.cost.amount;
56
+ if (baseline !== 0)
57
+ local.push("nonzero-session-baseline");
58
+ if (baseline === 0 || costs.length > 1)
59
+ value = cost.amount - baseline;
60
+ }
61
+ evidence.push(detail(latest, value, [...local, ...qualityReasons(costs)]));
62
+ continue;
63
+ }
64
+ // Currency and price basis can change in a request revision. Resolve
65
+ // identity and attribution over every version before monetary grouping.
66
+ const requests = new Map();
67
+ for (const o of costs) {
68
+ const id = o.payload.activityId;
69
+ requests.set(id, [...(requests.get(id) ?? []), o]);
70
+ }
71
+ const bases = new Map();
72
+ for (const request of requests.values()) {
73
+ if (!request.some(inScope))
74
+ continue;
75
+ const selected = request.at(-1);
76
+ const conflict = new Set(request.map(o => o.payload.cost.source)).size > 1
77
+ ? "cost-source-overlap"
78
+ : workItem && (!request.every(inScope) || new Set(request.map(o => o.fence.runId)).size !== 1)
79
+ ? "request-run-attribution-conflict" : undefined;
80
+ if (conflict !== undefined) {
81
+ evidence.push(detail(selected, null, [...local, conflict]));
82
+ continue;
83
+ }
84
+ const selectedCost = selected.payload.cost;
85
+ const key = JSON.stringify([selectedCost.currency, selectedCost.source, selectedCost.basis]);
86
+ bases.set(key, [...(bases.get(key) ?? []), selected]);
87
+ }
88
+ for (const group of bases.values()) {
89
+ const subtotal = group.reduce((sum, o) => sum + o.payload.cost.amount, 0);
90
+ evidence.push(detail(group.at(-1), safeAmount(subtotal), [...local, ...qualityReasons(group)]));
91
+ }
92
+ }
93
+ if (evidence.length === 0)
94
+ reasons.push(kind === "estimated" ? "price-unavailable" : "cost-unobserved");
95
+ reasons.push(...evidence.flatMap(e => e.amount.reasons));
96
+ const amounts = [...new Set(evidence.map(e => e.currency))].sort().flatMap(currency => {
97
+ const known = evidence.filter(e => e.currency === currency && e.amount.value !== null);
98
+ return known.length === 0 ? [] : [{
99
+ ...metric(safeAmount(known.reduce((sum, e) => sum + e.amount.value, 0)), reasons), currency
100
+ }];
101
+ });
102
+ return {
103
+ status: amounts.length === 0 ? "unknown" : reasons.length ? "partial" : "known",
104
+ amounts, reasons: [...new Set(reasons)].sort(), evidence
105
+ };
106
+ };
107
+ return { scope: "observed-sources-only", actual: project("actual"), estimated: project("estimated") };
108
+ }
109
+ function qualityReasons(observations) {
110
+ return [
111
+ ...(observations.some(o => o.payload.observationQuality === "partial"
112
+ || o.payload.observationQuality === "unavailable") ? ["source-coverage-partial"] : []),
113
+ ...(observations.some(o => (o.payload.cost?.basis?.excluded.length ?? 0) > 0)
114
+ ? ["estimate-exclusions"] : [])
115
+ ];
116
+ }
117
+ function detail(o, value, reasons) {
118
+ const cost = o.payload.cost;
119
+ return {
120
+ nativeSessionId: o.fence.nativeSessionId, roleName: o.fence.roleName,
121
+ agentId: o.fence.agentId, driverId: o.fence.driverId,
122
+ source: cost.source, currency: cost.currency, semantics: cost.semantics,
123
+ ...(cost.basis === undefined ? {} : { basis: cost.basis }),
124
+ amount: metric(value, reasons)
125
+ };
126
+ }
127
+ function safeAmount(value) {
128
+ return Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER ? value : null;
129
+ }
130
+ function metric(value, reasons) {
131
+ return { value, status: value === null ? "unknown" : reasons.length ? "partial" : "known",
132
+ reasons: [...new Set(reasons)].sort() };
133
+ }
@@ -1,8 +1,9 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
- import { lstatSync, mkdirSync, readFileSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
3
- import { dirname, isAbsolute, join, relative, resolve } from "node:path";
2
+ import { lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
5
5
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
6
+ import { scanProcessPathRefs } from "../resources/liveReferences.js";
6
7
  export const YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR = "YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR";
7
8
  export const YUI_TASK_RUNTIME_SERVICE_NAMESPACE = "YUI_TASK_RUNTIME_SERVICE_NAMESPACE";
8
9
  const MARKER_FILE = ".yui-task-runtime-owner.json";
@@ -55,6 +56,10 @@ export class FileTaskRuntimeIsolation {
55
56
  const { descriptor, fingerprint } = validatePreparation(preparation);
56
57
  const root = descriptor.roots.runtime;
57
58
  const existing = inspectRuntimeRoot(descriptor, fingerprint);
59
+ const claim = existing.find(resource => resource.id !== root);
60
+ if (claim !== undefined) {
61
+ throw new Error(`Task runtime has an unresolved cleanup claim: ${claim.id}.`);
62
+ }
58
63
  if (existing.length > 0) {
59
64
  const [resource] = existing;
60
65
  if (resource?.ownership !== "owned"
@@ -97,16 +102,24 @@ export class FileTaskRuntimeIsolation {
97
102
  cleanup(preparation, reason) {
98
103
  requireCleanupReason(reason);
99
104
  const { descriptor, fingerprint } = validatePreparation(preparation);
100
- const resources = [
101
- ...inspectRuntimeRoot(descriptor, fingerprint),
102
- ...(this.#inspectResources?.(descriptor) ?? [])
103
- ];
104
- if (resources.length === 0)
105
+ const resources = inspectTaskRuntimeCleanup(preparation, reason, this.#inspectResources?.(descriptor) ?? []);
106
+ if (resources.length === 0) {
107
+ this.#resourceRegistrar().markTaskRuntimeDeleted(descriptor);
105
108
  return;
106
- planTaskRuntimeCleanup(descriptor, reason, resources);
109
+ }
110
+ // Claims keep the original descriptor and owner marker. An interrupted
111
+ // rename is not absence: resume only exact, inactive claims and leave any
112
+ // partial/unmarked deletion discoverable through the original owner.
113
+ for (const claimed of runtimeCleanupClaims(descriptor.roots.runtime)) {
114
+ removeRuntimeClaim(descriptor, fingerprint, claimed);
115
+ }
107
116
  // Re-read the sole durable marker immediately before deletion. A missing,
108
117
  // replaced, symlinked, or mismatched runtime is never cleanup authority.
109
118
  const current = inspectRuntimeRoot(descriptor, fingerprint);
119
+ if (current.length === 0) {
120
+ this.#resourceRegistrar().markTaskRuntimeDeleted(descriptor);
121
+ return;
122
+ }
110
123
  if (current.length !== 1
111
124
  || current[0]?.ownership !== "owned"
112
125
  || current[0].descriptorFingerprint !== fingerprint) {
@@ -116,18 +129,7 @@ export class FileTaskRuntimeIsolation {
116
129
  const claimed = `${root}.cleanup-${randomBytes(16).toString("hex")}`;
117
130
  renameSync(root, claimed);
118
131
  try {
119
- const marker = parseMarker(readFileSync(join(claimed, MARKER_FILE), "utf8"));
120
- if (marker.fingerprint !== fingerprint
121
- || JSON.stringify(marker.descriptor) !== JSON.stringify(descriptor)) {
122
- throw new Error("Task runtime resources changed during cleanup claim.");
123
- }
124
- rmSync(claimed, { recursive: true });
125
- this.#resourceRegistrar().markPathsDeleted([
126
- descriptor.roots.runtime,
127
- descriptor.roots.data,
128
- descriptor.roots.cache,
129
- descriptor.roots.temporary
130
- ]);
132
+ removeRuntimeClaim(descriptor, fingerprint, claimed);
131
133
  }
132
134
  catch (error) {
133
135
  // Preserve a claimed-but-unverified resource. Restore its exact path only
@@ -140,6 +142,13 @@ export class FileTaskRuntimeIsolation {
140
142
  }
141
143
  throw error;
142
144
  }
145
+ if (inspectRuntimeRoot(descriptor, fingerprint).length !== 0) {
146
+ throw new Error("Task runtime resources changed during cleanup.");
147
+ }
148
+ // Receipt persistence is separate from the filesystem claim. If it fails,
149
+ // retain the caller's owner and retry using the same validated descriptor;
150
+ // do not try to restore an already removed directory.
151
+ this.#resourceRegistrar().markTaskRuntimeDeleted(descriptor);
143
152
  }
144
153
  }
145
154
  export function createTaskRuntimeIsolationDescriptor(input) {
@@ -329,8 +338,69 @@ export function planTaskRuntimeCleanup(descriptor, reason, resources) {
329
338
  }
330
339
  return Object.freeze([...ids].sort());
331
340
  }
341
+ /** Read-only cleanup admission, shared by resource preflight and deletion.
342
+ * Deletion still repeats ownership/live checks after claiming each directory.
343
+ */
344
+ export function inspectTaskRuntimeCleanup(preparation, reason, additionalResources = []) {
345
+ const { descriptor, fingerprint } = validatePreparation(preparation);
346
+ const resources = [...inspectRuntimeRoot(descriptor, fingerprint), ...additionalResources];
347
+ planTaskRuntimeCleanup(descriptor, reason, resources);
348
+ const paths = resources.filter(resource => resource.kind === "directory").map(resource => resource.id);
349
+ if (paths.length === 0)
350
+ return resources;
351
+ const scan = scanProcessPathRefs(paths);
352
+ if (scan.diagnostics.some(diagnostic => diagnostic.severity === "error")
353
+ || [...scan.refs.values()].some(refs => refs.length > 0)) {
354
+ throw new Error("Task runtime cleanup resources are live or unverified.");
355
+ }
356
+ return resources;
357
+ }
332
358
  function inspectRuntimeRoot(descriptor, expectedFingerprint) {
333
- const root = descriptor.roots.runtime;
359
+ return [
360
+ ...inspectRuntimeDirectory(descriptor, expectedFingerprint, descriptor.roots.runtime),
361
+ ...inspectTaskRuntimeCleanupClaims(descriptor)
362
+ ];
363
+ }
364
+ /** Read-only inventory shared by execution admission and physical cleanup.
365
+ * The descriptor remains the original identity after a cleanup rename.
366
+ */
367
+ export function inspectTaskRuntimeCleanupClaims(descriptor) {
368
+ const fingerprint = taskRuntimeIsolationFingerprint(descriptor);
369
+ return runtimeCleanupClaims(descriptor.roots.runtime)
370
+ .flatMap(root => inspectRuntimeDirectory(descriptor, fingerprint, root));
371
+ }
372
+ function runtimeCleanupClaims(root) {
373
+ const parent = dirname(root);
374
+ try {
375
+ // Names select candidates, never deletion authority. Include malformed or
376
+ // partially removed claims too so absence cannot produce a false receipt.
377
+ return readdirSync(parent).filter(name => name.startsWith(`${basename(root)}.cleanup-`))
378
+ .sort().map(name => join(parent, name));
379
+ }
380
+ catch (error) {
381
+ if (isNodeCode(error, "ENOENT"))
382
+ return [];
383
+ throw error;
384
+ }
385
+ }
386
+ function removeRuntimeClaim(descriptor, fingerprint, claimed) {
387
+ const current = inspectRuntimeDirectory(descriptor, fingerprint, claimed);
388
+ if (current.length !== 1 || current[0]?.ownership !== "owned") {
389
+ throw new Error(`Task runtime cleanup claim is not exactly owned: ${claimed}.`);
390
+ }
391
+ // A process cwd/fd follows rename. Inspect the physical claim, not only the
392
+ // descriptor's original (now missing) runtime path.
393
+ const scan = scanProcessPathRefs([claimed]);
394
+ if (scan.diagnostics.some(diagnostic => diagnostic.severity === "error")
395
+ || [...scan.refs.values()].some(refs => refs.length > 0)) {
396
+ throw new Error(`Task runtime cleanup claim is live or unverified: ${claimed}.`);
397
+ }
398
+ if (inspectRuntimeDirectory(descriptor, fingerprint, claimed)[0]?.ownership !== "owned") {
399
+ throw new Error(`Task runtime cleanup claim changed during inspection: ${claimed}.`);
400
+ }
401
+ rmSync(claimed, { recursive: true });
402
+ }
403
+ function inspectRuntimeDirectory(descriptor, expectedFingerprint, root) {
334
404
  let metadata;
335
405
  try {
336
406
  metadata = lstatSync(root);
@@ -1,3 +1,4 @@
1
+ import { projectTaskCosts } from "./taskCostMetrics.js";
1
2
  import { builtinDriverIdForAdapter } from "./builtinAgentDrivers.js";
2
3
  import { isRuntimeTokenEvidence, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
3
4
  import { projectSessionTokenObservations, unobservedSessionTokenMetrics } from "./sessionTokenMetrics.js";
@@ -39,7 +40,7 @@ export function projectTaskUsageMetrics(input) {
39
40
  continue;
40
41
  const resource = resourceKey(identity);
41
42
  const owners = resourceOwners.get(resource) ?? new Set();
42
- owners.add(identity.roleName);
43
+ owners.add(JSON.stringify([identity.roleName, identity.agentId]));
43
44
  resourceOwners.set(resource, owners);
44
45
  }
45
46
  const sessions = [...buckets.values()]
@@ -107,14 +108,21 @@ export function projectTaskUsageMetrics(input) {
107
108
  }];
108
109
  });
109
110
  const reasons = [];
111
+ const costReasons = [];
110
112
  if (scoped.some((observation) => sessionIdentity(observation) === null))
111
113
  reasons.push("session-identity-missing");
114
+ if (reasons.length > 0)
115
+ costReasons.push("session-identity-missing");
112
116
  for (const run of runs.values()) {
113
117
  if (input.workItemId !== undefined && run.workItemId !== input.workItemId)
114
118
  continue;
115
119
  if (!observations.some((observation) => exactRun(observation, runs)?.id === run.id
116
120
  && usageAuthority(observation) && isRuntimeTokenEvidence(observation)))
117
121
  reasons.push("run-usage-unobserved");
122
+ if (!observations.some((observation) => exactRun(observation, runs)?.id === run.id
123
+ && usageAuthority(observation) && !child(observation) && observation.payload.cost !== undefined)) {
124
+ costReasons.push("run-cost-unobserved");
125
+ }
118
126
  }
119
127
  const tokenValues = sessions.flatMap(({ tokens }) => tokens.value === null ? [] : [tokens.value]);
120
128
  const sum = safeSum(tokenValues);
@@ -149,6 +157,7 @@ export function projectTaskUsageMetrics(input) {
149
157
  elapsedSeconds: input.workItemId === undefined
150
158
  ? taskElapsed(input.task, events, now) : metric(null, ["work-item-lifecycle-not-task-elapsed"]),
151
159
  executionSeconds: executionDuration(scoped, now),
160
+ costs: projectTaskCosts(observations, inScope, input.workItemId !== undefined, costReasons),
152
161
  coverage: {
153
162
  basis: "task-fenced-observations",
154
163
  observedSessions: sessions.length,
@@ -199,7 +208,7 @@ function identityKey(identity) {
199
208
  return JSON.stringify([identity.taskId, identity.roleName, identity.agentId, identity.driverId, identity.nativeSessionId]);
200
209
  }
201
210
  function resourceKey(identity) {
202
- return JSON.stringify([identity.agentId, identity.driverId, identity.nativeSessionId]);
211
+ return JSON.stringify([identity.driverId, identity.nativeSessionId]);
203
212
  }
204
213
  function child({ fence }) {
205
214
  return fence.continuationId !== undefined || fence.parentContinuationId !== undefined;
@@ -0,0 +1,38 @@
1
+ import { projectTaskUsageMetrics } from "./taskUsageMetrics.js";
2
+ export const TASK_USAGE_FACT_LIMIT = 2000;
3
+ /** Same bounded, read-only fact query for the Task CLI and Web. */
4
+ export function readTaskUsage(store, taskId, options = {}) {
5
+ const task = store.getTask(taskId);
6
+ if (task === null)
7
+ return null;
8
+ const facts = store.readTaskUsageFacts(taskId);
9
+ return pageTaskUsage(projectTaskUsageMetrics({ task, ...facts, now: options.now }), facts.complete, options);
10
+ }
11
+ export function pageTaskUsage(input, complete = true, options = {}) {
12
+ const offset = options.offset ?? 0, limit = options.limit ?? 0;
13
+ if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 0 || limit > 50) {
14
+ throw new Error("Usage detail offset must be non-negative; limit must be 0..50.");
15
+ }
16
+ const bounded = (metric) => complete ? metric : {
17
+ ...metric, status: metric.value === null ? "unknown" : "partial",
18
+ reasons: [...new Set([...metric.reasons, "usage-history-limited"])]
19
+ };
20
+ const cost = (value) => ({
21
+ ...value,
22
+ status: complete || value.status === "unknown" ? value.status : "partial",
23
+ reasons: complete ? value.reasons : [...new Set([...value.reasons, "usage-history-limited"])],
24
+ amounts: value.amounts.map(amount => ({ ...bounded(amount), currency: amount.currency })),
25
+ evidence: value.evidence.slice(offset, offset + limit)
26
+ });
27
+ const total = Math.max(input.sessions.length, input.costs.actual.evidence.length, input.costs.estimated.evidence.length);
28
+ return {
29
+ ...input, tokens: bounded(input.tokens), toolCalls: bounded(input.toolCalls),
30
+ executionSeconds: bounded(input.executionSeconds),
31
+ costs: { scope: input.costs.scope, actual: cost(input.costs.actual), estimated: cost(input.costs.estimated) },
32
+ sessions: input.sessions.slice(offset, offset + limit),
33
+ details: { offset, limit, sessionTotal: input.sessions.length,
34
+ actualTotal: input.costs.actual.evidence.length, estimatedTotal: input.costs.estimated.evidence.length,
35
+ nextOffset: offset + limit < total ? offset + limit : null },
36
+ history: { complete, limit: TASK_USAGE_FACT_LIMIT }
37
+ };
38
+ }
@@ -36,6 +36,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
36
36
  });
37
37
  return projectTaskExecution({
38
38
  task,
39
+ usageFacts: store.readTaskUsageFacts(taskId),
39
40
  roles,
40
41
  runs,
41
42
  usageRuns,
@@ -110,6 +111,7 @@ export function projectTaskExecution(facts) {
110
111
  workItems,
111
112
  executionGroups: observabilityGroups,
112
113
  runs: facts.usageRuns ?? runs,
114
+ usageFacts: facts.usageFacts,
113
115
  events,
114
116
  contextSnapshots: facts.contextSnapshots,
115
117
  sessionTokens,
@@ -1,4 +1,5 @@
1
1
  import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
2
+ import { pageTaskUsage } from "../runtime/taskUsageQuery.js";
2
3
  /**
3
4
  * Build the read-only DAG, execution, cost, and context view consumed by CLI
4
5
  * and Web. It deliberately derives every value from existing Task records and
@@ -145,8 +146,15 @@ function projectCost(input, groups, workItemId) {
145
146
  const groupIds = new Set(uniqueGroups.map(({ id }) => id));
146
147
  const attempts = input.runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
147
148
  const laneCount = uniqueGroups.reduce((total, group) => total + group.lanes.length, 0);
149
+ const usage = projectTaskUsageMetrics({
150
+ ...input,
151
+ ...(input.usageFacts === undefined ? {} : {
152
+ events: input.usageFacts.events, runs: input.usageFacts.runs
153
+ }),
154
+ workItemId
155
+ });
148
156
  return Object.freeze({
149
- ...projectTaskUsageMetrics({ ...input, workItemId }),
157
+ ...(input.usageFacts === undefined ? usage : pageTaskUsage(usage, input.usageFacts.complete)),
150
158
  laneCount,
151
159
  groupCount: uniqueGroups.length,
152
160
  retryCount: Math.max(0, attempts.length - laneCount),
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute, join, relative, resolve } from "node:path";
2
2
  import { createHash } from "node:crypto";
3
+ import { statSync } from "node:fs";
3
4
  /**
4
5
  * The single authority for every Yui self-managed path derived from a Home.
5
6
  *
@@ -64,6 +65,18 @@ export function integrationTmuxSocketRoot(home) {
64
65
  const homeDigest = createHash("sha256").update(homeRoot(home)).digest("hex").slice(0, 16);
65
66
  return join("/tmp", `yi-${uid.toString(36)}-${homeDigest}`);
66
67
  }
68
+ /** Short IPC endpoint for one foreground HTTP Job. Its durable owner and
69
+ * receipts stay in the Job artifact directory; only the socket lives here. */
70
+ export function jobPreviewSocketRoot(artifactDir) {
71
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
72
+ const identity = statSync(artifactDir);
73
+ if (!identity.isDirectory())
74
+ throw new Error("Preview Job artifacts must be a directory.");
75
+ const owner = createHash("sha256").update(JSON.stringify([
76
+ resolve(artifactDir), identity.dev, identity.ino
77
+ ])).digest("hex").slice(0, 24);
78
+ return join("/tmp", `yp-${uid.toString(36)}-${owner}`);
79
+ }
67
80
  /**
68
81
  * Disposable staging prefix for `yui update`'s side-by-side package install
69
82
  * (an upgrade artifact): `<home>/runtime/update-staging`. Kept under Home so a