@zq-silk/yui 2.4.0 → 2.6.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.
- package/dist/cli/commandCatalog.js +4 -3
- package/dist/commands/taskCommands.js +48 -15
- package/dist/commands/taskContextCommand.js +3 -2
- package/dist/context/sessionBootstrapManifest.js +8 -2
- package/dist/context/taskContext.js +7 -4
- package/dist/controller/fileSchedulerStoreAdapter.js +7 -2
- package/dist/controller/globalInputDelivery.js +2 -4
- package/dist/controller/providerRetryDelivery.js +4 -2
- package/dist/executor/agentAdapter.js +3 -5
- package/dist/executor/fileRoleLaunchPlanner.js +2 -2
- package/dist/interaction/notificationPresentation.js +85 -0
- package/dist/interaction/operatorPresentation.js +1 -2
- package/dist/kernel/builtinCapabilities.js +2 -1
- package/dist/runtime/builtinAgentErrorMappers.js +9 -5
- package/dist/runtime/providerRetry.js +38 -0
- package/dist/runtime/taskSessions.js +240 -0
- package/dist/scheduler/leaderWakeupProcessor.js +1 -9
- package/dist/scheduler/taskExecutionProjection.js +78 -6
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app/taskView.js +1 -0
- package/dist/web/assets/client/domain/records.js +70 -2
- package/dist/web/assets/client/domain/runs.js +3 -2
- package/dist/web/assets/client/domain/vocab.js +1 -1
- package/dist/web/assets/client/i18n/en.js +17 -1
- package/dist/web/assets/client/i18n/zh-CN.js +17 -1
- package/dist/web/assets/client/lib/api.js +4 -1
- package/dist/web/assets/client/lib/theme.js +31 -10
- package/dist/web/assets/client/views/task/actions.js +38 -21
- package/dist/web/assets/client/views/task/observation.js +12 -3
- package/dist/web/assets/client/views/task/overview.js +7 -40
- package/dist/web/assets/client/views/task/runtime.js +71 -34
- package/dist/web/assets/shared/icons.js +2 -0
- package/dist/web/assets/shell/index.js +1 -0
- package/dist/web/assets/shell/settings.js +1 -0
- package/dist/web/assets/styles/views/task.js +14 -0
- package/dist/web/webConversation.js +2 -2
- package/dist/web/webServer.js +12 -0
- package/dist/web/webSessions.js +2 -165
- package/dist/web/webSnapshot.js +9 -5
- package/dist/web/webTaskActivity.js +54 -0
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/cli-information-contract.md +1 -1
- package/docs/cli-information-contract.zh-CN.md +1 -1
- package/docs/managed-turn-and-session-runtime.md +22 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +15 -0
- package/docs/observability/README.md +25 -1
- package/docs/observability/README.zh-CN.md +18 -1
- package/docs/provider-retry.md +11 -0
- package/docs/release-workflow.md +53 -0
- package/docs/release-workflow.zh-CN.md +40 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -4
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-operator/SKILL.md +3 -0
- package/skills/yui-reviewer/SKILL.md +4 -2
- package/skills/yui-runtime/SKILL.md +43 -2
- package/skills/yui-runtime/references/recovery.md +9 -0
- package/skills/yui-worker/SKILL.md +3 -2
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
2
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
3
|
+
import { projectRuntimeTaskEvents, classifyRuntimeHealth } from "../runtime/runtimeProjection.js";
|
|
4
|
+
import { DEFAULT_RUNTIME_HEALTH_POLICY } from "../runtime/runtimeHealthPolicy.js";
|
|
5
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
6
|
+
/** Read-only presentation of the selected native connections, never an AgentRun
|
|
7
|
+
* count. Foreground facts are fenced to the exact input; retained continuations
|
|
8
|
+
* stay attached to their original input and are not counted as extra Sessions.
|
|
9
|
+
* No live probe, timers, persistence or recovery actions are introduced here. */
|
|
10
|
+
export function projectTaskSessions(input) {
|
|
11
|
+
const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
|
|
12
|
+
const sessions = [];
|
|
13
|
+
const seen = new Map();
|
|
14
|
+
const selectedIdentities = new Set();
|
|
15
|
+
const observationIdentity = (fence) => JSON.stringify([fence.roleName, fence.agentId, fence.driverId, fence.nativeSessionId]);
|
|
16
|
+
const observations = input.events.map(event => ({ event, observation: runtimeObservationFromTaskEvent(event) }))
|
|
17
|
+
.filter(entry => entry.event.taskId === input.taskId && entry.observation?.fence.taskId === input.taskId);
|
|
18
|
+
for (const set of input.sessionSets) {
|
|
19
|
+
if (set.owner.scope !== "task" || set.owner.taskId !== input.taskId)
|
|
20
|
+
continue;
|
|
21
|
+
const session = set.sessions[set.activeAgentId];
|
|
22
|
+
const binding = set.providerBinding;
|
|
23
|
+
const conversation = binding?.conversations.find(c => c.epoch === binding.currentConversationEpoch && c.status === "current");
|
|
24
|
+
// Replacement can leave an empty set. That is absence, not uncertain
|
|
25
|
+
// execution; retained child facts are still discovered below.
|
|
26
|
+
if (!session && !conversation && !binding?.run)
|
|
27
|
+
continue;
|
|
28
|
+
const nativeSessionId = session?.nativeSessionId ?? conversation?.conversationId ?? null;
|
|
29
|
+
const identity = JSON.stringify([binding?.providerNamespace ?? session?.adapterId,
|
|
30
|
+
binding?.accountScope ?? set.activeAgentId, nativeSessionId ?? set.owner.roleName]);
|
|
31
|
+
const prior = seen.get(identity);
|
|
32
|
+
if (prior !== undefined) {
|
|
33
|
+
if (sessions[prior].roleName !== set.owner.roleName)
|
|
34
|
+
sessions[prior] = {
|
|
35
|
+
...sessions[prior], group: "unknown", reason: "Multiple Roles name this native Session; inspect ownership."
|
|
36
|
+
};
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
seen.set(identity, sessions.length);
|
|
40
|
+
const turn = binding?.run;
|
|
41
|
+
let driverId = null;
|
|
42
|
+
if (session) {
|
|
43
|
+
try {
|
|
44
|
+
driverId = builtinDriverIdForAdapter(session.adapterId);
|
|
45
|
+
}
|
|
46
|
+
catch { /* Unsupported native observation remains explicitly unknown. */ }
|
|
47
|
+
}
|
|
48
|
+
const row = {
|
|
49
|
+
taskId: input.taskId, roleName: set.owner.roleName, agentId: set.activeAgentId,
|
|
50
|
+
driverId,
|
|
51
|
+
nativeSessionId, attemptId: turn?.attemptId ?? null, nativeTurnId: turn?.nativeTurnId ?? null,
|
|
52
|
+
runId: turn?.runId ?? null, group: "unknown", reason: "Current native input is not observable.",
|
|
53
|
+
layer: null, lastActivityAt: null, semanticProgressAt: input.semanticProgressAt ?? null,
|
|
54
|
+
sourceUpdatedAt: turn?.updatedAt ?? session?.updatedAt ?? set.updatedAt,
|
|
55
|
+
waitingReason: null, operations: [], background: []
|
|
56
|
+
};
|
|
57
|
+
if (!session || driverId === null) {
|
|
58
|
+
sessions.push(row);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const fence = { taskId: input.taskId, roleName: set.owner.roleName,
|
|
62
|
+
agentId: session.agentId, driverId, nativeSessionId: session.nativeSessionId };
|
|
63
|
+
selectedIdentities.add(observationIdentity(fence));
|
|
64
|
+
const sessionFacts = observations.filter(({ observation }) => Object.entries(fence).every(([key, value]) => observation.fence[key] === value));
|
|
65
|
+
// Each Run's continuation fold has its own authority. Foreground operations
|
|
66
|
+
// from older inputs must not make the current input look active.
|
|
67
|
+
const groups = new Map();
|
|
68
|
+
for (const { event, observation } of sessionFacts) {
|
|
69
|
+
const runId = observation.fence.runId;
|
|
70
|
+
const group = groups.get(runId) ?? [];
|
|
71
|
+
group.push(event);
|
|
72
|
+
groups.set(runId, group);
|
|
73
|
+
}
|
|
74
|
+
const background = backgroundForRuns(fence, session.createdAt, groups);
|
|
75
|
+
if (!binding || !conversation || conversation.conversationId !== session.nativeSessionId) {
|
|
76
|
+
sessions.push({ ...row, background });
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (!turn) {
|
|
80
|
+
const sessionEvents = sessionFacts.filter(({ observation }) => {
|
|
81
|
+
const f = observation.fence;
|
|
82
|
+
return f.runId === undefined && f.nativeTurnId === undefined
|
|
83
|
+
&& f.receiptId === undefined && f.continuationId === undefined;
|
|
84
|
+
}).map(entry => entry.event);
|
|
85
|
+
const projection = projectRuntimeTaskEvents(fence, session.createdAt, sessionEvents);
|
|
86
|
+
const ready = session.status !== "ended" && projection.session === "ready"
|
|
87
|
+
&& projection.host !== "exited" && projection.run === "none"
|
|
88
|
+
&& projection.observer.status !== "unavailable" && projection.observer.status !== "degraded"
|
|
89
|
+
&& projection.goal !== "active" && binding.goal?.status !== "active"
|
|
90
|
+
&& Object.keys(projection.operations).length === 0;
|
|
91
|
+
sessions.push({ ...row, background, ...(background.length ? {
|
|
92
|
+
group: "background", reason: "Retained background execution is not settled."
|
|
93
|
+
} : session.status === "ended" || projection.session === "ended" ? {
|
|
94
|
+
group: "stopped", reason: "The selected Session ended with no submitted native input."
|
|
95
|
+
} : ready ? {
|
|
96
|
+
group: "idle", reason: "The selected Session is ready with no submitted native input."
|
|
97
|
+
} : {}) });
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const exactEvents = sessionFacts.filter(({ observation }) => {
|
|
101
|
+
const observed = observation;
|
|
102
|
+
const f = observed.fence;
|
|
103
|
+
if (f.continuationId !== undefined || f.runId !== turn.runId)
|
|
104
|
+
return false;
|
|
105
|
+
if (f.receiptId === undefined && f.nativeTurnId === undefined
|
|
106
|
+
&& ["host.observed", "session.ended", "session.failed", "observer.health"].includes(observed.kind)) {
|
|
107
|
+
return observed.receivedAt >= turn.submittedAt;
|
|
108
|
+
}
|
|
109
|
+
if (f.receiptId !== undefined && f.receiptId !== turn.attemptId)
|
|
110
|
+
return false;
|
|
111
|
+
if (f.nativeTurnId !== undefined && f.nativeTurnId !== turn.nativeTurnId)
|
|
112
|
+
return false;
|
|
113
|
+
return f.receiptId === turn.attemptId
|
|
114
|
+
|| (turn.nativeTurnId !== undefined && f.nativeTurnId === turn.nativeTurnId);
|
|
115
|
+
}).map(entry => entry.event);
|
|
116
|
+
const projection = projectRuntimeTaskEvents({
|
|
117
|
+
...fence, ...(turn.runId ? { runId: turn.runId } : {}),
|
|
118
|
+
receiptId: turn.attemptId, ...(turn.nativeTurnId ? { nativeTurnId: turn.nativeTurnId } : {})
|
|
119
|
+
}, turn.submittedAt, exactEvents);
|
|
120
|
+
const health = classifyRuntimeHealth({ projection, now: input.now, policy,
|
|
121
|
+
semanticProgressAt: input.semanticProgressAt ?? turn.submittedAt });
|
|
122
|
+
const retry = providerRetryProjection(binding);
|
|
123
|
+
let group;
|
|
124
|
+
let reason = health.reason;
|
|
125
|
+
if (turn.status === "delivery-unknown") {
|
|
126
|
+
group = "unknown";
|
|
127
|
+
reason = "Original input delivery is unknown; do not replay it.";
|
|
128
|
+
}
|
|
129
|
+
else if (session.status === "ended") {
|
|
130
|
+
group = background.length ? "background" : "stopped";
|
|
131
|
+
reason = "The selected Session ended; Task acceptance and retained background execution are separate.";
|
|
132
|
+
}
|
|
133
|
+
else if (retry?.status === "waiting") {
|
|
134
|
+
group = "waiting";
|
|
135
|
+
reason = `Controller-owned Provider retry waiting (${retry.attempts}/${retry.limit}); next eligible ${retry.nextEligibleAt}. Existing work is preserved.`;
|
|
136
|
+
}
|
|
137
|
+
else if (["failed", "cancelled", "rejected"].includes(turn.status)) {
|
|
138
|
+
group = background.length ? "background" : "stopped";
|
|
139
|
+
reason = turn.terminalReason ?? "The selected native execution stopped; Task acceptance is separate.";
|
|
140
|
+
}
|
|
141
|
+
else if (turn.status === "completed") {
|
|
142
|
+
group = background.length ? "background" : "idle";
|
|
143
|
+
reason = background.length ? "Main Turn ended; background execution remains unsettled."
|
|
144
|
+
: "The native Turn ended; this is not Task completion.";
|
|
145
|
+
}
|
|
146
|
+
else if (turn.status !== "accepted") {
|
|
147
|
+
group = "waiting";
|
|
148
|
+
reason = "Waiting for native input acceptance.";
|
|
149
|
+
}
|
|
150
|
+
else if (projection.run === "waiting") {
|
|
151
|
+
group = "waiting";
|
|
152
|
+
}
|
|
153
|
+
else if (["broken", "stopped"].includes(health.layer)) {
|
|
154
|
+
group = "stopped";
|
|
155
|
+
}
|
|
156
|
+
else if (["completed", "failed", "cancelled"].includes(projection.run)) {
|
|
157
|
+
group = "unknown";
|
|
158
|
+
reason = "Native terminal and current input binding disagree; inspect the exact original input.";
|
|
159
|
+
}
|
|
160
|
+
else if (projection.observer.status === "unavailable") {
|
|
161
|
+
group = "unknown";
|
|
162
|
+
}
|
|
163
|
+
else if (health.layer === "diagnostic-needed") {
|
|
164
|
+
group = "diagnostic";
|
|
165
|
+
}
|
|
166
|
+
else if (health.runtimeIdleMs < policy.quietAfterMs && projection.activity.kind !== "provider") {
|
|
167
|
+
group = "active";
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
group = "quiet";
|
|
171
|
+
reason = "Accepted input has no recent structured activity; silence is not a proven deadlock.";
|
|
172
|
+
}
|
|
173
|
+
sessions.push({ ...row, group, reason, layer: health.layer, background,
|
|
174
|
+
lastActivityAt: projection.lastRuntimeActivityAt ?? null,
|
|
175
|
+
waitingReason: projection.waitingReason ?? null, operations: health.activeOperations });
|
|
176
|
+
}
|
|
177
|
+
// Retained, exactly fenced child observations can outlive a selected Session.
|
|
178
|
+
// Do not revive old foreground Turns or enumerate historical Sessions: only
|
|
179
|
+
// identities with still-unsettled child facts enter this current projection.
|
|
180
|
+
const retained = new Map();
|
|
181
|
+
for (const entry of observations) {
|
|
182
|
+
const fence = entry.observation.fence;
|
|
183
|
+
const key = observationIdentity(fence);
|
|
184
|
+
if (!fence.nativeSessionId || selectedIdentities.has(key))
|
|
185
|
+
continue;
|
|
186
|
+
const group = retained.get(key) ?? [];
|
|
187
|
+
group.push(entry);
|
|
188
|
+
retained.set(key, group);
|
|
189
|
+
}
|
|
190
|
+
for (const group of retained.values()) {
|
|
191
|
+
const first = group[0].observation;
|
|
192
|
+
const fence = first.fence;
|
|
193
|
+
const runs = new Map();
|
|
194
|
+
for (const entry of group) {
|
|
195
|
+
const runId = entry.observation.fence.runId;
|
|
196
|
+
const events = runs.get(runId) ?? [];
|
|
197
|
+
events.push(entry.event);
|
|
198
|
+
runs.set(runId, events);
|
|
199
|
+
}
|
|
200
|
+
const background = backgroundForRuns({
|
|
201
|
+
taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
|
|
202
|
+
driverId: fence.driverId, nativeSessionId: fence.nativeSessionId
|
|
203
|
+
}, first.receivedAt, runs);
|
|
204
|
+
if (!background.length)
|
|
205
|
+
continue;
|
|
206
|
+
sessions.push({
|
|
207
|
+
taskId: input.taskId, roleName: fence.roleName, agentId: fence.agentId,
|
|
208
|
+
driverId: fence.driverId,
|
|
209
|
+
nativeSessionId: fence.nativeSessionId, attemptId: null, nativeTurnId: null, runId: null,
|
|
210
|
+
group: "background", reason: "Retained child observations from a non-selected native Session; foreground execution is not inferred.",
|
|
211
|
+
layer: null, lastActivityAt: group.at(-1).observation.receivedAt,
|
|
212
|
+
semanticProgressAt: null, sourceUpdatedAt: group.at(-1).observation.receivedAt,
|
|
213
|
+
waitingReason: null, operations: [], background
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
const counts = {
|
|
217
|
+
active: 0, waiting: 0, quiet: 0, diagnostic: 0, unknown: 0, stopped: 0, idle: 0, background: 0
|
|
218
|
+
};
|
|
219
|
+
for (const session of sessions)
|
|
220
|
+
counts[session.group]++;
|
|
221
|
+
return { scope: "task-recorded-native-activity", taskId: input.taskId,
|
|
222
|
+
readAt: input.now.toISOString(), counts, sessions,
|
|
223
|
+
coverage: "Selected Sessions and retained, exactly fenced child observations only; not a live process or complete historical Session inventory." };
|
|
224
|
+
}
|
|
225
|
+
function backgroundForRuns(fence, since, groups) {
|
|
226
|
+
return [...groups].flatMap(([runId, events]) => {
|
|
227
|
+
const projection = projectRuntimeTaskEvents({ ...fence, ...(runId === undefined ? {} : { runId }) }, since, events);
|
|
228
|
+
return [
|
|
229
|
+
...Object.entries(projection.continuations)
|
|
230
|
+
.filter(([, child]) => child.execution !== "quiescent" || child.identityConflict)
|
|
231
|
+
.map(([id, child]) => ({ id: `${runId ?? "native"}/${id}`, runId: runId ?? null,
|
|
232
|
+
execution: child.identityConflict ? "unknown" : child.execution })),
|
|
233
|
+
// Drivers may emit both forms for one child. This fold is already
|
|
234
|
+
// fenced to Task/Role/Agent/driver/Session/Run; continuation is richer.
|
|
235
|
+
...Object.entries(projection.operations).filter(([id, operation]) => operation.kind === "subagent" && !Object.hasOwn(projection.continuations, id))
|
|
236
|
+
.map(([id]) => ({ id: `${runId ?? "native"}/operation:${id}`, runId: runId ?? null,
|
|
237
|
+
execution: "unsettled" }))
|
|
238
|
+
];
|
|
239
|
+
});
|
|
240
|
+
}
|
|
@@ -86,15 +86,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
86
86
|
attempted = true;
|
|
87
87
|
const outcome = await delivery.sendOnce({
|
|
88
88
|
delivery: ready, receiptId: notification.attemptId, notificationId: notification.wakeId,
|
|
89
|
-
text:
|
|
90
|
-
`Yui Task notification: task=${task.id} wake=${notification.wakeId}.`,
|
|
91
|
-
`Read current context: yui task context ${task.id} --json.`,
|
|
92
|
-
`Read the fixed notification window: yui task wake show ${task.id} ${notification.wakeId}.`,
|
|
93
|
-
"Read referenced result Messages in full before deciding their disposition.",
|
|
94
|
-
"Follow the Leader Skill to advance outstanding Task work from these durable facts, or record the exact decision/authority needed.",
|
|
95
|
-
"This is a notification, not an execution assignment. No separate final report is required.",
|
|
96
|
-
"Reading context is not an acknowledgement that its requirements have been implemented."
|
|
97
|
-
].join("\n")
|
|
89
|
+
text: notification.presentation
|
|
98
90
|
});
|
|
99
91
|
const accepted = outcome.status === "sent" || outcome.status === "already-sent";
|
|
100
92
|
const disposition = accepted ? "accepted"
|
|
@@ -5,6 +5,7 @@ import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
|
5
5
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
6
6
|
import { actionableExecutionLaneRecoveries, summarizeExecutionGroupHealth } from "../execution/executionHealth.js";
|
|
7
7
|
import { buildTaskObservabilityProjection } from "./taskObservabilityProjection.js";
|
|
8
|
+
import { projectTaskSessions } from "../runtime/taskSessions.js";
|
|
8
9
|
import { isRoleRunStalled, latestStallProgressAt } from "./roleRunStall.js";
|
|
9
10
|
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
10
11
|
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
@@ -13,7 +14,7 @@ import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runt
|
|
|
13
14
|
* aggregate. This function only reads; it never starts a Controller, queues a
|
|
14
15
|
* wake, writes a Message, or mutates any record.
|
|
15
16
|
*/
|
|
16
|
-
export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
17
|
+
export function buildTaskExecutionProjection(store, taskId, now = new Date(), nativeSessions) {
|
|
17
18
|
const task = store.getTask(taskId);
|
|
18
19
|
if (task === null)
|
|
19
20
|
return null;
|
|
@@ -23,6 +24,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
|
23
24
|
const workItems = store.listWorkItems(taskId);
|
|
24
25
|
const reviewRounds = store.listReviewRounds(taskId);
|
|
25
26
|
const runs = operationalTaskRecords(usageRuns, events, "run");
|
|
27
|
+
const brief = store.getTaskBrief(taskId);
|
|
26
28
|
const leaderMailbox = store.getWorkMailbox({
|
|
27
29
|
kind: "role",
|
|
28
30
|
taskId,
|
|
@@ -36,6 +38,12 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
|
36
38
|
});
|
|
37
39
|
return projectTaskExecution({
|
|
38
40
|
task,
|
|
41
|
+
jobs: store.listDurableJobs(taskId),
|
|
42
|
+
nativeSessions: nativeSessions ?? projectTaskSessions({
|
|
43
|
+
taskId, sessionSets: store.listRoleSessionSets(taskId), events, now,
|
|
44
|
+
...(brief?.updatedAt === undefined ? {} : { semanticProgressAt: brief.updatedAt }),
|
|
45
|
+
policy: resolveRuntimeHealth(store.getConfig().runtimeHealth)
|
|
46
|
+
}),
|
|
39
47
|
usageFacts: store.readTaskUsageFacts(taskId),
|
|
40
48
|
roles,
|
|
41
49
|
runs,
|
|
@@ -56,7 +64,7 @@ export function buildTaskExecutionProjection(store, taskId, now = new Date()) {
|
|
|
56
64
|
changeSets: store.listChangeSets(taskId),
|
|
57
65
|
integrations: store.listIntegrationAttempts(taskId),
|
|
58
66
|
events,
|
|
59
|
-
brief
|
|
67
|
+
brief,
|
|
60
68
|
pendingWakeup: store.getPendingWakeup(taskId),
|
|
61
69
|
leaderMailbox,
|
|
62
70
|
leaderFailure: store.getLeaderFailure(taskId),
|
|
@@ -118,8 +126,27 @@ export function projectTaskExecution(facts) {
|
|
|
118
126
|
now
|
|
119
127
|
});
|
|
120
128
|
const laneRecovery = actionableExecutionLaneRecoveries(groupSummaries)[0];
|
|
129
|
+
const jobs = facts.jobs ?? [];
|
|
130
|
+
const native = facts.nativeSessions;
|
|
131
|
+
const activity = {
|
|
132
|
+
jobs: {
|
|
133
|
+
queued: jobs.filter(job => job.status === "queued").length,
|
|
134
|
+
running: jobs.filter(job => job.status === "running").length,
|
|
135
|
+
unknown: jobs.filter(job => job.status === "unknown-needs-attention").length
|
|
136
|
+
},
|
|
137
|
+
openInputs: inputRequests.filter(input => input.status === "open").length,
|
|
138
|
+
native: {
|
|
139
|
+
active: (native?.counts.active ?? 0) + (native?.counts.quiet ?? 0),
|
|
140
|
+
waiting: native?.counts.waiting ?? 0,
|
|
141
|
+
unknown: (native?.counts.unknown ?? 0) + (native?.counts.diagnostic ?? 0)
|
|
142
|
+
+ (native?.sessions.reduce((sum, session) => sum
|
|
143
|
+
+ session.background.filter(child => child.execution === "unknown").length, 0) ?? 0),
|
|
144
|
+
background: native?.sessions.reduce((sum, session) => sum + session.background.length, 0) ?? 0
|
|
145
|
+
}
|
|
146
|
+
};
|
|
121
147
|
const render = (input) => projection({
|
|
122
148
|
...input,
|
|
149
|
+
activity,
|
|
123
150
|
executionGroups: groupSummaries,
|
|
124
151
|
observability
|
|
125
152
|
});
|
|
@@ -128,10 +155,12 @@ export function projectTaskExecution(facts) {
|
|
|
128
155
|
const known = facts.runDelivery?.[run.id];
|
|
129
156
|
if (known !== undefined && known !== "unobserved")
|
|
130
157
|
return known;
|
|
131
|
-
const observed = events.map(runtimeObservationFromTaskEvent).filter((event) => event?.fence.runId === run.id && event.
|
|
158
|
+
const observed = events.map(runtimeObservationFromTaskEvent).filter((event) => event?.fence.runId === run.id && event.fence.taskId === task.id
|
|
159
|
+
&& event.fence.roleName === run.roleName && event.fence.agentId === run.effective.agentId
|
|
160
|
+
&& event.authority !== "transport"
|
|
132
161
|
&& ["turn.accepted", "turn.completed", "turn.failed", "turn.cancelled"].includes(event.kind)).at(-1);
|
|
133
162
|
return observed === undefined || observed === null ? "unobserved"
|
|
134
|
-
: observed.kind.slice("
|
|
163
|
+
: observed.kind.slice("turn.".length);
|
|
135
164
|
};
|
|
136
165
|
const runHasSession = (run) => roleSessions.some((session) => (session.roleName === run.roleName
|
|
137
166
|
&& session.agentId === run.effective.agentId
|
|
@@ -206,7 +235,7 @@ export function projectTaskExecution(facts) {
|
|
|
206
235
|
const activeLeader = activeRuns.find((run) => run.roleName === "leader");
|
|
207
236
|
const pendingDeliveryRuns = activeRuns.filter((run) => deliveryOf(run) !== "accepted");
|
|
208
237
|
const hasPendingLeaderWork = pendingWakeup !== null
|
|
209
|
-
|| leaderMailbox?.pending
|
|
238
|
+
|| leaderMailbox?.pending != null;
|
|
210
239
|
const recoveryPending = isRecoveryPending(pendingWakeup, leaderMailbox, leaderFailure);
|
|
211
240
|
const providerRecoveries = (facts.providerRetries ?? []).filter(retry => retry.status === "waiting" || retry.status === "in-flight");
|
|
212
241
|
const recoveringRunIds = new Set(providerRecoveries.flatMap(retry => [retry.previousRunId, retry.successorRunId].filter((id) => id !== undefined)));
|
|
@@ -257,13 +286,30 @@ export function projectTaskExecution(facts) {
|
|
|
257
286
|
summary: openInputs[0].question,
|
|
258
287
|
reason: "open-input-request",
|
|
259
288
|
monitoring,
|
|
260
|
-
failClosed:
|
|
289
|
+
failClosed: activity.jobs.unknown > 0 || activity.native.unknown > 0,
|
|
261
290
|
activeRuns: activeRunViews,
|
|
262
291
|
attention,
|
|
263
292
|
blockers,
|
|
264
293
|
pendingWakeup
|
|
265
294
|
});
|
|
266
295
|
}
|
|
296
|
+
if (activity.jobs.unknown > 0 || activity.native.unknown > 0) {
|
|
297
|
+
return render({
|
|
298
|
+
task, status: "attention", owner: "leader", action: "inspect-attention",
|
|
299
|
+
summary: `${activity.jobs.unknown} Job(s) have unknown outcomes; ${activity.native.unknown} native observation(s) need attention. Other parallel activity remains visible; do not infer completion or replay.`,
|
|
300
|
+
reason: "parallel-execution-unknown", monitoring, failClosed: true,
|
|
301
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
const nativeWait = native?.sessions.find(session => session.group === "waiting" && ["user", "permission"].includes(session.waitingReason ?? ""));
|
|
305
|
+
if (nativeWait) {
|
|
306
|
+
return render({
|
|
307
|
+
task, status: "waiting-user", owner: "user", action: "inspect-attention",
|
|
308
|
+
summary: `Native Session ${nativeWait.roleName} is waiting for ${nativeWait.waitingReason}; other activity remains visible.`,
|
|
309
|
+
reason: "native-input-wait", monitoring, failClosed: false,
|
|
310
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
311
|
+
});
|
|
312
|
+
}
|
|
267
313
|
if (laneRecovery !== undefined && (laneRecovery.runId === undefined || !recoveringRunIds.has(laneRecovery.runId))) {
|
|
268
314
|
return render({
|
|
269
315
|
task,
|
|
@@ -382,6 +428,15 @@ export function projectTaskExecution(facts) {
|
|
|
382
428
|
pendingWakeup
|
|
383
429
|
});
|
|
384
430
|
}
|
|
431
|
+
if (native?.sessions.some(session => session.roleName === "leader"
|
|
432
|
+
&& ["active", "quiet"].includes(session.group))) {
|
|
433
|
+
return render({
|
|
434
|
+
task, status: "working", owner: "leader", action: "advance-task",
|
|
435
|
+
summary: "Leader native input is accepted; managed AgentRuns and parallel background work remain separate facts.",
|
|
436
|
+
reason: "leader-native-turn", monitoring, failClosed: false,
|
|
437
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
438
|
+
});
|
|
439
|
+
}
|
|
385
440
|
if (hasPendingLeaderWork) {
|
|
386
441
|
return render({
|
|
387
442
|
task,
|
|
@@ -449,6 +504,22 @@ export function projectTaskExecution(facts) {
|
|
|
449
504
|
pendingWakeup
|
|
450
505
|
});
|
|
451
506
|
}
|
|
507
|
+
if (activity.jobs.queued + activity.jobs.running > 0) {
|
|
508
|
+
return render({
|
|
509
|
+
task, status: "waiting-on-jobs", owner: "none", action: "wait-for-jobs",
|
|
510
|
+
summary: `${activity.jobs.running} background Job(s) running; ${activity.jobs.queued} queued. Leader is not currently executing.`,
|
|
511
|
+
reason: "background-jobs", monitoring, failClosed: false,
|
|
512
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
if (activity.native.active + activity.native.waiting + activity.native.background > 0) {
|
|
516
|
+
return render({
|
|
517
|
+
task, status: "waiting-on-agents", owner: "none", action: "wait-for-agents",
|
|
518
|
+
summary: "Recorded native execution or retained background activity remains unsettled; this is separate from managed AgentRuns.",
|
|
519
|
+
reason: "native-execution", monitoring, failClosed: false,
|
|
520
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
521
|
+
});
|
|
522
|
+
}
|
|
452
523
|
if (candidateReady || unresolvedIntegration) {
|
|
453
524
|
return render({
|
|
454
525
|
task,
|
|
@@ -496,6 +567,7 @@ function projection(input) {
|
|
|
496
567
|
monitoring: input.monitoring,
|
|
497
568
|
failClosed: input.failClosed,
|
|
498
569
|
activeRuns: input.activeRuns,
|
|
570
|
+
activity: input.activity,
|
|
499
571
|
executionGroups: input.executionGroups ?? [],
|
|
500
572
|
observability: input.observability,
|
|
501
573
|
attention: input.attention,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { APP_SCRIPT, CLIENT_MODULES } from "./client/index.js";
|
|
7
|
+
import { THEME_BOOTSTRAP_SCRIPT } from "./client/lib/theme.js";
|
|
7
8
|
import { FONT_WOFF2_BASE64 } from "./fontData.js";
|
|
8
9
|
import { DASHBOARD_HTML } from "./shell/index.js";
|
|
9
10
|
import { STYLESHEETS } from "./styles/index.js";
|
|
@@ -13,6 +14,7 @@ const JS = "text/javascript; charset=utf-8";
|
|
|
13
14
|
export const WEB_ASSETS = Object.freeze({
|
|
14
15
|
...Object.fromEntries(STYLESHEETS.map(({ name, body }) => [`/assets/css/${name}.css`, { contentType: CSS, body }])),
|
|
15
16
|
...Object.fromEntries(Object.entries(CLIENT_MODULES).map(([path, body]) => [`/assets/js/${path}.js`, { contentType: JS, body }])),
|
|
17
|
+
"/assets/theme.js": { contentType: JS, body: THEME_BOOTSTRAP_SCRIPT },
|
|
16
18
|
"/assets/app.js": { contentType: JS, body: APP_SCRIPT },
|
|
17
19
|
...fontAssets(),
|
|
18
20
|
"/assets/vendor/xterm.mjs": vendorAsset("@xterm/xterm/lib/xterm.mjs", JS),
|
|
@@ -4,9 +4,77 @@ export const RECORDS_SCRIPT = String.raw `
|
|
|
4
4
|
// never acknowledges or accepts anything.
|
|
5
5
|
import { h, icon } from "/assets/js/lib/dom.js";
|
|
6
6
|
import { button, jsonBlock, mono, note } from "/assets/js/ui/primitives.js";
|
|
7
|
-
import { rawDisclosure } from "/assets/js/ui/containers.js";
|
|
7
|
+
import { rawDisclosure, listCard, setCardCount } from "/assets/js/ui/containers.js";
|
|
8
8
|
import { richText } from "/assets/js/ui/text.js";
|
|
9
|
-
import { cachedExact, readExact, summaryFields } from "/assets/js/domain/context.js";
|
|
9
|
+
import { cachedExact, readExact, summaryFields, entriesOf, totalOf, exactCache } from "/assets/js/domain/context.js";
|
|
10
|
+
|
|
11
|
+
// Current-family counts and pages use exactly the Context current filter.
|
|
12
|
+
// A changed Context revalidates already-loaded membership with one bounded
|
|
13
|
+
// page. Only the family's own digest invalidates its pages, not unrelated
|
|
14
|
+
// runtime events. Loading a list does not eagerly inspect its bodies.
|
|
15
|
+
export function currentRecordCard(data, store, t, actions, options) {
|
|
16
|
+
const view = data.viewState;
|
|
17
|
+
exactCache(view);
|
|
18
|
+
view.openRows = view.openRows || {};
|
|
19
|
+
view.currentLists = view.currentLists || {};
|
|
20
|
+
let state = view.currentLists[store];
|
|
21
|
+
if (!state || (!state.started && state.key !== data.core.coreCursor)) {
|
|
22
|
+
state = view.currentLists[store] = {
|
|
23
|
+
key: data.core.coreCursor, items: entriesOf(data.core, store),
|
|
24
|
+
total: totalOf(data.core, store), started: false, cursor: null, error: null, pending: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const revalidate = state.key !== data.core.coreCursor;
|
|
28
|
+
state.key = data.core.coreCursor;
|
|
29
|
+
const element = listCard({ id: options.id, title: options.title, icon: options.icon || "pulse", count: state.total });
|
|
30
|
+
const rows = h("div.stack"), footer = h("div.stack", { "aria-live": "polite" });
|
|
31
|
+
element.list.append(rows, footer);
|
|
32
|
+
function draw(updateRows) {
|
|
33
|
+
setCardCount(element, state.total);
|
|
34
|
+
if (updateRows !== false) {
|
|
35
|
+
rows.replaceChildren();
|
|
36
|
+
state.items.forEach(function (item) {
|
|
37
|
+
rows.append(!item.omitted && item.value && options.render ? options.render(item.value, null, item)
|
|
38
|
+
: lazyRow(item, data.task.id, t, actions, {
|
|
39
|
+
cache: view.exact,
|
|
40
|
+
openRows: view.openRows,
|
|
41
|
+
head: h("span", null, item.ref.refId + (item.summary ? " · " + item.summary : "")),
|
|
42
|
+
render: options.render
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
footer.replaceChildren(note(t("activity.loaded") + " " + state.items.length + " / " + state.total));
|
|
47
|
+
if (!state.total) footer.append(note(t("activity.none")));
|
|
48
|
+
if (state.error) footer.append(note(state.error + " · " + t("activity.refreshHint"), "bad"));
|
|
49
|
+
if ((!state.started && state.items.length < state.total) || state.cursor) {
|
|
50
|
+
footer.append(button(t("activity.more") + " (" + Math.max(0, state.total - state.items.length) + ")", {
|
|
51
|
+
disabled: state.pending, onClick: function () { load(state.started); }
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
footer.append(button(t("activity.refresh"), { disabled: state.pending, onClick: function () { load(false); } }));
|
|
55
|
+
element.dataset.reading = String(state.pending);
|
|
56
|
+
}
|
|
57
|
+
async function load(more) {
|
|
58
|
+
if (state.pending) return;
|
|
59
|
+
const busy = function () { return !!rows.querySelector('[data-unsent="true"], [data-reading="true"]'); };
|
|
60
|
+
if (busy()) { state.error = t("activity.busy"); draw(false); return; }
|
|
61
|
+
state.pending = true;
|
|
62
|
+
draw(false);
|
|
63
|
+
try {
|
|
64
|
+
const page = await actions.list(data.task.id, store, { view: "current", limit: 20, cursor: more ? state.cursor : null });
|
|
65
|
+
if (busy()) throw new Error(t("activity.busy"));
|
|
66
|
+
if (more || !state.started || state.digest !== page.digest) {
|
|
67
|
+
state.items = more ? state.items.concat(page.items) : page.items;
|
|
68
|
+
state.cursor = page.nextCursor;
|
|
69
|
+
}
|
|
70
|
+
state.digest = page.digest; state.total = page.total; state.started = true; state.error = null;
|
|
71
|
+
} catch (error) { state.error = error.message; }
|
|
72
|
+
finally { state.pending = false; draw(!state.error); }
|
|
73
|
+
}
|
|
74
|
+
draw();
|
|
75
|
+
if (revalidate) load(false);
|
|
76
|
+
return element;
|
|
77
|
+
}
|
|
10
78
|
|
|
11
79
|
// A row drawn from its exact value: synchronously when cached, otherwise the
|
|
12
80
|
// summary card until the read returns and replaces it in place.
|
|
@@ -16,7 +16,7 @@ export function runCard(run, t, locale) {
|
|
|
16
16
|
run.purpose ? chip(label(t, "run.purpose", run.purpose)) : null,
|
|
17
17
|
run.workItemId ? chip(run.workItemId) : null,
|
|
18
18
|
h("span.spacer"),
|
|
19
|
-
statusBadge(t, "run", "run", run.status)), runBody(run, t, locale));
|
|
19
|
+
statusBadge(t, "run", "run", run.status)), ...runBody(run, t, locale));
|
|
20
20
|
return element;
|
|
21
21
|
}
|
|
22
22
|
|
|
@@ -107,7 +107,8 @@ export function roleCard(role, t, locale, options) {
|
|
|
107
107
|
function roleFacts(role, t, locale, opts) {
|
|
108
108
|
const session = opts.runtimeRole && opts.runtimeRole.runtimeSession;
|
|
109
109
|
const rows = [];
|
|
110
|
-
rows.push([t("role.session"), session && session.nativeSessionId ? mono(session.nativeSessionId)
|
|
110
|
+
rows.push([t("role.session"), session && session.nativeSessionId ? mono(session.nativeSessionId)
|
|
111
|
+
: !opts.runtimeRole ? t("activity.notLoaded") : t("role.noSession")]);
|
|
111
112
|
if (role.launchRevision !== undefined) rows.push([t("role.desired"), "r" + role.launchRevision + (opts.launchDrift ? " · " + t("role.drift") : "")]);
|
|
112
113
|
if (role.defaultAccess !== undefined) rows.push([t("role.access"), role.defaultAccess]);
|
|
113
114
|
if (opts.retry) rows.push([t("role.providerRetry"), label(t, "retry", opts.retry.status) + " · "
|
|
@@ -22,7 +22,7 @@ export function label(t, prefix, value) {
|
|
|
22
22
|
const TONES = {
|
|
23
23
|
task: { draft: "warn", active: "info", completed: "ok", cancelled: "idle", archived: "idle" },
|
|
24
24
|
exec: {
|
|
25
|
-
"needs-leader-action": "accent", "waiting-on-agents": "info", "waiting-user": "warn",
|
|
25
|
+
"needs-leader-action": "accent", "waiting-on-agents": "info", "waiting-on-jobs": "info", "waiting-user": "warn",
|
|
26
26
|
recovering: "warn", attention: "bad", "progressing-with-attention": "warn", blocked: "bad",
|
|
27
27
|
conflicted: "warn", working: "info", completed: "ok", retired: "idle", cancelled: "idle",
|
|
28
28
|
accepted: "ok", open: "info", archived: "idle"
|
|
@@ -58,7 +58,10 @@ export const EN = {
|
|
|
58
58
|
"create.hint": "Planning saves a Draft and queues the Leader. It does not start development. Uncheck to save only.",
|
|
59
59
|
"create.open": "Open saved task",
|
|
60
60
|
"workbench.next": "Next step",
|
|
61
|
-
"workbench.continue": "
|
|
61
|
+
"workbench.continue": "Talk to Leader",
|
|
62
|
+
"workbench.prompt.running": "Talk to the current Leader to adjust direction or add information.",
|
|
63
|
+
"workbench.prompt.activate": "Start the Task once the draft and plan are confirmed, or keep discussing with the Leader.",
|
|
64
|
+
"workbench.prompt.archive": "This Task has ended. Archive it, or follow up with the Leader.",
|
|
62
65
|
"workbench.activate": "Start task",
|
|
63
66
|
"workbench.activateConfirm": "Have you confirmed the Draft and plan? Request activation now?",
|
|
64
67
|
"workbench.activateHint": "Uses the existing activation process. A saved request is not an active Task.",
|
|
@@ -700,6 +703,19 @@ export const EN = {
|
|
|
700
703
|
"priority.low": "Low",
|
|
701
704
|
"exec.status.needs-leader-action": "Needs Leader action",
|
|
702
705
|
"exec.status.waiting-on-agents": "Waiting on agents",
|
|
706
|
+
"exec.status.waiting-on-jobs": "Waiting on background jobs",
|
|
707
|
+
"exec.action.wait-for-jobs": "Wait for background jobs",
|
|
708
|
+
"activity.loaded": "Loaded",
|
|
709
|
+
"activity.none": "No current items in this source.",
|
|
710
|
+
"activity.notLoaded": "Not loaded. Use the current activity list to read more.",
|
|
711
|
+
"activity.more": "Load more",
|
|
712
|
+
"activity.refresh": "Refresh list",
|
|
713
|
+
"activity.refreshHint": "If the source changed, refresh before continuing.",
|
|
714
|
+
"activity.busy": "Finish or clear the unsent answer, or wait for the current read before replacing this list.",
|
|
715
|
+
"activity.reviews": "Unfinished reviews",
|
|
716
|
+
"activity.native": "Native activity",
|
|
717
|
+
"activity.openSession": "Open current Role Session",
|
|
718
|
+
"activity.scope": "Partial coverage: selected native Sessions and retained, exactly identified child observations. Not a live process or complete historical Session inventory. Native facts may describe an AgentRun above; counts must not be added together.",
|
|
703
719
|
"exec.status.waiting-user": "Waiting on you",
|
|
704
720
|
"exec.status.recovering": "Recovering",
|
|
705
721
|
"exec.status.attention": "Needs attention",
|
|
@@ -56,7 +56,10 @@ export const ZH_CN = {
|
|
|
56
56
|
"create.hint": "保存 Draft 并通知 Leader 规划,不启动开发。取消勾选则仅保存任务。",
|
|
57
57
|
"create.open": "打开已保存任务",
|
|
58
58
|
"workbench.next": "下一步",
|
|
59
|
-
"workbench.continue": "
|
|
59
|
+
"workbench.continue": "与 Leader 对话",
|
|
60
|
+
"workbench.prompt.running": "需要调整方向或补充信息时,直接与当前 Leader 对话。",
|
|
61
|
+
"workbench.prompt.activate": "确认草稿与方案后启动任务,或继续与 Leader 讨论。",
|
|
62
|
+
"workbench.prompt.archive": "任务已结束。可以归档,或继续与 Leader 跟进。",
|
|
60
63
|
"workbench.activate": "启动任务",
|
|
61
64
|
"workbench.activateConfirm": "已确认草稿和方案,申请激活此任务?",
|
|
62
65
|
"workbench.activateHint": "复用现有激活流程。请求已保存不代表任务已激活。",
|
|
@@ -698,6 +701,19 @@ export const ZH_CN = {
|
|
|
698
701
|
"priority.low": "低",
|
|
699
702
|
"exec.status.needs-leader-action": "待 Leader 处理",
|
|
700
703
|
"exec.status.waiting-on-agents": "等待智能体",
|
|
704
|
+
"exec.status.waiting-on-jobs": "等待后台任务",
|
|
705
|
+
"exec.action.wait-for-jobs": "等待后台任务结果",
|
|
706
|
+
"activity.loaded": "已加载",
|
|
707
|
+
"activity.none": "此来源没有当前事项。",
|
|
708
|
+
"activity.notLoaded": "尚未加载;可通过当前活动列表继续读取。",
|
|
709
|
+
"activity.more": "继续加载",
|
|
710
|
+
"activity.refresh": "刷新列表",
|
|
711
|
+
"activity.refreshHint": "若来源已变化,请刷新后继续。",
|
|
712
|
+
"activity.busy": "请先完成或清空未发送的回答,或等待当前读取结束,再替换列表。",
|
|
713
|
+
"activity.reviews": "未结束的审查",
|
|
714
|
+
"activity.native": "原生活动",
|
|
715
|
+
"activity.openSession": "打开该角色的当前会话",
|
|
716
|
+
"activity.scope": "部分覆盖:选中的原生会话,以及身份明确、尚未结束的保留子活动观测;不是实时进程或完整历史会话清单。原生事实可能与上方 AgentRun 描述同一执行,计数不可直接相加。",
|
|
701
717
|
"exec.status.waiting-user": "等待你回答",
|
|
702
718
|
"exec.status.recovering": "恢复中",
|
|
703
719
|
"exec.status.attention": "需要关注",
|