@zq-silk/yui 2.5.0 → 2.7.1

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 (104) hide show
  1. package/README.md +23 -0
  2. package/dist/cli/commandCatalog.js +31 -4
  3. package/dist/commands/globalRoleCommands.js +22 -2
  4. package/dist/commands/inboxCommands.js +352 -0
  5. package/dist/commands/roleSkillValidation.js +2 -0
  6. package/dist/commands/taskCommands.js +40 -7
  7. package/dist/commands/taskContextCommand.js +3 -2
  8. package/dist/context/currentSkills.js +48 -0
  9. package/dist/context/roleSessionContext.js +13 -16
  10. package/dist/context/runContextPack.js +2 -0
  11. package/dist/context/runInputContract.js +1 -1
  12. package/dist/context/sessionBootstrapManifest.js +27 -4
  13. package/dist/context/skillCatalog.js +2 -3
  14. package/dist/context/taskContext.js +9 -4
  15. package/dist/controlPlaneCli.js +18 -1
  16. package/dist/controller/controller.js +3 -2
  17. package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
  18. package/dist/controller/globalInputDelivery.js +73 -15
  19. package/dist/controller/globalRuntimeAttention.js +1 -1
  20. package/dist/controller/operatorNotification.js +6 -1
  21. package/dist/controller/providerRetryDelivery.js +3 -5
  22. package/dist/controller/runtime.js +2 -2
  23. package/dist/doctor/doctor.js +25 -2
  24. package/dist/executor/effectiveLaunch.js +2 -2
  25. package/dist/executor/fileRoleLaunchPlanner.js +4 -6
  26. package/dist/interaction/notificationPresentation.js +2 -2
  27. package/dist/interaction/operatorPresentation.js +1 -1
  28. package/dist/kernel/builtinCapabilities.js +2 -1
  29. package/dist/message/globalInputBacklog.js +69 -0
  30. package/dist/message/globalMessageQuery.js +1 -0
  31. package/dist/message/inboxReceipt.js +32 -0
  32. package/dist/message/message.js +6 -1
  33. package/dist/message/messageContinuation.js +34 -13
  34. package/dist/message/taskInboxOwnership.js +21 -0
  35. package/dist/nativeAgent/session/backends.js +1 -1
  36. package/dist/observability/runtimeIdentity.js +1 -1
  37. package/dist/resources/sqliteResourceRegistry.js +1 -1
  38. package/dist/runtime/agentHost.js +105 -16
  39. package/dist/runtime/builtinAgentErrorMappers.js +9 -5
  40. package/dist/runtime/providerRetry.js +38 -0
  41. package/dist/runtime/taskSessions.js +240 -0
  42. package/dist/scheduler/operatorEvent.js +49 -0
  43. package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
  44. package/dist/scheduler/taskExecutionProjection.js +78 -6
  45. package/dist/scheduler/taskWake.js +5 -0
  46. package/dist/sqlite/driver.js +108 -0
  47. package/dist/storage/sqliteSchema.js +11 -1
  48. package/dist/storage/sqliteStore.js +60 -1
  49. package/dist/storage/storageSchema.js +1 -1
  50. package/dist/storage/storageVersions.js +1 -1
  51. package/dist/storage/storeRpc.js +1 -0
  52. package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
  53. package/dist/task/completionReadiness.js +11 -4
  54. package/dist/telemetry/sqliteTelemetryStore.js +1 -1
  55. package/dist/web/assets/assetManifest.js +2 -0
  56. package/dist/web/assets/client/app/taskView.js +1 -0
  57. package/dist/web/assets/client/domain/records.js +70 -2
  58. package/dist/web/assets/client/domain/runs.js +3 -2
  59. package/dist/web/assets/client/domain/vocab.js +1 -1
  60. package/dist/web/assets/client/i18n/en.js +17 -1
  61. package/dist/web/assets/client/i18n/zh-CN.js +17 -1
  62. package/dist/web/assets/client/lib/api.js +4 -1
  63. package/dist/web/assets/client/lib/theme.js +31 -10
  64. package/dist/web/assets/client/views/task/actions.js +38 -21
  65. package/dist/web/assets/client/views/task/observation.js +12 -3
  66. package/dist/web/assets/client/views/task/overview.js +7 -40
  67. package/dist/web/assets/client/views/task/runtime.js +71 -34
  68. package/dist/web/assets/shared/icons.js +2 -0
  69. package/dist/web/assets/shell/index.js +1 -0
  70. package/dist/web/assets/shell/settings.js +1 -0
  71. package/dist/web/assets/styles/views/task.js +14 -0
  72. package/dist/web/webServer.js +12 -0
  73. package/dist/web/webSessions.js +2 -165
  74. package/dist/web/webSnapshot.js +9 -5
  75. package/dist/web/webTaskActivity.js +54 -0
  76. package/dist/web/webTaskSurface.js +1 -0
  77. package/docs/managed-turn-and-session-runtime.md +81 -5
  78. package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
  79. package/docs/observability/README.md +25 -1
  80. package/docs/observability/README.zh-CN.md +18 -1
  81. package/docs/provider-retry.md +11 -0
  82. package/docs/release-workflow.md +74 -2
  83. package/docs/release-workflow.zh-CN.md +56 -1
  84. package/docs/storage-baseline.md +17 -7
  85. package/docs/storage-baseline.zh-CN.md +11 -5
  86. package/docs/task-delivery.md +17 -8
  87. package/docs/task-delivery.zh-CN.md +11 -5
  88. package/docs/testing/verification-levels.md +22 -4
  89. package/docs/testing/verification-levels.zh-CN.md +8 -1
  90. package/i18n/README.zh-CN.md +19 -0
  91. package/package.json +5 -2
  92. package/skills/yui-leader/SKILL.md +28 -11
  93. package/skills/yui-leader/VERSION +1 -0
  94. package/skills/yui-operator/SKILL.md +10 -2
  95. package/skills/yui-operator/VERSION +1 -0
  96. package/skills/yui-operator/references/task-delivery.md +2 -0
  97. package/skills/yui-reviewer/SKILL.md +8 -1
  98. package/skills/yui-reviewer/VERSION +1 -0
  99. package/skills/yui-runtime/SKILL.md +94 -11
  100. package/skills/yui-runtime/VERSION +1 -0
  101. package/skills/yui-runtime/references/publication.md +16 -5
  102. package/skills/yui-runtime/references/recovery.md +9 -0
  103. package/skills/yui-worker/SKILL.md +8 -1
  104. package/skills/yui-worker/VERSION +1 -0
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { taskMessageInboxReceipt } from "../message/taskInboxOwnership.js";
3
4
  import { agentExecutionComponentLabel } from "../agent/executionComponents.js";
4
5
  import { assertLeaderArchiveAdmission } from "../task/leaderArchiveAuthority.js";
5
6
  import { createRun, runExecutionObservation, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
@@ -45,7 +46,7 @@ import { copyGlobalRoleToTaskRole, activeRoleAgentBinding, createRole, createRol
45
46
  import { roleLaunchEventPayload, saveTaskRoleUpdate } from "../role/taskRoleUpdate.js";
46
47
  import { RUNTIME_SESSION_REPLACE_REQUIRED_REASON, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
47
48
  import { currentManagedRuntime, resolveManagedTaskReader } from "../runtime/managedCaller.js";
48
- import { controlProviderRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
49
+ import { controlProviderRetry, providerRunRetry, providerRetryProjection, renderProviderRetry } from "../runtime/providerRetry.js";
49
50
  import { currentProviderConversation, transferProviderAuthority } from "../runtime/providerRuntimeIdentity.js";
50
51
  import { projectProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
51
52
  import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
@@ -1631,9 +1632,25 @@ function taskMessageCommand(args, store, options) {
1631
1632
  const message = store.transaction((tx) => {
1632
1633
  assertTaskDeliveryAuthority(tx, options.environment, ref.taskId);
1633
1634
  const current = tx.listMessages(ref.taskId).find((entry) => entry.id === ref.localId);
1634
- if (current?.recipient?.ownerRunId === undefined || current.continuation?.runId !== undefined) {
1635
+ if (current?.inbox?.ack !== undefined || current?.recipient?.ownerRunId === undefined || current.continuation?.runId !== undefined) {
1635
1636
  throw usageError("Only an unassigned pending Message can be explicitly handed off.");
1636
1637
  }
1638
+ if (current.inbox !== undefined) {
1639
+ const actor = current.inbox.claim.actor;
1640
+ for (const runId of new Set([actor.runId, current.recipient.ownerRunId])) {
1641
+ const handler = runId === undefined ? null : tx.getRun(ref.taskId, runId);
1642
+ const observation = handler === null ? undefined : runExecutionObservation(handler, tx.getTaskRoleSessionSet(ref.taskId, handler.roleName)?.providerBinding, tx.listEvents(ref.taskId));
1643
+ if (handler === null || handler.status === "active"
1644
+ || handler.result?.failureReason === "delivery-unknown"
1645
+ || handler.result?.provider === undefined
1646
+ || runId === actor.runId && handler.result.provider.conversationId !== actor.nativeSessionId
1647
+ || !["completed", "failed", "cancelled"].includes(observation?.delivery ?? "")) {
1648
+ throw usageError("The original and current inbox handlers must have exact stopped native execution before explicit handoff.");
1649
+ }
1650
+ }
1651
+ // Preserve the original claim: the new authorized Assignment must
1652
+ // inspect prior effects and ack explicitly, never auto-replay the input.
1653
+ }
1637
1654
  const recipient = resolveMessageRecipient(tx, ref.taskId, requiredOption(parsed.options, "--to"), {
1638
1655
  ...(current.recipient.workItemId === undefined ? {} : { workItemId: current.recipient.workItemId }),
1639
1656
  ...(current.recipient.reviewRoundId === undefined ? {} : { reviewRoundId: current.recipient.reviewRoundId })
@@ -1853,15 +1870,23 @@ function steerTaskInput(taskId, input, store, options) {
1853
1870
  // and idempotent, so the Host's later settlement fold promotes it to a proven
1854
1871
  // terminal and never rewinds it. An idempotent replay above already returned
1855
1872
  // the saved Message, so this records the attempt exactly once.
1856
- store.transaction((tx) => {
1873
+ const admitted = store.transaction((tx) => {
1857
1874
  const saved = tx.listMessages(result.task.id).find((entry) => entry.id === result.message.id);
1858
1875
  const requestId = saved?.inputControl?.requestId;
1859
- if (saved === undefined || requestId === undefined)
1860
- return;
1876
+ // The recipient may claim between Message persistence and control
1877
+ // admission. A Leader window owns its referenced originals even after ack;
1878
+ // never re-submit them through a delayed steer.
1879
+ if (saved === undefined || requestId === undefined || taskMessageInboxReceipt(tx, saved) !== undefined)
1880
+ return false;
1861
1881
  tx.updateMessage(result.task.id, recordTaskMessageControlOutcome(saved, {
1862
1882
  requestId, receiptId, outcome: "pending", observedAt: clock(options)
1863
1883
  }));
1884
+ return true;
1864
1885
  });
1886
+ if (!admitted) {
1887
+ return output(`Steer message ${result.message.id} saved but not delivered (TARGET_CHANGED: inbox handling owns the input).\n`, { taskId: result.task.id, message: messageReceipt(result.message),
1888
+ steer: { state: "not-steered", code: "TARGET_CHANGED", detail: "Inbox handling already owns the original input." } });
1889
+ }
1865
1890
  return {
1866
1891
  kind: "input-steer",
1867
1892
  taskId: result.task.id,
@@ -2407,6 +2432,9 @@ function registerInterruptThen(store, taskId, roleName, thenMessageRef, target,
2407
2432
  }
2408
2433
  // A Message that already delivered, or was already ruled undeliverable, is not
2409
2434
  // a fresh input to reuse; reusing it would replay or resurrect a settled fact.
2435
+ if (taskMessageInboxReceipt(store, message) !== undefined) {
2436
+ return { code: "TARGET_CHANGED", detail: `Message ${thenMessageRef} is owned by active inbox handling.` };
2437
+ }
2410
2438
  if (message.continuation?.runId !== undefined) {
2411
2439
  return { code: "TARGET_CHANGED", detail: `Message ${thenMessageRef} was already delivered by ${message.continuation.runId}.` };
2412
2440
  }
@@ -4897,7 +4925,9 @@ function retryRun(args, store, options) {
4897
4925
  return output(`Retry already recorded as ${existing.payload.runId}; retry that exact successor if needed.\n`);
4898
4926
  }
4899
4927
  const sessions = tx.getTaskRoleSessionSet(previous.taskId, previous.roleName);
4900
- const retry = sessions?.providerBinding?.retry;
4928
+ const retry = options.providerRetryChainId !== undefined ? sessions?.providerBinding?.retry
4929
+ : sessions?.providerBinding == null ? undefined
4930
+ : providerRunRetry(sessions.providerBinding, previous.id, clock(options).getTime());
4901
4931
  if (options.providerRetryChainId !== undefined
4902
4932
  && (retry?.chainId !== options.providerRetryChainId || retry.status !== "waiting"
4903
4933
  || retry.previousRunId !== previous.id || clock(options).getTime() < Date.parse(retry.nextEligibleAt)
@@ -4909,7 +4939,10 @@ function retryRun(args, store, options) {
4909
4939
  const successor = tx.getActiveRun(previous.taskId, previous.roleName);
4910
4940
  tx.saveTaskRoleSessionSet({
4911
4941
  ...sessions, providerBinding: {
4912
- ...sessions.providerBinding, retry: {
4942
+ ...sessions.providerBinding,
4943
+ ...(sessions.providerBinding.retry !== undefined && sessions.providerBinding.retry.chainId !== retry.chainId
4944
+ ? { retryHistory: [...(sessions.providerBinding.retryHistory ?? []), sessions.providerBinding.retry] } : {}),
4945
+ retry: {
4913
4946
  ...retry, status: "waiting", successorAutomatic: options.providerRetryChainId !== undefined,
4914
4947
  ...(options.providerRetryChainId === undefined ? {
4915
4948
  intentAt: clock(options).toISOString(),
@@ -17,12 +17,12 @@ export function runTaskContextCommand(args, store, environment = {}) {
17
17
  const action = explicit ? args[0] : "read";
18
18
  const taskId = args[explicit ? 1 : 0];
19
19
  if (!taskId || taskId.startsWith("--"))
20
- throw usageError("Task context usage: yui task context [read|list|delta|inspect] <task> [--after <cursor|timestamp>] [--continuation <cursor>] [--cursor <cursor>] [--limit <1..100>] [--store <store> --ref <id> --digest <digest>] [--status <status>] [--work-item <id>].");
20
+ throw usageError("Task context usage: yui task context [read|list|delta|inspect] <task> [--after <cursor|timestamp>] [--continuation <cursor>] [--cursor <cursor>] [--limit <1..100>] [--store <store> --ref <id> --digest <digest>] [--view <current|all>] [--status <status>] [--work-item <id>].");
21
21
  const options = new Map();
22
22
  const rest = args.slice(explicit ? 2 : 1);
23
23
  const allowed = action === "read" ? [] : action === "delta"
24
24
  ? ["--after", "--continuation", "--limit"] : action === "list"
25
- ? ["--store", "--cursor", "--limit", "--status", "--after", "--work-item"] : ["--store", "--ref", "--digest", "--cursor"];
25
+ ? ["--store", "--cursor", "--limit", "--status", "--after", "--work-item", "--view"] : ["--store", "--ref", "--digest", "--cursor"];
26
26
  for (let i = 0; i < rest.length; i += 2) {
27
27
  const flag = rest[i];
28
28
  const value = rest[i + 1];
@@ -40,6 +40,7 @@ export function runTaskContextCommand(args, store, environment = {}) {
40
40
  throw usageError("Context list requires --store.");
41
41
  data = listTaskContext(store, taskId, family, {
42
42
  cursor: options.get("--cursor"),
43
+ view: options.get("--view"),
43
44
  status: options.get("--status"), after: options.get("--after"), workItemId: options.get("--work-item"),
44
45
  ...(options.has("--limit") ? { limit: Number(options.get("--limit")) } : {})
45
46
  }, environment);
@@ -0,0 +1,48 @@
1
+ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { join, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const BUILTIN_YUI_SKILLS = new Set([
5
+ "yui-runtime", "yui-operator", "yui-leader", "yui-worker", "yui-reviewer"
6
+ ]);
7
+ export function currentSkillPath(home, id) {
8
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(id))
9
+ throw new Error(`Invalid configured Skill id: ${id}.`);
10
+ if (BUILTIN_YUI_SKILLS.has(id))
11
+ return fileURLToPath(new URL(`../../skills/${id}`, import.meta.url));
12
+ if (home === undefined)
13
+ throw new Error("YUI_HOME is required to load configured Role skills.");
14
+ return resolve(home, "skills", id);
15
+ }
16
+ /** Per-turn metadata only: never read the body, traverse resources, snapshot a
17
+ * package, or claim that the model still has instructions in its context. */
18
+ export function checkCurrentSkills(home, ids) {
19
+ return [...new Set(ids)].map(id => {
20
+ const path = currentSkillPath(home, id);
21
+ const versionPath = join(path, "VERSION");
22
+ try {
23
+ for (const file of [join(path, "SKILL.md"), versionPath]) {
24
+ if (realpathSync(file) !== resolve(file) || !lstatSync(file).isFile()) {
25
+ throw new Error(`Skill files must be regular files without symlinks: ${file}.`);
26
+ }
27
+ accessSync(file, constants.R_OK);
28
+ }
29
+ if (lstatSync(versionPath).size > 128)
30
+ throw new Error("VERSION exceeds 128 bytes.");
31
+ const version = readFileSync(versionPath, "utf8").trim();
32
+ if (!/^[1-9][0-9]{0,8}$/u.test(version))
33
+ throw new Error("VERSION must contain a positive internal integer.");
34
+ return { id, path, version, versionPath };
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Current Skill ${id} is missing, unreadable or invalid at ${path}. `
38
+ + `${error instanceof Error ? error.message : String(error)} `
39
+ + "Restore the current installation/configured Skill (including VERSION) in this Home, then check again; "
40
+ + "do not replace the Session or replay input to repair Skill loading.", { cause: error });
41
+ }
42
+ });
43
+ }
44
+ export const SKILL_CHECK_COMMAND = "yui session skills --json";
45
+ export const SKILL_REUSE_RULE = "At the start of every turn, check current Skill versions and availability. "
46
+ + "Read full SKILL.md and necessary references on first entry, version change, recovery, "
47
+ + "or missing/compacted/uncertain context; reuse only unchanged full instructions available now. "
48
+ + "A version check is not a loading receipt or authority.";
@@ -3,29 +3,18 @@ import { readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { snapshotSkillPackage, verifySkillPackage } from "./skillPackage.js";
6
+ import { BUILTIN_YUI_SKILLS, checkCurrentSkills } from "./currentSkills.js";
7
+ export { BUILTIN_YUI_SKILLS } from "./currentSkills.js";
6
8
  import { SYSTEM_LEADER_ROLE, SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
7
- export const BUILTIN_YUI_SKILLS = new Set([
8
- "yui-runtime",
9
- "yui-operator",
10
- "yui-leader",
11
- "yui-worker",
12
- "yui-reviewer"
13
- ]);
14
9
  /**
15
10
  * Compiles stable Role policy for an Agent-native instruction channel.
16
11
  * Dynamic wakeups and AgentRun assignments deliberately remain outside this value.
17
12
  */
18
13
  export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
19
14
  const kind = roleSessionKind(role, owner, options.purpose ?? "execution");
20
- const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
21
- const skillIds = unique([
22
- "yui-runtime",
23
- ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
24
- ...(role.skills ?? [])
25
- ]);
26
- const skills = options.skillPackages === undefined
27
- ? loadYuiSkillContexts(yuiHome, skillIds)
28
- : options.skillPackages.map(loadSkillPackageContext);
15
+ const skills = checkCurrentSkills(yuiHome, roleSkillIds(role, kind)).map(skill => ({
16
+ ...skill, content: readFileSync(join(skill.path, "SKILL.md"), "utf8").trim()
17
+ }));
29
18
  return {
30
19
  developerInstructions: renderDeveloperInstructions(kind, role, owner),
31
20
  skills,
@@ -34,6 +23,14 @@ export function compileRoleSessionContext(yuiHome, role, owner, options = {}) {
34
23
  : { managedContextFile: roleContextFile(yuiHome, role, owner, kind) })
35
24
  };
36
25
  }
26
+ export function roleSkillIds(role, kind) {
27
+ const builtInSkillId = kind === "global" ? undefined : `yui-${kind}`;
28
+ return unique([
29
+ "yui-runtime",
30
+ ...(builtInSkillId === undefined ? [] : [builtInSkillId]),
31
+ ...(role.skills ?? [])
32
+ ]);
33
+ }
37
34
  function renderDeveloperInstructions(kind, role, owner) {
38
35
  const core = renderRoleCore(kind, role, owner);
39
36
  const profile = [
@@ -344,6 +344,8 @@ export function buildRunContextPack(store, taskId, runId) {
344
344
  const preliminaryBytes = Buffer.byteLength(JSON.stringify({ ...body, digest }), "utf8");
345
345
  const pack = Object.freeze({
346
346
  ...body,
347
+ // Current loading guidance is not part of the frozen Assignment digest.
348
+ skillCheckCommand: "yui session skills --json",
347
349
  inventory: Object.freeze({
348
350
  refs: pointers.length,
349
351
  frozenBytes: preliminaryBytes,
@@ -79,7 +79,7 @@ export function serializeRunInputEnvelope(value) {
79
79
  .join(",");
80
80
  const snapshot = normalized.contextSnapshotRef;
81
81
  const lines = [
82
- "Yui AgentRun input. Follow the Session Manifest and injected Skills.",
82
+ "Yui AgentRun input. Check current Skills: yui session skills --json. Follow the Session Manifest profile and current Skills.",
83
83
  `${normalized.subject.taskId === undefined ? "" : `task=${normalized.subject.taskId} `}run=${normalized.runId} role=${normalized.roleName}`,
84
84
  `source=${normalized.source.type}/${normalized.source.channel} purpose=${normalized.purpose} subject=${subject || "global"} snapshot=${snapshot === undefined ? "none" : `${snapshot.id}@${snapshot.digest}`}`,
85
85
  normalized.deltaRefIds.length === 0
@@ -3,6 +3,7 @@ import { chmodSync, readFileSync } from "node:fs";
3
3
  import { join, resolve } from "node:path";
4
4
  import { writeTextFileAtomically } from "../storage/durableFile.js";
5
5
  import { validateSkillPackageRef } from "./skillPackage.js";
6
+ import { checkCurrentSkills, SKILL_CHECK_COMMAND, SKILL_REUSE_RULE } from "./currentSkills.js";
6
7
  import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
7
8
  export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
8
9
  /** Portable read pointers accompany every managed input, including the first
@@ -20,9 +21,22 @@ export function withSessionContextPointer(text, environment) {
20
21
  /** No remembered/sent flag can prove instructions survived context compaction. */
21
22
  export function sessionContextInstructions(manifestPath, digest) {
22
23
  return `Yui Session Manifest: ${manifestPath}${digest === undefined ? "" : ` (digest ${digest})`}. `
23
- + "Apply yui-runtime and its Role Skills/profile by exact manifest paths. "
24
- + "Load on first entry, version change or missing context; reuse only same-version instructions "
25
- + "fully available now, never infer availability from prior reads.";
24
+ + `Before work on every turn run the Manifest's contextProtocol.skillCheckCommand `
25
+ + `(older Manifests: ${SKILL_CHECK_COMMAND}; Global loadCommand also checks current Skills). `
26
+ + "Use its current stable Skill paths instead of historical frozen-package paths; preserve the Manifest's Role profile. "
27
+ + SKILL_REUSE_RULE;
28
+ }
29
+ /** The caller must already pass the ordinary managed Session/CLI/Home checks.
30
+ * Old manifests remain immutable audit records, while this read resolves their
31
+ * Skill IDs against the currently executing installation and Home. */
32
+ export function checkSessionSkills(home, manifestPath) {
33
+ const manifest = readSessionBootstrapManifest(manifestPath);
34
+ return {
35
+ skills: checkCurrentSkills(home, manifest.skills.map(skill => skill.id)),
36
+ roleProfileRef: manifest.roleProfileRef,
37
+ rule: SKILL_REUSE_RULE,
38
+ historicalPackagesAreAuditOnly: true
39
+ };
26
40
  }
27
41
  /** Read back one immutable Session Manifest and verify its content digest. */
28
42
  export function readSessionBootstrapManifest(path) {
@@ -76,6 +90,10 @@ export function readSessionBootstrapManifest(path) {
76
90
  requireText(entry.id, "Session Manifest Skill id");
77
91
  requireText(entry.path, "Session Manifest Skill path");
78
92
  requireDigest(entry.digest, "Session Manifest Skill digest");
93
+ if (entry.version !== undefined) {
94
+ requireText(entry.version, "Session Manifest Skill version");
95
+ requireText(entry.versionPath, "Session Manifest Skill version path");
96
+ }
79
97
  if (entry.package !== undefined) {
80
98
  const ref = entry.package;
81
99
  validateSkillPackageRef(ref);
@@ -121,6 +139,7 @@ export function materializeSessionBootstrap(input) {
121
139
  id: skill.id,
122
140
  path: skill.path,
123
141
  digest: skill.package?.digest ?? digest(skill.content),
142
+ ...(skill.version === undefined ? {} : { version: skill.version, versionPath: skill.versionPath }),
124
143
  ...(skill.package === undefined ? {} : { package: skill.package })
125
144
  })),
126
145
  roleProfileRef: { digest: profileDigest, path: roleProfilePath },
@@ -129,11 +148,15 @@ export function materializeSessionBootstrap(input) {
129
148
  // A Global Session may move between Yui's remote TUI and Desktop.
130
149
  // Carry its read entry in the Thread-visible Manifest instead of
131
150
  // depending on the client process that happened to create it.
132
- loadCommand: renderGlobalContextCommand(input.entryPoint, home, input.role.name)
151
+ loadCommand: renderGlobalContextCommand(input.entryPoint, home, input.role.name),
152
+ // Same self-contained read remains usable from Desktop without the
153
+ // launching shell's managed environment or PATH.
154
+ skillCheckCommand: renderGlobalContextCommand(input.entryPoint, home, input.role.name)
133
155
  }
134
156
  : {
135
157
  loadCommand: `yui task run context ${quoteShellWord(`${input.owner.taskId}/<run-id>`)} --json`,
136
158
  expandCommand: `yui task run context expand ${quoteShellWord(`${input.owner.taskId}/<run-id>`)} <ref-id> --store <store> --mode full --json`,
159
+ skillCheckCommand: SKILL_CHECK_COMMAND,
137
160
  ...(input.role.name === "leader" ? {
138
161
  currentCommand: `yui task context ${quoteShellWord(input.owner.taskId)} --json`
139
162
  } : {})
@@ -1,9 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { dirname, join, relative, resolve, sep } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
4
  import { verifySkillPackage } from "./skillPackage.js";
6
- import { BUILTIN_YUI_SKILLS } from "./roleSessionContext.js";
5
+ import { BUILTIN_YUI_SKILLS, currentSkillPath } from "./currentSkills.js";
7
6
  const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
8
7
  const MAX_TEXT = 256 * 1024;
9
8
  const digest = (data) => createHash("sha256").update(data).digest("hex");
@@ -13,7 +12,7 @@ export function skillSource(home, id) {
13
12
  throw new Error("Invalid Skill id.");
14
13
  const builtin = BUILTIN_YUI_SKILLS.has(id);
15
14
  return { kind: builtin ? "builtin" : "configured",
16
- path: builtin ? fileURLToPath(new URL(`../../skills/${id}`, import.meta.url)) : resolve(home, "skills", id) };
15
+ path: currentSkillPath(home, id) };
17
16
  }
18
17
  function physical(path) {
19
18
  if (realpathSync(path) !== resolve(path))
@@ -67,6 +67,8 @@ export function readTaskContext(store, taskId, environment = {}) {
67
67
  discovery: `task context list ${taskId} --store <store>`,
68
68
  commandDiscovery: {
69
69
  help: "yui help", describe: "yui --json help",
70
+ skillCheck: "yui session skills --json",
71
+ ownInbox: "yui inbox list --json",
70
72
  note: "Help and completion share the current caller's offline command view. Follow that view for operation-specific discovery; execution always revalidates authority."
71
73
  },
72
74
  detail: `task context inspect ${taskId} --store <store> --ref <refId> --digest <digest>`,
@@ -191,7 +193,10 @@ export function readTaskContextResource(store, taskId, selector, environment = {
191
193
  export function listTaskContext(store, taskId, family, options = {}, environment = {}) {
192
194
  return store.readTransaction(reader => {
193
195
  const scope = authorizeContext(reader, taskId, environment);
194
- const sources = authorizedSources(reader, scope, false, family);
196
+ if (options.view !== undefined && !["current", "all"].includes(options.view)) {
197
+ throw usageError("Context list view must be current or all.");
198
+ }
199
+ const sources = authorizedSources(reader, scope, options.view === "current", family, true);
195
200
  if (!CONTEXT_FAMILIES.includes(family))
196
201
  throw usageError(`Unknown Context store: ${family}.`);
197
202
  if (options.after !== undefined && !Number.isFinite(Date.parse(options.after)))
@@ -216,7 +221,7 @@ export function listTaskContext(store, taskId, family, options = {}, environment
216
221
  }
217
222
  }
218
223
  return recordPage(records, `task:${taskId}/${family}/${JSON.stringify({
219
- status: options.status, after: options.after, workItemId: options.workItemId
224
+ view: options.view ?? "all", status: options.status, after: options.after, workItemId: options.workItemId
220
225
  })}`, options);
221
226
  });
222
227
  }
@@ -447,7 +452,7 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
447
452
  default: return null;
448
453
  }
449
454
  }
450
- function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily) {
455
+ function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily, discovery = false) {
451
456
  const taskId = task.id;
452
457
  const sources = [];
453
458
  const add = (family, id, value) => {
@@ -462,7 +467,7 @@ function authorizedSources(store, { task, allow, caller }, current = false, requ
462
467
  const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
463
468
  sources.push({ family, count, *read(offset = 0) {
464
469
  for (const { id, value } of store.queryContextRecords(taskId, {
465
- ...selection, offset, limit: current ? 8 : MAX_RECORDS
470
+ ...selection, offset, limit: current && !discovery ? 8 : MAX_RECORDS
466
471
  }).records)
467
472
  yield materialize(family, id, value);
468
473
  } });
@@ -38,6 +38,7 @@ import { runDurableJobCommand } from "./commands/durableJobCommands.js";
38
38
  import { parseExecutionAuditOptions, runExecutionAuditCommand } from "./commands/executionAuditCommands.js";
39
39
  import { runGlobalRoleCommand, previewGlobalRoleAgentConfigurationMutation } from "./commands/globalRoleCommands.js";
40
40
  import { runJobCommand } from "./commands/jobCommands.js";
41
+ import { runInboxCommand } from "./commands/inboxCommands.js";
41
42
  import { applyOperatorSessionControl, runOperatorCommand } from "./commands/operatorCommands.js";
42
43
  import { previewProfileAgentConfigurationMutation, runProfileCommand } from "./commands/profileCommands.js";
43
44
  import { runProjectCommand } from "./commands/projectCommands.js";
@@ -61,7 +62,7 @@ import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
61
62
  import { FileCompletionManager } from "./completion/fileCompletionManager.js";
62
63
  import { CONFIG_DOMAINS } from "./config/configCatalog.js";
63
64
  import { resolveTmuxBin, resolveTmuxHistoryLimit } from "./config/yuiConfig.js";
64
- import { readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
65
+ import { checkSessionSkills, readSessionBootstrapManifest } from "./context/sessionBootstrapManifest.js";
65
66
  import { assertFileTaskControllerStorageCompatible, ensureFileTaskController, FileTaskWorkflowRuntime, refreshRunningFileTaskControllerConfiguration, refreshRunningFileTaskControllerEnvironment, restartFileTaskController, stopFileTaskController } from "./controller/clientRuntime.js";
66
67
  import { FileSchedulerStoreAdapter } from "./controller/fileSchedulerStoreAdapter.js";
67
68
  import { createControllerIntegrationJobPort } from "./controller/jobClient.js";
@@ -632,6 +633,11 @@ export async function main() {
632
633
  return;
633
634
  }
634
635
  assertTaskInvocationScope(resolved, process.env);
636
+ if (resolved[0] === "inbox") {
637
+ const result = runInboxCommand(resolved.slice(1), store, process.env);
638
+ emit(JSON.stringify(result, null, 2), false, result);
639
+ return;
640
+ }
635
641
  if (resolved[0] === "task" && resolved[1] === "role" && resolved[2] === "capabilities") {
636
642
  const query = parseTaskAgentCapabilityQuery(resolved.slice(3));
637
643
  const result = await callController(home, "task.role-capabilities", {
@@ -789,6 +795,17 @@ export async function main() {
789
795
  return;
790
796
  }
791
797
  if (resolved[0] === "session") {
798
+ if (resolved[1] === "skills") {
799
+ if (resolved.length !== 2)
800
+ throw usageError("Usage: yui session skills --json");
801
+ const path = process.env.YUI_SESSION_MANIFEST;
802
+ if (!["task", "global"].includes(process.env.YUI_SESSION_SCOPE ?? "") || path === undefined) {
803
+ throw usageError("Skill checks require the current managed Session and its Manifest.");
804
+ }
805
+ const data = checkSessionSkills(home, path);
806
+ emit(JSON.stringify(data, null, 2), false, data);
807
+ return;
808
+ }
792
809
  if (resolved[1] === "stop") {
793
810
  const options = parseSessionStopOptions(resolved.slice(2));
794
811
  const result = await runSessionStopCommand({
@@ -996,7 +996,7 @@ export class FileTaskController {
996
996
  }
997
997
  const firstRuntimeDrain = await this.#drainRuntimeEvents();
998
998
  await this.#providerRetry?.reconcile();
999
- await this.#globalInputDelivery?.();
999
+ await this.#globalInputDelivery?.({ reconcile: scope.kind === "full", now: this.#now() });
1000
1000
  for (const taskId of runtimeTaskFailureIds(firstRuntimeDrain)) {
1001
1001
  runtimeFailedTaskIds.add(taskId);
1002
1002
  }
@@ -1076,7 +1076,8 @@ export class FileTaskController {
1076
1076
  if (scope.kind === "full"
1077
1077
  || scope.keys.some((key) => key !== "operator")
1078
1078
  || runtimeAdvanced) {
1079
- this.#signalOperatorMailbox(runtimeAdvanced);
1079
+ this.#signalOperatorMailbox(runtimeAdvanced || scope.kind === "full"
1080
+ || scope.kind === "dirty" && scope.keys.some(key => parseMailboxKey(key).kind === "global-role"));
1080
1081
  }
1081
1082
  }
1082
1083
  catch (error) {
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { taskMessageInboxReceipt, unfinishedLeaderInbox } from "../message/taskInboxOwnership.js";
3
4
  import { contextSnapshotRef } from "../context/contextSnapshot.js";
4
5
  import { freezeRunContextSnapshot } from "../context/runContextPack.js";
5
6
  import { taskNotificationPresentation } from "../interaction/notificationPresentation.js";
@@ -30,6 +31,7 @@ import { standardAgentError } from "../runtime/agentError.js";
30
31
  import { recordTaskAgentError } from "./taskAgentError.js";
31
32
  import { recordGlobalRuntimeAttention, recordGlobalRuntimeFailure } from "./globalRuntimeAttention.js";
32
33
  import { queueOperatorNotification } from "./operatorNotification.js";
34
+ import { claimOperatorAttention } from "../scheduler/operatorEvent.js";
33
35
  import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
34
36
  import { RUNTIME_CLEANUP_REQUIRED_REASON, RUNTIME_HOST_DETACH_REQUIRED_REASON, RUNTIME_LIFECYCLE_OWNER, hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, isRuntimeCleanupReason, runtimeCleanupDisposition, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
35
37
  import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
@@ -882,6 +884,11 @@ export class FileSchedulerStoreAdapter {
882
884
  return [...this.store.listPendingActivationRequestTaskIds()].sort((left, right) => (left.localeCompare(right, undefined, { numeric: true })));
883
885
  }
884
886
  getTask(taskId) { return this.store.getTask(taskId); }
887
+ getEvent(taskId, eventId) {
888
+ return this.store.queryContextRecords(taskId, {
889
+ family: "task-event", ids: [eventId], limit: 1
890
+ }).records[0]?.value ?? null;
891
+ }
885
892
  getTaskWorkspace(taskId) { return this.store.getTaskWorkspace(taskId); }
886
893
  getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
887
894
  listDecisions(taskId) { return this.store.listDecisions(taskId); }
@@ -1146,7 +1153,8 @@ export class FileSchedulerStoreAdapter {
1146
1153
  throw new AgentHostProviderSessionBusyError("The next Global input belongs to an explicit then handoff.");
1147
1154
  }
1148
1155
  if (message !== undefined) {
1149
- if (message.delivery !== undefined || message.notDelivered !== undefined
1156
+ if (store.queryGlobalRoleMessages(input.roleName, { handling: true, limit: 0 }).count > 0
1157
+ || message.inbox !== undefined || message.delivery !== undefined || message.notDelivered !== undefined
1150
1158
  || message.control !== undefined && message.control.outcome !== "rejected"
1151
1159
  || message.deliveryTarget?.agentId !== input.agentId
1152
1160
  || message.deliveryTarget?.nativeSessionId !== input.nativeSessionId) {
@@ -1567,7 +1575,7 @@ export class FileSchedulerStoreAdapter {
1567
1575
  if (mailbox.processing !== null) {
1568
1576
  return { status: "processing", processing: mailbox.processing };
1569
1577
  }
1570
- let claimed = claimPending(mailbox, {
1578
+ let claimed = (input.target.kind === "operator" ? claimOperatorAttention : claimPending)(mailbox, {
1571
1579
  batchId: input.batchId,
1572
1580
  owner: input.owner,
1573
1581
  startedAt: input.now.toISOString()
@@ -1737,7 +1745,7 @@ export class FileSchedulerStoreAdapter {
1737
1745
  clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
1738
1746
  getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
1739
1747
  prepareMessageContinuations(taskId, now) {
1740
- const roles = new Set(this.store.listMessages(taskId).flatMap((message) => message.recipient?.ownerRunId !== undefined && message.continuation?.runId === undefined ? [message.recipient.roleName] : []));
1748
+ const roles = new Set(this.store.listMessages(taskId).flatMap((message) => message.inbox === undefined && message.recipient?.ownerRunId !== undefined && message.continuation?.runId === undefined ? [message.recipient.roleName] : []));
1741
1749
  for (const roleName of roles) {
1742
1750
  try {
1743
1751
  this.store.transaction((store) => prepareMessageContinuations(store, taskId, now, roleName));
@@ -1746,7 +1754,7 @@ export class FileSchedulerStoreAdapter {
1746
1754
  const reason = `preparation-failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 1000);
1747
1755
  this.store.transaction((store) => {
1748
1756
  for (const message of store.listMessages(taskId)) {
1749
- if (message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined && message.continuation?.runId === undefined
1757
+ if (message.inbox === undefined && message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined && message.continuation?.runId === undefined
1750
1758
  && message.continuation?.notDeliveredReason !== reason) {
1751
1759
  store.updateMessage(taskId, { ...message, continuation: { notDeliveredReason: reason } });
1752
1760
  }
@@ -1855,6 +1863,8 @@ export class FileSchedulerStoreAdapter {
1855
1863
  return { wakeId, attemptId: nextAttempt, disposition: "submit",
1856
1864
  presentation: taskNotificationPresentation(wake, store) };
1857
1865
  }
1866
+ if (unfinishedLeaderInbox(store, taskId))
1867
+ return null;
1858
1868
  if (mailbox.pending === null || (provider !== null && provider !== undefined
1859
1869
  && ["submitting", "accepted", "delivery-unknown"].includes(provider.status)))
1860
1870
  return null;
@@ -1864,6 +1874,11 @@ export class FileSchedulerStoreAdapter {
1864
1874
  const sessions = store.getTaskRoleSessionSet(taskId, "leader");
1865
1875
  const session = sessions?.sessions[sessions.activeAgentId];
1866
1876
  const obsoleteIds = new Set();
1877
+ for (const message of store.listMessages(taskId)) {
1878
+ if (pendingMessageIds.has(message.id) && taskMessageInboxReceipt(store, message) !== undefined) {
1879
+ obsoleteIds.add(message.id);
1880
+ }
1881
+ }
1867
1882
  for (const message of store.listMessages(taskId)) {
1868
1883
  if (!pendingMessageIds.has(message.id) || message.inputControl?.expectedSessionId === undefined)
1869
1884
  continue;