@zq-silk/yui 0.15.11 → 0.15.12

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 (101) hide show
  1. package/ARCHITECTURE.md +8 -4
  2. package/ARCHITECTURE.zh-CN.md +5 -2
  3. package/README.md +13 -5
  4. package/dist/agentRun/agentRun.js +3 -0
  5. package/dist/cli/commandCatalog.js +38 -10
  6. package/dist/cli/interactionPolicy.js +4 -0
  7. package/dist/cli/managedDiagnostics.js +1 -1
  8. package/dist/cli.js +46 -15
  9. package/dist/commands/executionAuditCommands.js +10 -0
  10. package/dist/commands/globalRoleCommands.js +27 -2
  11. package/dist/commands/projectCommands.js +44 -15
  12. package/dist/commands/taskCommands.js +95 -33
  13. package/dist/commands/taskIntegrationCommands.js +3 -1
  14. package/dist/commands/taskOverviewCommand.js +4 -3
  15. package/dist/commands/taskPublicationAdoptCommand.js +127 -0
  16. package/dist/commands/taskPublicationCommands.js +11 -2
  17. package/dist/commands/taskPublicationVerifyCommand.js +23 -39
  18. package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
  19. package/dist/context/runContextPack.js +3 -0
  20. package/dist/context/taskCatalog.js +187 -0
  21. package/dist/context/taskContext.js +17 -4
  22. package/dist/controller/controller.js +11 -2
  23. package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
  24. package/dist/controller/globalInputDelivery.js +13 -0
  25. package/dist/controller/providerRetryAdmission.js +100 -0
  26. package/dist/controller/providerRetryDelivery.js +218 -0
  27. package/dist/controller/runtime.js +36 -1
  28. package/dist/integration/gitIntegrationService.js +19 -6
  29. package/dist/lifecycle/exactRunTerminalization.js +4 -1
  30. package/dist/message/globalProviderRetry.js +15 -0
  31. package/dist/observability/executionAudit.js +19 -0
  32. package/dist/repository/gitWorkspace.js +371 -105
  33. package/dist/repository/projectMaintenanceLock.js +75 -18
  34. package/dist/repository/taskWorkspaceCoordinator.js +71 -124
  35. package/dist/repository/taskWorkspacePreparer.js +86 -24
  36. package/dist/repository/workspaceCleanupInspection.js +187 -0
  37. package/dist/runtime/agentError.js +5 -3
  38. package/dist/runtime/agentHost.js +28 -11
  39. package/dist/runtime/builtinAgentErrorMappers.js +91 -0
  40. package/dist/runtime/codexAppServerRuntime.js +34 -3
  41. package/dist/runtime/providerControl.js +5 -1
  42. package/dist/runtime/providerRetry.js +198 -0
  43. package/dist/runtime/providerRuntimeIdentity.js +28 -2
  44. package/dist/runtime/sessionTokenMetrics.js +15 -5
  45. package/dist/runtime/structuredProviderHost.js +6 -2
  46. package/dist/runtime/taskUsageMetrics.js +275 -0
  47. package/dist/scheduler/activeRoleRunDelivery.js +12 -0
  48. package/dist/scheduler/leaderWakeupProcessor.js +5 -0
  49. package/dist/scheduler/taskExecutionProjection.js +26 -5
  50. package/dist/scheduler/taskObservabilityProjection.js +6 -44
  51. package/dist/storage/sqliteSchema.js +31 -0
  52. package/dist/storage/sqliteStore.js +17 -0
  53. package/dist/storage/storageVersions.js +1 -1
  54. package/dist/storage/storeRpc.js +1 -0
  55. package/dist/storage/taskCatalog.js +123 -0
  56. package/dist/storage/taskStore.js +2 -0
  57. package/dist/task/archiveDiagnostics.js +1 -0
  58. package/dist/task/archivePreflight.js +124 -0
  59. package/dist/task/publicationAdoption.js +56 -0
  60. package/dist/task/publicationReference.js +10 -0
  61. package/dist/task/remoteDelivery.js +31 -16
  62. package/dist/web/assets/client/app.js +92 -17
  63. package/dist/web/assets/client/components.js +55 -13
  64. package/dist/web/assets/client/i18n.js +72 -4
  65. package/dist/web/assets/client/taskSurface.js +2 -1
  66. package/dist/web/assets/client/view.js +35 -7
  67. package/dist/web/assets/shell.js +6 -0
  68. package/dist/web/assets/styles/layout.js +7 -0
  69. package/dist/web/webServer.js +14 -3
  70. package/dist/web/webSnapshot.js +12 -3
  71. package/dist/workspace/cleanupInspection.js +63 -0
  72. package/dist/workspace/workItemChangeSetManager.js +110 -50
  73. package/docs/agent-result-consumption.md +4 -0
  74. package/docs/agent-result-consumption.zh-CN.md +3 -0
  75. package/docs/agent-runtime-drivers.md +7 -0
  76. package/docs/agent-runtime-drivers.zh-CN.md +5 -0
  77. package/docs/architecture/README.md +2 -0
  78. package/docs/architecture/README.zh-CN.md +3 -1
  79. package/docs/architecture/capabilities-and-resources.md +30 -5
  80. package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
  81. package/docs/managed-turn-and-session-runtime.md +47 -0
  82. package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
  83. package/docs/observability/README.md +62 -0
  84. package/docs/observability/README.zh-CN.md +47 -0
  85. package/docs/project-refresh.md +77 -0
  86. package/docs/project-refresh.zh-CN.md +59 -0
  87. package/docs/provider-retry.md +70 -0
  88. package/docs/task-delivery.md +133 -13
  89. package/docs/task-delivery.zh-CN.md +99 -10
  90. package/docs/task-discovery.md +102 -0
  91. package/docs/task-discovery.zh-CN.md +86 -0
  92. package/docs/testing/verification-levels.md +16 -0
  93. package/docs/testing/verification-levels.zh-CN.md +13 -1
  94. package/i18n/README.zh-CN.md +13 -7
  95. package/package.json +1 -1
  96. package/skills/yui-leader/references/execution.md +3 -2
  97. package/skills/yui-operator/SKILL.md +13 -2
  98. package/skills/yui-reviewer/SKILL.md +4 -0
  99. package/skills/yui-runtime/SKILL.md +17 -2
  100. package/skills/yui-runtime/references/publication.md +26 -4
  101. package/skills/yui-runtime/references/recovery.md +24 -0
@@ -0,0 +1,63 @@
1
+ import { isAbsolute, join, relative } from "node:path";
2
+ export class CleanupInspectionError extends Error {
3
+ checks;
4
+ constructor(checks) {
5
+ super(checks.map(renderCleanupCheck).join("\n"));
6
+ this.checks = checks;
7
+ this.name = "CleanupInspectionError";
8
+ }
9
+ }
10
+ export function renderCleanupCheck(check) {
11
+ return `[${check.reason}] ${check.resource}: ${check.detail}`
12
+ + ` Expected=${JSON.stringify(check.expected)}; observed=${JSON.stringify(check.observed)}.`
13
+ + (check.actions.length === 0 ? "" : ` Inspect/resolve: ${check.actions.join("; ")}.`);
14
+ }
15
+ /** Do not forward arbitrary Git stderr, native arguments, or foreign paths. */
16
+ export function cleanupCheckFromError(error, resource, sources, actions) {
17
+ if (error instanceof CleanupInspectionError) {
18
+ return error.checks.map(check => ({ ...check, resource, sources, actions }));
19
+ }
20
+ let cause = error;
21
+ let errorCode;
22
+ for (let depth = 0; depth < 4 && cause instanceof Error; depth += 1) {
23
+ const code = cause.code;
24
+ if ((typeof code === "string" && /^[A-Z][A-Z0-9_]{0,63}$/.test(code))
25
+ || (typeof code === "number" && Number.isSafeInteger(code))) {
26
+ errorCode = code;
27
+ break;
28
+ }
29
+ cause = cause.cause;
30
+ }
31
+ return [{ resource, reason: "inspection-unavailable", status: "unknown",
32
+ detail: "The resource could not be inspected; no safe cleanup conclusion is available.",
33
+ expected: "readable owned resource", observed: errorCode === undefined ? "unavailable" : { errorCode }, sources, actions }];
34
+ }
35
+ export function cleanupFailure(reason, detail, expected, observed, status = "blocked") {
36
+ throw new CleanupInspectionError([{ resource: "git-workspace", reason, status,
37
+ detail, expected, observed, sources: [], actions: [] }]);
38
+ }
39
+ /** Describe exact differences without disclosing paths outside this Task.
40
+ * Legacy locations are displayed only for this Task's exact branch identity;
41
+ * displaying a historical location is not accepting it as migration proof.
42
+ */
43
+ export function workspacePathValue(workspace, path) {
44
+ const inside = (root) => {
45
+ const value = relative(root, path);
46
+ return value !== ".." && !value.startsWith("../") && !isAbsolute(value) ? value || "." : undefined;
47
+ };
48
+ const marker = `/workspaces/tasks/${workspace.owner.taskId}/`;
49
+ const index = workspace.root.lastIndexOf(marker);
50
+ if (index >= 0) {
51
+ const home = workspace.root.slice(0, index);
52
+ const taskPath = inside(join(home, "workspaces", "tasks", workspace.owner.taskId));
53
+ if (taskPath !== undefined)
54
+ return `<task>/${taskPath}`;
55
+ const legacyPath = inside(join(home, "workspaces", "worktree"));
56
+ const parts = legacyPath?.split("/");
57
+ const taskSegment = workspace.entries.find(e => e.access === "write")?.branch.split("/")[1];
58
+ if (parts?.length === 3 && parts[1] === taskSegment)
59
+ return `<legacy-worktree>/${legacyPath}`;
60
+ }
61
+ const local = inside(workspace.root);
62
+ return local === undefined ? "[outside authorized Task paths]" : `<owner>/${local}`;
63
+ }
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from "node:util";
2
- import { lstat } from "node:fs/promises";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ import { join } from "node:path";
3
4
  import { createWorkItemChangeSet } from "../integration/changeSet.js";
4
5
  import { createChangeSetManifest } from "../integration/changeSetManifest.js";
5
6
  import { deriveManifestTags } from "../integration/manifestTags.js";
@@ -8,6 +9,7 @@ import { sameTaskFinalReviewContract } from "../review/taskFinalReviewContract.j
8
9
  import { governingWorkItemCandidate } from "../workItem/workItem.js";
9
10
  import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
10
11
  import { captureManagedGitChanges } from "./gitChangeSetCapture.js";
12
+ import { CleanupInspectionError, cleanupCheckFromError, workspacePathValue } from "./cleanupInspection.js";
11
13
  const CAPTURABLE_WORK_ITEM_STATUSES = new Set([
12
14
  "open",
13
15
  "accepted",
@@ -45,6 +47,12 @@ export class WorkItemChangeSetManager {
45
47
  return captured;
46
48
  }
47
49
  async assertIntegrated(taskId, workItemId, candidateId) {
50
+ const inspection = await this.inspectIntegrated(taskId, workItemId, candidateId);
51
+ if (inspection.checks.length > 0)
52
+ throw new CleanupInspectionError(inspection.checks);
53
+ return inspection.proof;
54
+ }
55
+ async inspectIntegrated(taskId, workItemId, candidateId) {
48
56
  const item = this.store.getWorkItem(taskId, workItemId);
49
57
  if (item === null)
50
58
  throw new Error(`Work item not found: ${taskId}/${workItemId}.`);
@@ -54,62 +62,114 @@ export class WorkItemChangeSetManager {
54
62
  throw new Error(`Candidate not found: ${taskId}/${workItemId}/${candidateId}.`);
55
63
  }
56
64
  const workspace = this.store.getWorkItemWorkspace(item.taskId, item.id);
57
- if (workspace === null
58
- || workspace.owner.type !== "work-item"
59
- || workspace.owner.workItemId !== item.id)
60
- return null;
65
+ if (workspace === null)
66
+ return { proof: null, checks: [] };
61
67
  const git = new NodeGitWorkspace();
62
68
  const projects = [];
63
- for (const entry of writableEntries(workspace)) {
64
- const path = await lstat(entry.path).catch(error => {
65
- if (error.code === "ENOENT")
66
- return null;
67
- throw error;
68
- });
69
- if (path?.isSymbolicLink())
70
- throw new Error(`WorkItem path is a symbolic link: ${entry.path}.`);
71
- if (path !== null && !await git.isClean(entry.path)) {
72
- throw new Error(`WorkItem Project workspace is not clean: ${item.id}/${entry.projectId}.`);
69
+ const checks = [];
70
+ const sources = [`work-item:${taskId}/${workItemId}`,
71
+ `candidate:${taskId}/${workItemId}/${candidate?.id ?? "missing"}`, managedWorkspaceKey(workspace.owner)];
72
+ const actions = [`yui task work show ${taskId}/${workItemId}`, `yui task integration list ${taskId}`];
73
+ const add = (resource, reason, detail, expected, observed) => {
74
+ checks.push({ resource, reason, detail, expected, observed, status: "blocked", sources, actions });
75
+ };
76
+ const resource = `work-item:${taskId}/${workItemId}`;
77
+ if (workspace.owner.type !== "work-item" || workspace.owner.taskId !== taskId || workspace.owner.workItemId !== item.id) {
78
+ add(resource, "workspace-identity-mismatch", "Managed workspace is not owned by this WorkItem.", { type: "work-item", taskId, workItemId }, "different owner");
79
+ return { proof: null, checks };
80
+ }
81
+ if (candidate?.workspace === undefined) {
82
+ add(resource, "candidate-workspace-missing", "The governing Candidate has no frozen workspace.", "frozen WorkItem workspace", null);
83
+ }
84
+ else if (!isDeepStrictEqual(candidate.workspace, workspace)) {
85
+ const frozen = candidate.workspace;
86
+ if (!isDeepStrictEqual(frozen.owner, workspace.owner)) {
87
+ add(resource, "workspace-identity-mismatch", "Frozen and current workspace owners differ.", frozen.owner, workspace.owner);
88
+ }
89
+ // Paths remain frozen evidence. A path-shaped difference alone is NOT a
90
+ // receipt proving that migration moved this exact Candidate safely.
91
+ if (frozen.root !== workspace.root) {
92
+ add(resource, "workspace-path-mismatch", "Workspace roots differ; relocation is not proven.", workspacePathValue(workspace, frozen.root), workspacePathValue(workspace, workspace.root));
93
+ }
94
+ if (!isDeepStrictEqual(frozen.entries.map(e => e.projectId), workspace.entries.map(e => e.projectId))) {
95
+ add(resource, "workspace-identity-mismatch", "Frozen and current Project scope/order differ.", frozen.entries.map(e => e.projectId), workspace.entries.map(e => e.projectId));
96
+ }
97
+ for (const entry of workspace.entries) {
98
+ const previous = frozen.entries.find(e => e.projectId === entry.projectId);
99
+ if (previous === undefined)
100
+ continue;
101
+ const entryResource = `${resource}/${entry.projectId}`;
102
+ if (previous.path !== entry.path) {
103
+ add(entryResource, "workspace-path-mismatch", "Frozen and current Project paths differ; no relocation receipt is available. Historical evidence is unchanged.", workspacePathValue(workspace, previous.path), workspacePathValue(workspace, entry.path));
104
+ }
105
+ for (const key of ["directory", "access", "branch", "baseRef", "baseCommit"]) {
106
+ if (previous[key] !== entry[key])
107
+ add(entryResource, "workspace-identity-mismatch", `Frozen and current ${key} differ.`, { [key]: previous[key] }, { [key]: entry[key] });
108
+ }
109
+ }
110
+ for (const key of ["schemaVersion", "createdAt", "updatedAt"]) {
111
+ if (frozen[key] !== workspace[key])
112
+ add(resource, "workspace-metadata-mismatch", `Frozen and current ${key} differ.`, { [key]: frozen[key] }, { [key]: workspace[key] });
73
113
  }
114
+ }
115
+ for (const entry of writableEntries(workspace)) {
116
+ const projectResource = `${resource}/${entry.projectId}`;
74
117
  const resultCommit = candidate?.gitSnapshot?.projects.find(({ projectId }) => projectId === entry.projectId)?.commit;
75
- // Absence is a filesystem fact, not proof of integration or Git cleanup.
76
- // Check any retained branch against the same frozen Candidate; the
77
- // cleanup primitive separately removes its exact Git registration.
78
- const repository = this.store.getTaskWorkspace(taskId)?.entries.find(e => e.projectId === entry.projectId);
79
- const workspaceHeadCommit = path !== null ? (await git.inspect(entry.path, "HEAD")).baseCommit
80
- : repository !== undefined && await git.refExists(repository.path, entry.branch)
81
- ? (await git.inspect(repository.path, entry.branch)).baseCommit
82
- : resultCommit;
83
- if (candidate?.workspace === undefined
84
- || !isDeepStrictEqual(candidate.workspace, workspace)
85
- || resultCommit === undefined
86
- || (candidateId === undefined && resultCommit !== workspaceHeadCommit)) {
87
- throw new Error(`WorkItem Project no longer matches its frozen result: ${item.id}/${entry.projectId}.`);
118
+ if (resultCommit === undefined)
119
+ add(projectResource, "frozen-commit-missing", "The governing Candidate has no frozen Project commit.", "frozen commit", null);
120
+ if (resultCommit !== undefined) {
121
+ // An explicit historical selection is proved against its immutable
122
+ // integrated commit, not mislabeled as the workspace's current HEAD.
123
+ const integrated = this.store.listIntegrationAttempts(item.taskId).some(integration => integration.status === "committed"
124
+ && integration.projectId === entry.projectId
125
+ && integration.source.kind === "work-item"
126
+ && integration.source.workItemId === item.id
127
+ && integration.source.startCommit === entry.baseCommit
128
+ && integration.source.resultCommit === resultCommit);
129
+ if (!integrated) {
130
+ add(projectResource, "result-not-integrated", "WorkItem result is not integrated.", { baseCommit: entry.baseCommit, resultCommit }, "no committed Integration with these exact commits");
131
+ }
132
+ projects.push({ projectId: entry.projectId, baseCommit: entry.baseCommit, headCommit: resultCommit });
88
133
  }
89
- // An explicit historical selection is proved against its immutable
90
- // integrated commit, not mislabeled as the workspace's current HEAD.
91
- const headCommit = resultCommit;
92
- const integrated = this.store.listIntegrationAttempts(item.taskId).some((integration) => (integration.status === "committed"
93
- && integration.projectId === entry.projectId
94
- && integration.source.kind === "work-item"
95
- && integration.source.workItemId === item.id
96
- && integration.source.startCommit === entry.baseCommit
97
- && integration.source.resultCommit === headCommit));
98
- if (!integrated) {
99
- throw new Error(`WorkItem result is not integrated: ${item.id}/${entry.projectId}.`);
134
+ if (entry.path !== join(workspace.root, entry.directory)) {
135
+ add(projectResource, "workspace-path-mismatch", "Current path differs from the exact owner-root/Project location.", workspacePathValue(workspace, join(workspace.root, entry.directory)), workspacePathValue(workspace, entry.path));
136
+ continue;
137
+ }
138
+ try {
139
+ const path = await lstat(entry.path).catch(error => {
140
+ if (error.code === "ENOENT")
141
+ return null;
142
+ throw error;
143
+ });
144
+ if (path !== null && (path.isSymbolicLink() || await realpath(entry.path) !== entry.path)) {
145
+ add(projectResource, "workspace-identity-mismatch", "WorkItem path resolves through a symbolic link.", "real owned directory", "symbolic link");
146
+ continue;
147
+ }
148
+ // Absence is a filesystem fact, not proof of integration or Git cleanup.
149
+ // Check any retained branch against the same frozen Candidate; the
150
+ // cleanup primitive separately removes its exact Git registration.
151
+ const repository = this.store.getTaskWorkspace(taskId)?.entries.find(e => e.projectId === entry.projectId);
152
+ const workspaceHeadCommit = path !== null ? (await git.inspect(entry.path, "HEAD")).baseCommit
153
+ : repository !== undefined && await git.refExists(repository.path, entry.branch)
154
+ ? (await git.inspect(repository.path, entry.branch)).baseCommit
155
+ : resultCommit;
156
+ if (candidateId === undefined && resultCommit !== undefined && resultCommit !== workspaceHeadCommit) {
157
+ add(projectResource, "head-mismatch", "Current HEAD no longer matches the frozen result.", resultCommit, workspaceHeadCommit ?? null);
158
+ }
159
+ if (path !== null && !await git.inspectClean(entry.path)) {
160
+ add(projectResource, "dirty-worktree", "WorkItem Project workspace is not clean.", "clean", "dirty");
161
+ }
162
+ }
163
+ catch (error) {
164
+ checks.push(...cleanupCheckFromError(error, projectResource, sources, actions));
100
165
  }
101
- projects.push({
102
- projectId: entry.projectId,
103
- baseCommit: entry.baseCommit,
104
- headCommit
105
- });
106
166
  }
107
- return {
108
- workItemId: item.id,
109
- ...(item.assignee === undefined ? {} : { assignee: item.assignee }),
110
- workspace,
111
- projects
112
- };
167
+ return { checks, proof: checks.length > 0 ? null : {
168
+ workItemId: item.id,
169
+ ...(item.assignee === undefined ? {} : { assignee: item.assignee }),
170
+ workspace,
171
+ projects
172
+ } };
113
173
  }
114
174
  /**
115
175
  * Fail-closed proof that retiring the aggregate will not hide unrecorded Git
@@ -2,6 +2,10 @@
2
2
 
3
3
  # Agent result consumption
4
4
 
5
+ For candidate discovery before reading original results, use the
6
+ [bounded Task catalog](task-discovery.md). A catalog summary never replaces a
7
+ requirement or the original result.
8
+
5
9
  Every explicitly dispatched AgentRun produces one durable original result.
6
10
  Ordinary notification and native conversation do not implicitly create Runs.
7
11
  The next Agent in the ownership chain reads the exact result and decides what
@@ -2,6 +2,9 @@
2
2
 
3
3
  # Agent 结果消费
4
4
 
5
+ 读取原始结果前,可用[有界 Task 目录](task-discovery.zh-CN.md)发现候选任务。
6
+ 目录摘要不替代需求正文或原始结果。
7
+
5
8
  每一次显式派发的 AgentRun 都产生一个持久的原始结果。普通通知和原生对话不会
6
9
  隐式创建 Run。所有权链上的下一个 Agent 读取这个精确结果并判断它意味着什么。
7
10
 
@@ -67,6 +67,13 @@ than guessed. Incremental observers report health and coverage; sampling does
67
67
  not block lifecycle events. Metrics never trigger model selection, wake, retry,
68
68
  resource release or acceptance.
69
69
 
70
+ [Task usage and time](observability/README.md#task-usage-and-time) reuse this
71
+ reducer across historical Sessions. Task-fenced observations are not a billing
72
+ completeness guarantee. Native child counters are excluded from parent Session
73
+ totals without an explicit non-overlap contract; they are never extra requests.
74
+ Raw cumulative Session counters and safely attributable Task increments remain
75
+ distinct, and only exact request/Run bindings support WorkItem allocation.
76
+
70
77
  ## Native children
71
78
 
72
79
  Native subagents are collaboration inside a parent conversation, not Yui Roles,
@@ -56,6 +56,11 @@ Task 完成。
56
56
  和覆盖度;采样不阻塞生命周期事件。度量绝不触发模型选择、唤醒、重试、资源释放
57
57
  或接受。
58
58
 
59
+ [Task 用量与耗时](observability/README.zh-CN.md#task-用量与耗时)跨历史 Session
60
+ 复用此 reducer。Task 围栏内的观察不是完整账单保证。缺少明确互斥合同时,原生
61
+ 子计数不加入父 Session 总量,也不作为额外请求。原始 Session 累计值与安全归属
62
+ 的 Task 增量分开;仅精确请求/Run 绑定支持 WorkItem 分配。
63
+
59
64
  ## 原生子代
60
65
 
61
66
  原生 subagent 是父对话内部的协作,不是 Yui Role、Lane 或独立的受管工作区 owner。
@@ -20,11 +20,13 @@ not a claim that every real Provider scenario has been validated.
20
20
  | Question | Current document |
21
21
  | --- | --- |
22
22
  | How do Session, AgentRun, messages and activation fit together? | [Session and AgentRun runtime](../managed-turn-and-session-runtime.md) |
23
+ | What can Web control, and how do queue, steer and interrupt differ? | [Web permissions](capabilities-and-resources.md#cli-and-web) · [Input timing](../managed-turn-and-session-runtime.md#input-timing-queue-steer-and-interrupt) |
23
24
  | Who consumes results, synthesis and review? | [Result consumption](../agent-result-consumption.md) |
24
25
  | When is a WorkItem dependency satisfied? | [Task dependencies](../task-dag-semantics.md) |
25
26
  | How are records referenced inside a Task? | [Task-local identity](../task-local-identity.md) |
26
27
  | How do Roles, Profiles and run configuration take effect? | [Roles and configuration](../roles-and-configuration.md) |
27
28
  | How do delivery, integration and archive work? | [Task delivery](../task-delivery.md) |
29
+ | What does Project refresh synchronize, and how are partial failures reported? | [Project refresh](../project-refresh.md) |
28
30
  | How do Provider, ACP and configuration facts connect? | [Provider runtime](../provider-runtime.md) |
29
31
  | Who interprets runtime observations and errors? | [Agent Drivers](../agent-runtime-drivers.md) |
30
32
  | How are plugins created, validated and adopted? | [Plugin SDK](../plugin-sdk.md) |
@@ -8,7 +8,7 @@
8
8
 
9
9
  - [English README](../../README.md):安装、配置和日常使用。
10
10
  - [中文 README](../../i18n/README.zh-CN.md):同一产品入口的中文说明。
11
- - [总体架构](../../ARCHITECTURE.md):职责、权威和端到端流程(英文)。
11
+ - [总体架构](../../ARCHITECTURE.zh-CN.md):职责、权威和端到端流程。
12
12
  - [能力、资源与 Surface](capabilities-and-resources.zh-CN.md):扩展入口、实例所有权和资源效果。
13
13
 
14
14
  ## 领域合同
@@ -16,11 +16,13 @@
16
16
  | 问题 | 当前文档 |
17
17
  | --- | --- |
18
18
  | Session、AgentRun、消息和激活如何配合? | [执行与会话](../managed-turn-and-session-runtime.zh-CN.md) |
19
+ | Web 可以控制什么,queue、steer 和 interrupt 有何区别? | [Web 权限](capabilities-and-resources.zh-CN.md#cli-与-web) · [输入时机](../managed-turn-and-session-runtime.zh-CN.md#输入时机queuesteer-与-interrupt) |
19
20
  | 谁消费结果、综合与审查? | [结果消费](../agent-result-consumption.zh-CN.md) |
20
21
  | WorkItem 依赖何时满足? | [Task 依赖](../task-dag-semantics.zh-CN.md) |
21
22
  | Task 内记录如何引用? | [局部身份](../task-local-identity.zh-CN.md) |
22
23
  | Role、Profile 与运行配置如何生效? | [角色与配置](../roles-and-configuration.zh-CN.md) |
23
24
  | 怎样交付、集成和归档? | [交付生命周期](../task-delivery.zh-CN.md) |
25
+ | Project refresh 同步什么,怎样报告部分失败? | [Project refresh](../project-refresh.zh-CN.md) |
24
26
  | Provider、ACP 与配置事实如何接入? | [Provider Runtime](../provider-runtime.zh-CN.md) |
25
27
  | 运行观察和错误由谁解释? | [Agent Drivers](../agent-runtime-drivers.zh-CN.md) |
26
28
  | 如何创建、验证与采用插件? | [插件 SDK](../plugin-sdk.zh-CN.md) |
@@ -111,8 +111,33 @@ capability's original name. A Web panel accepts only controlled text, an HTTP(S)
111
111
  link or a JSON query description — not author scripts or arbitrary HTML.
112
112
 
113
113
  The Web listener is started and stopped by the Controller and allows loopback
114
- only. A browser write goes through an existing domain transaction, and its error
115
- distinguishes a definite non-commit from a committed-but-unknown result. A query
116
- panel cannot use the browser's identity to run a mutation or manage plugins. A
117
- terminal connection only attaches a client; it does not take over durable
118
- ownership of the native conversation.
114
+ only (`127.0.0.1`, `::1` or `localhost`; default port 4173). `yui web` opens this
115
+ local surface, not a remote multi-user service or an OS sandbox.
116
+
117
+ The page supplies a token that authenticates all HTTP API reads and writes via
118
+ `x-yui-web-token`; the server also checks the loopback Host. These controls act
119
+ as the trusted local user, not a Role selected by the request body. They support
120
+ Task metadata edits, messages, InputRequest answers, and explicit
121
+ `queue / steer / interrupt` for Task or Global Roles. Managed Agent capability
122
+ RPC retains its own Session authentication and scope; a browser token is not
123
+ a way for an Agent or plugin to bypass those boundaries.
124
+
125
+ Read-only dashboard, Context and query-panel projections remain separate from
126
+ these mutations. A query panel cannot borrow the browser's user authority to
127
+ mutate state or manage plugins. Task controls share the public CLI's domain
128
+ commands; Global Role controls share the Global handler but currently lack a
129
+ registered top-level CLI path. Message submission intent
130
+ (`record / discuss / develop`, default `discuss`) is
131
+ separate from [input timing](../managed-turn-and-session-runtime.md#input-timing-queue-steer-and-interrupt).
132
+ Transport acceptance does not establish implementation or Task acceptance.
133
+
134
+ A browser write uses an existing domain transaction. Errors distinguish proven
135
+ `not-submitted` from `unknown`, which can include an already-committed Message
136
+ whose native delivery failed or is unconfirmed. Read the original Message,
137
+ control receipt and current Session before choosing recovery; do not blindly
138
+ resubmit under a new request ID or switch actions.
139
+
140
+ A terminal WebSocket checks the token and same-origin handshake. It attaches a
141
+ client without taking over durable conversation ownership, and respects the
142
+ connection's `readOnly` flag. A terminal attachment is not a grant to control a
143
+ different Session.
@@ -78,6 +78,26 @@ Surface contribution 由 Registry 当前获授权目录派生,没有第二份
78
78
  CLI contribution 使用能力原名称。Web panel 只接受受控 text、HTTP(S) link 或
79
79
  JSON query 描述,不接受作者脚本或任意 HTML。
80
80
 
81
- Web listener 由 Controller 启停,仅允许 loopback。浏览器写入通过现有领域
82
- 事务,错误区分确定未提交与提交结果未知。查询面板不能借浏览器身份执行 mutation
83
- 或插件管理。终端连接只 attach 客户端,不接管原生对话的持久所有权。
81
+ Web listener 由 Controller 启停,仅允许 loopback(`127.0.0.1`、`::1` 或
82
+ `localhost`;默认端口 4173)。`yui web` 打开的是本地 Surface,不是远程多用户
83
+ 服务或 OS 沙箱。
84
+
85
+ 页面提供 token,通过 `x-yui-web-token` 认证所有 HTTP API 读取与写入;服务端
86
+ 也检查 loopback Host。这些控制使用受信任本地用户身份,不由请求正文选择 Role。
87
+ 它们支持修改 Task 元数据、发送消息、回答 InputRequest,以及对 Task 或 Global
88
+ Role 显式 `queue / steer / interrupt`。受管 Agent 的能力 RPC 仍使用自身 Session
89
+ 认证与范围;浏览器 token 不是 Agent 或插件绕过这些边界的途径。
90
+
91
+ 只读 dashboard、Context 和查询面板投影与这些修改分开。查询面板不能借浏览器的
92
+ 用户权限修改状态或管理插件。Task 控制复用公开 CLI 领域命令;Global Role 控制复用
93
+ Global 处理器,但目前缺少已注册的顶层 CLI 路径。消息提交意图
94
+ (`record / discuss / develop`,默认 `discuss`)与
95
+ [输入时机](../managed-turn-and-session-runtime.zh-CN.md#输入时机queuesteer-与-interrupt)
96
+ 分开。传输接受不证明需求已实施或 Task 已验收。
97
+
98
+ 浏览器写入使用现有领域事务。错误区分已证明的 `not-submitted` 与 `unknown`;
99
+ 后者可能包含已经提交、但原生投递失败或未确认的 Message。选择恢复动作前先读
100
+ 原始 Message、控制回执和当前 Session,不盲目换 request ID 重发或切换动作。
101
+
102
+ 终端 WebSocket 校验 token 和同源握手。它只 attach 客户端,不接管对话的持久
103
+ 所有权,并遵守连接的 `readOnly` 标记。附着终端不授予控制其他 Session 的权限。
@@ -109,6 +109,53 @@ Resolve releases the claim after native-effect fences are clear. It neither
109
109
  replays the notification nor invents acceptance or completion. Independent
110
110
  Role work and legal local facts are not a Task-wide recovery lock.
111
111
 
112
+ ## Input timing: queue, steer and interrupt
113
+
114
+ Submission intent (`record / discuss / develop`) decides how a requirement is
115
+ routed. Input timing decides when an already-authorized input reaches a Role;
116
+ it does not activate a Task, expand an Assignment or upgrade planning authority.
117
+ The [authenticated Web controls](architecture/capabilities-and-resources.md#cli-and-web)
118
+ use the same three operations as the CLI.
119
+
120
+ | Action | Effect | What it does not prove |
121
+ | --- | --- | --- |
122
+ | `queue` | Saves a Message for the recipient's next legal opportunity, idempotently by request ID | Reading Context or accepting delivery is not implementation |
123
+ | `steer` | Saves a Message and attempts native steering of the exact current Turn | Unsupported, stale or unconfirmed steering is not a queued continuation |
124
+ | `interrupt` | Records a control request and asks the Provider to cancel the exact current Turn | A stop request is not a terminal or proof that background resources stopped |
125
+
126
+ Inspect the Session before selecting a live target:
127
+
128
+ ```sh
129
+ yui task role session inspect <task> <role>
130
+ yui task message queue <task> "<continuation>" --request-id <id> --to leader
131
+ yui task message steer <task> "<correction>" --request-id <id> --to leader --expected-target <turn>
132
+ yui task role interrupt <task> <role> --expected-target <turn> --request-id <id> [--then-message <task/message>]
133
+ ```
134
+
135
+ Worker/Reviewer messages retain their existing `--work-item` or `--review-round`
136
+ association. Reusing a request ID with different content or a different target
137
+ is a conflict. `steer` and `interrupt` never silently retarget, replace a Session,
138
+ kill a process or fall back to another action. No live managed Turn yields
139
+ `NO_ACTIVE_TURN`; stale targets and unsupported control remain explicit outcomes.
140
+
141
+ Bare interrupt creates no Message. Optional `--then-message` names an already-saved,
142
+ eligible input and reserves its next opportunity only after an exact terminal,
143
+ within the original Session/writer boundary. It is not a fourth action or a way
144
+ to replay accepted, pending or unknown steering. A conclusive non-delivery can
145
+ permit an explicit new control choice when the user's intent authorizes it;
146
+ uncertainty cannot.
147
+
148
+ Global Roles use the same three actions with their own owner and Session,
149
+ without inventing a Task or Run. The local-user Web surface exposes them through
150
+ the shared Global Role handler. There is a current CLI availability gap:
151
+ `src/cli.ts` implements `yui role message queue|steer` and `yui role interrupt`,
152
+ but `src/cli/commandCatalog.ts` does not register the top-level `role` command,
153
+ so public CLI routing rejects these paths as unknown. They are not usable CLI
154
+ examples; report this gap rather than fabricating a Task/Run or borrowing the
155
+ browser's user authority. New controlled Global Sessions use the Host console.
156
+ A live unmanaged Session is not silently adopted; an explicit Session lifecycle
157
+ action is needed first.
158
+
112
159
  ## Exact results
113
160
 
114
161
  The native terminal settles only the matching execution. Known native Turn IDs
@@ -94,6 +94,46 @@ yui task wake resolve <task> <wake> --reason <quiescence-evidence>
94
94
  resolve 在原生效果围栏清除后释放该认领。它既不重放通知,也不编造接受或完成。独立的
95
95
  Role 工作和合法的本地事实不是一把 Task 范围的恢复锁。
96
96
 
97
+ ## 输入时机:queue、steer 与 interrupt
98
+
99
+ 提交意图(`record / discuss / develop`)决定需求如何路由。输入时机决定一条已经
100
+ 获授权的输入何时到达 Role;它不激活 Task、不扩大 Assignment,也不提升 planning
101
+ 权限。[经认证的 Web 控制](architecture/capabilities-and-resources.zh-CN.md#cli-与-web)
102
+ 与 CLI 使用同样的三种操作。
103
+
104
+ | 动作 | 效果 | 不证明什么 |
105
+ | --- | --- | --- |
106
+ | `queue` | 保存 Message,等待收件人的下一个合法机会,按 request ID 幂等 | 读取 Context 或接受投递不等于实施 |
107
+ | `steer` | 保存 Message,并尝试原生 steer 精确的当前 Turn | 不受支持、目标陈旧或未确认的 steer 不等于排队延续 |
108
+ | `interrupt` | 记录控制请求,请 Provider 取消精确的当前 Turn | 停止请求不等于终态,也不证明后台资源已停止 |
109
+
110
+ 选择实时目标前先检查 Session:
111
+
112
+ ```sh
113
+ yui task role session inspect <task> <role>
114
+ yui task message queue <task> "<continuation>" --request-id <id> --to leader
115
+ yui task message steer <task> "<correction>" --request-id <id> --to leader --expected-target <turn>
116
+ yui task role interrupt <task> <role> --expected-target <turn> --request-id <id> [--then-message <task/message>]
117
+ ```
118
+
119
+ Worker/Reviewer 消息保留既有的 `--work-item` 或 `--review-round` 关联。同一个
120
+ request ID 若换正文或目标会产生冲突。`steer` 与 `interrupt` 不会静默改目标、
121
+ 替换 Session、杀进程或回退到另一动作。没有活动受管 Turn 时返回 `NO_ACTIVE_TURN`;
122
+ 陈旧目标与不受支持的控制也保持为显式结果。
123
+
124
+ 裸 interrupt 不创建 Message。可选的 `--then-message` 引用一条已保存且符合交接条件
125
+ 的输入,只在精确终态之后、原 Session/writer 边界内保留下一次机会。它不是第四种动作,
126
+ 也不能用来重放已接受、待确认或未知的 steer。明确证明未投递时,若用户意图允许,
127
+ 可以显式选择新的控制;不确定性不允许重放。
128
+
129
+ Global Role 使用同样的三种动作和自己的 owner、Session,不虚构 Task 或 Run。
130
+ 本地用户 Web Surface 通过共享 Global Role 处理器暴露这些动作。目前 CLI 存在可用性
131
+ 缺口:`src/cli.ts` 实现了 `yui role message queue|steer` 和 `yui role interrupt`,
132
+ 但 `src/cli/commandCatalog.ts` 没有注册顶层 `role`,因此公开 CLI 路由会拒绝这些路径,
133
+ 报告 unknown command。它们不是可用的 CLI 示例;应报告该缺口,不虚构 Task/Run 或
134
+ 借用浏览器用户权限。新的受控 Global Session 使用 Host console。活动的非受管 Session
135
+ 不会被静默采用,需要先执行显式的 Session 生命周期操作。
136
+
97
137
  ## 精确结果
98
138
 
99
139
  原生终态只结算相匹配的那次执行。已知的原生 Turn ID 必须匹配;串行流可以使用已证明的
@@ -81,3 +81,65 @@ raw Provider history merely to explain a status.
81
81
  Start with exact read-only records. Process changes, cancellation, grant updates
82
82
  and resource cleanup require the relevant explicit action and scope. A generic
83
83
  diagnostic request does not authorize live-model, shared or production tests.
84
+
85
+ ## Task usage and time
86
+
87
+ Task overview, Web Task/WorkItem cards and `execution audit` use the same pure
88
+ projection of authorized Task events. Reading never samples a Provider or opens
89
+ raw transcripts. There is no new metric store, migration, price table or budget
90
+ policy. Existing history remains readable.
91
+
92
+ Each metric has `value`, `status` (`known`, `partial`, `unknown`) and `reasons`.
93
+ Unknown is `null`, not zero. A known zero requires actual numeric evidence.
94
+ Partial is an observed subtotal, not a complete bill or guaranteed monotonic
95
+ lower bound. Coverage names the observed Session identities, source/semantics
96
+ and evidence cutoff; it is not a percentage of an unknowable Provider total.
97
+ The declared basis is Task-fenced, observed sources only.
98
+ JSON consumers read `cost.tokens.value` and `cost.toolCalls.value` with their
99
+ status/reasons, replacing the numeric placeholders and observable flags.
100
+ `elapsedSeconds` and `executionSeconds` replace the misleading Group-sum
101
+ `wallClockSeconds`; this changes a read projection, not persistent storage.
102
+
103
+ - Request usage reuses the Session reducer: stable request identity, latest
104
+ received revision, input plus output, no extra addition of cache/reasoning
105
+ subsets. Missing boundaries, mixed semantics and cumulative rollback are not
106
+ guessed. Remaining context is capacity, never consumption.
107
+ - Replaced Sessions remain in the lifetime view. A nonzero first cumulative
108
+ snapshot is an excluded baseline: it may predate the Task. Later comparable
109
+ increments are partial; one nonzero snapshot alone yields unknown Task usage.
110
+ A zero baseline supports the subsequent counter. JSON also exposes raw
111
+ Session counters separately; they are not additional Task consumption.
112
+ - Direct Leader chat can contribute without a Run or WorkItem. WorkItems
113
+ receive only request usage whose revisions share one exact, matching Run
114
+ binding. Cumulative counters are not apportioned. Raw whole-Session totals
115
+ are not exposed as WorkItem usage. Task totals need not equal WorkItem sums.
116
+ - Child counters are excluded because the current contract cannot prove they
117
+ are additional to the parent. Child evidence marks coverage partial. Conflicting
118
+ Role ownership of one native counter is unknown, not two independent totals.
119
+ - Tool counts deduplicate retained exact native Session/Turn/operation identities,
120
+ including failures. Operation history is compacted, so this is always partial
121
+ when evidence exists and unknown otherwise. Absence never proves zero tools.
122
+
123
+ **Task elapsed** runs from Task creation (including planning and waiting) to its
124
+ recorded completion, retirement or cancellation, or to the read time if active.
125
+ An archived Task keeps its original endpoint; missing terminal evidence is
126
+ unknown. Group count is irrelevant.
127
+
128
+ **Observed native execution sum** merges overlapping complete Turn intervals
129
+ within one native resource and adds independent parallel resources. Two
130
+ independent ten-second intervals can total twenty seconds during ten seconds of
131
+ elapsed time. It is not CPU/GPU time. Since Turn history is compacted, this is
132
+ partial; missing start/end and live Turns are excluded, never extended indefinitely.
133
+ Subsecond precision is retained. WorkItem cards do not substitute Group duration
134
+ for either measure.
135
+
136
+ The audit `usage` section is explicitly **Task lifetime** even when `--since` or
137
+ `--until` filters other sections. It does not offer window consumption; filtering
138
+ cumulative snapshots first would mislabel historical usage as window usage.
139
+ Its existing AgentRun-duration section remains a separately labeled Run metric.
140
+
141
+ Deterministic fixtures cover the shared reducer and CLI/Web/audit semantics.
142
+ They do not establish live Provider completeness. Built-in normalization supports
143
+ Codex cumulative and Claude request observations when supplied; this delivery
144
+ does not collect real-model billing evidence or assert Provider behavior was
145
+ live-tested.