@zq-silk/yui 1.1.0 → 2.0.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/ARCHITECTURE.md +2 -1
- package/ARCHITECTURE.zh-CN.md +2 -1
- package/README.md +3 -1
- package/dist/cli/commandCatalog.js +66 -54
- package/dist/cli/managedDiagnostics.js +2 -1
- package/dist/cli/updatePorts.js +0 -5
- package/dist/cli.js +113 -107
- package/dist/commands/controllerCommands.js +2 -1
- package/dist/commands/globalRoleCommands.js +84 -43
- package/dist/commands/grantCommands.js +48 -8
- package/dist/commands/taskCommands.js +146 -196
- package/dist/commands/taskContextCommand.js +29 -6
- package/dist/commands/taskFactCommands.js +10 -69
- package/dist/commands/taskInputCommands.js +26 -35
- package/dist/commands/taskPublicationCommands.js +2 -35
- package/dist/commands/taskPublicationVerifyCommand.js +1 -2
- package/dist/commands/taskUpstreamCommands.js +2 -1
- package/dist/context/runContextPack.js +8 -29
- package/dist/context/sessionBootstrapManifest.js +5 -101
- package/dist/context/taskContext.js +98 -39
- package/dist/controller/clientRuntime.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
- package/dist/controller/runtime.js +6 -0
- package/dist/core/controllerClient.js +46 -21
- package/dist/core/protocol.js +12 -7
- package/dist/doctor/doctor.js +6 -3
- package/dist/doctor/ptyProbe.js +74 -0
- package/dist/doctor/ptyProbeChild.js +90 -0
- package/dist/errors/cliError.js +4 -4
- package/dist/errors/cliFailure.js +213 -0
- package/dist/executor/fileRoleLaunchPlanner.js +1 -4
- package/dist/grant/capabilityGrant.js +13 -0
- package/dist/grant/taskAuthorization.js +127 -0
- package/dist/kernel/builtinCapabilities.js +60 -8
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/output/boundedRead.js +118 -0
- package/dist/release/releaseWorkflowEngine.js +8 -1
- package/dist/runtime/managedCaller.js +2 -2
- package/dist/runtime/runtimeCoherence.js +10 -8
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +3 -0
- package/dist/task/leaderArchive.js +111 -0
- package/dist/task/leaderArchiveAuthority.js +24 -0
- package/dist/tmux/commandExecutor.js +7 -5
- package/docs/architecture/README.md +1 -0
- package/docs/architecture/README.zh-CN.md +1 -0
- package/docs/cli-information-contract.md +107 -0
- package/docs/cli-information-contract.zh-CN.md +81 -0
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
- package/docs/plugin-sdk.md +7 -3
- package/docs/plugin-sdk.zh-CN.md +5 -2
- package/docs/release-workflow.md +19 -9
- package/docs/release-workflow.zh-CN.md +14 -7
- package/docs/storage-baseline.md +5 -0
- package/docs/testing/verification-levels.md +12 -2
- package/docs/testing/verification-levels.zh-CN.md +8 -0
- package/i18n/README.zh-CN.md +3 -1
- package/package.json +3 -3
- package/skills/yui-leader/SKILL.md +8 -1
- package/skills/yui-leader/references/authorization.md +64 -0
- package/skills/yui-leader/references/execution.md +18 -4
- package/skills/yui-leader/references/task-plugins.md +4 -2
- package/skills/yui-operator/SKILL.md +9 -2
- package/skills/yui-runtime/SKILL.md +51 -9
- package/skills/yui-runtime/references/publication.md +3 -1
|
@@ -5,14 +5,15 @@ import { expandTaskMessageResult } from "../message/message.js";
|
|
|
5
5
|
import { resolveManagedTaskReader } from "../runtime/managedCaller.js";
|
|
6
6
|
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
7
7
|
import { projectTaskRemoteDeliveryFromStore } from "../task/remoteDeliveryService.js";
|
|
8
|
+
import { retiredTaskRecordIds, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
|
|
8
9
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
9
10
|
import { contextContentDigest } from "./contextSnapshot.js";
|
|
10
11
|
import { buildRunContextPack } from "./runContextPack.js";
|
|
11
12
|
import { sourceRunContextValue } from "./sourceRunContext.js";
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
13
|
+
import { boundedDocument, recordPage } from "../output/boundedRead.js";
|
|
14
|
+
const MAX_RECORDS = 64;
|
|
15
|
+
const MAX_VALUE_BYTES = 2048;
|
|
16
|
+
const MAX_PAGE_BYTES = 24 * 1024;
|
|
16
17
|
const MAX_EVENTS = 100;
|
|
17
18
|
const MAX_ATTENTION_REFS = 8;
|
|
18
19
|
/** A current read model, not a second snapshot store or a delivery receipt.
|
|
@@ -21,7 +22,7 @@ const MAX_ATTENTION_REFS = 8;
|
|
|
21
22
|
export function readTaskContext(store, taskId, environment = {}) {
|
|
22
23
|
return store.readTransaction((reader) => {
|
|
23
24
|
const scope = authorizeContext(reader, taskId, environment);
|
|
24
|
-
const sources = authorizedSources(reader, scope);
|
|
25
|
+
const sources = authorizedSources(reader, scope, true);
|
|
25
26
|
const count = sources.reduce((total, source) => total + source.count, 0);
|
|
26
27
|
const cursor = currentCursor(reader, taskId);
|
|
27
28
|
// Reserve a small, classified view before ordinary history consumes the
|
|
@@ -34,10 +35,12 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
34
35
|
for (const entry of source.read()) {
|
|
35
36
|
if (records.length >= MAX_RECORDS)
|
|
36
37
|
break page;
|
|
38
|
+
if (records.filter(record => record.ref.store === source.family).length >= 8)
|
|
39
|
+
continue;
|
|
37
40
|
if (entry.ref.store === "run")
|
|
38
41
|
runtimeEvents ??= reader.listEventsByType(taskId, ["runtime.agent-error"]);
|
|
39
42
|
const valueBytes = Buffer.byteLength(JSON.stringify(entry.value));
|
|
40
|
-
const content = valueBytes > MAX_VALUE_BYTES
|
|
43
|
+
const content = valueBytes > MAX_VALUE_BYTES || ["task-message", "run", "work-item", "project-knowledge"].includes(entry.ref.store)
|
|
41
44
|
? { ref: entry.ref, summary: summarize(entry.value), omitted: true }
|
|
42
45
|
: { ...entry, omitted: false };
|
|
43
46
|
const record = { ...content, ...(entry.ref.store === "run" ? {
|
|
@@ -53,11 +56,20 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
53
56
|
}
|
|
54
57
|
return {
|
|
55
58
|
taskId, coreCursor: encode(cursor), throughCursor: encode(cursor),
|
|
59
|
+
view: "current",
|
|
60
|
+
collections: Object.entries(sources.reduce((counts, source) => {
|
|
61
|
+
counts[source.family] = (counts[source.family] ?? 0) + source.count;
|
|
62
|
+
return counts;
|
|
63
|
+
}, {})).map(([store, total]) => ({
|
|
64
|
+
store, total, returned: records.filter(record => record.ref.store === store).length
|
|
65
|
+
})),
|
|
66
|
+
discovery: `task context list ${taskId} --store <store>`,
|
|
67
|
+
detail: `task context inspect ${taskId} --store <store> --ref <refId> --digest <digest>`,
|
|
56
68
|
attention,
|
|
57
69
|
records, count,
|
|
58
70
|
omitted: { records: count - records.length, values: records.filter((r) => r.omitted).length },
|
|
59
71
|
observations: [],
|
|
60
|
-
limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: MAX_PAGE_BYTES,
|
|
72
|
+
limits: { records: MAX_RECORDS, valueBytes: MAX_VALUE_BYTES, pageBytes: 32 * 1024, recordBytes: MAX_PAGE_BYTES,
|
|
61
73
|
attentionRefsPerCategory: MAX_ATTENTION_REFS, attentionRefBytesPerCategory: MAX_VALUE_BYTES }
|
|
62
74
|
};
|
|
63
75
|
});
|
|
@@ -65,15 +77,16 @@ export function readTaskContext(store, taskId, environment = {}) {
|
|
|
65
77
|
function summarizeAttention(store, taskId, allow) {
|
|
66
78
|
const group = () => ({ count: 0, refs: [], omittedRefs: 0 });
|
|
67
79
|
const attention = {
|
|
68
|
-
openInputs: group(), pendingOperations: group(), unknownOperations: group()
|
|
80
|
+
openInputs: group(), pendingOperations: group(), unknownOperations: group(), messages: group()
|
|
69
81
|
};
|
|
70
82
|
for (const [category, family, statuses] of [
|
|
71
83
|
["openInputs", "input-request", ["open"]],
|
|
72
84
|
["pendingOperations", "job", ["queued", "running"]],
|
|
73
|
-
["unknownOperations", "job", ["unknown-needs-attention"]]
|
|
85
|
+
["unknownOperations", "job", ["unknown-needs-attention"]],
|
|
86
|
+
["messages", "task-message", undefined]
|
|
74
87
|
]) {
|
|
75
88
|
const page = store.queryContextRecords(taskId, {
|
|
76
|
-
family, statuses, ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
|
|
89
|
+
family, statuses, descending: category === "messages", ids: allowedIds(allow, family), limit: MAX_ATTENTION_REFS
|
|
77
90
|
});
|
|
78
91
|
const summary = attention[category];
|
|
79
92
|
summary.count = page.count;
|
|
@@ -152,22 +165,58 @@ export function inspectTaskContext(store, taskId, selector, environment = {}) {
|
|
|
152
165
|
currentRef: entry.ref
|
|
153
166
|
});
|
|
154
167
|
}
|
|
155
|
-
if (Buffer.byteLength(JSON.stringify(entry.value)) > MAX_INSPECT_BYTES) {
|
|
156
|
-
throw usageError("Context value exceeds the bounded inspect limit.", undefined, { ref: entry.ref, maxBytes: MAX_INSPECT_BYTES });
|
|
157
|
-
}
|
|
158
168
|
const expansion = selector.store === "task-message"
|
|
159
169
|
? expandTaskMessageResult(value, (task, run) => reader.getRun(task, run))
|
|
160
170
|
: undefined;
|
|
161
171
|
const response = { ...entry, ...(expansion !== undefined && "result" in expansion
|
|
162
172
|
? { result: expansion.result } : {}),
|
|
163
173
|
...(selector.store === "run" ? { execution: runExecutionObservation(value, reader.getTaskRoleSessionSet(taskId, value.roleName)?.providerBinding, reader.listEventsByType(taskId, ["runtime.agent-error"])) } : {}),
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
174
|
+
};
|
|
175
|
+
return boundedDocument(response, `task:${taskId}/${selector.store}/${selector.refId}`, selector.cursor);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** Discover one authorized family without copying its bodies or unrelated Task
|
|
179
|
+
* state. Content-version cursors fail explicitly if the collection changes.
|
|
180
|
+
*/
|
|
181
|
+
export function listTaskContext(store, taskId, family, options = {}, environment = {}) {
|
|
182
|
+
return store.readTransaction(reader => {
|
|
183
|
+
const scope = authorizeContext(reader, taskId, environment);
|
|
184
|
+
const sources = authorizedSources(reader, scope, false, family);
|
|
185
|
+
if (!CONTEXT_FAMILIES.includes(family))
|
|
186
|
+
throw usageError(`Unknown Context store: ${family}.`);
|
|
187
|
+
if (options.after !== undefined && !Number.isFinite(Date.parse(options.after)))
|
|
188
|
+
throw usageError("Invalid --after timestamp.");
|
|
189
|
+
const retiredKind = family === "task-message" ? "message" : family === "run" ? "run" : undefined;
|
|
190
|
+
const retired = retiredKind === undefined ? undefined
|
|
191
|
+
: retiredTaskRecordIds(reader.listEventsByType(taskId, [TASK_RECORD_RETIRED_EVENT]), retiredKind);
|
|
192
|
+
const records = [];
|
|
193
|
+
for (const source of sources) {
|
|
194
|
+
for (let offset = 0; offset < source.count; offset += MAX_RECORDS) {
|
|
195
|
+
for (const entry of source.read(offset)) {
|
|
196
|
+
const value = entry.value;
|
|
197
|
+
if (options.status !== undefined && value.status !== options.status
|
|
198
|
+
|| options.workItemId !== undefined && value.workItemId !== options.workItemId
|
|
199
|
+
|| options.after !== undefined && Date.parse(String(value.createdAt)) <= Date.parse(options.after))
|
|
200
|
+
continue;
|
|
201
|
+
records.push({ ref: entry.ref, summary: summarize(value),
|
|
202
|
+
...(retired === undefined ? {} : { history: retired.has(entry.ref.refId) ? "retired" : "active" }),
|
|
203
|
+
...Object.fromEntries(["status", "kind", "type", "createdAt", "roleName", "assignee", "purpose", "workItemId", "reviewRoundId"]
|
|
204
|
+
.filter(key => typeof value[key] === "string").map(key => [key, value[key]])) });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
167
207
|
}
|
|
168
|
-
return
|
|
208
|
+
return recordPage(records, `task:${taskId}/${family}/${JSON.stringify({
|
|
209
|
+
status: options.status, after: options.after, workItemId: options.workItemId
|
|
210
|
+
})}`, options);
|
|
169
211
|
});
|
|
170
212
|
}
|
|
213
|
+
export const CONTEXT_FAMILIES = [
|
|
214
|
+
"task", "task-brief", "remote-delivery", "role", "role-profile",
|
|
215
|
+
"project-policy", "project-knowledge", "input-request", "work-item",
|
|
216
|
+
"accepted-work-item", "candidate", "environment-preparation", "managed-workspace",
|
|
217
|
+
"change-set", "task-message", "run", "source-run", "review-round", "mailbox",
|
|
218
|
+
"task-decision", "task-milestone", "job", "publication", "task-event"
|
|
219
|
+
];
|
|
171
220
|
export function listContextMessages(store, taskId, environment = {}) {
|
|
172
221
|
return store.readTransaction((reader) => {
|
|
173
222
|
const { allow } = authorizeContext(reader, taskId, environment);
|
|
@@ -241,7 +290,7 @@ function authorizeContext(store, taskId, environment) {
|
|
|
241
290
|
if (caller.currentRunId === undefined)
|
|
242
291
|
throw usageError("A managed Role needs a current AgentRun to read its scoped Context.");
|
|
243
292
|
const pack = buildRunContextPack(store, taskId, caller.currentRunId);
|
|
244
|
-
allow = new Set(pack.
|
|
293
|
+
allow = new Set(pack.pointers.map((ref) => `${ref.store}:${ref.refId}`));
|
|
245
294
|
allow.add(`role:${caller.roleName}`);
|
|
246
295
|
allow.add(`turn:${caller.currentRunId}`);
|
|
247
296
|
// Shared current user intent and this exact Assignment's live steers augment
|
|
@@ -389,31 +438,35 @@ function inspectValue(store, { task, allow, caller }, { store: family, refId })
|
|
|
389
438
|
default: return null;
|
|
390
439
|
}
|
|
391
440
|
}
|
|
392
|
-
function authorizedSources(store, { task, allow, caller }) {
|
|
441
|
+
function authorizedSources(store, { task, allow, caller }, current = false, requestedFamily) {
|
|
393
442
|
const taskId = task.id;
|
|
394
443
|
const sources = [];
|
|
395
444
|
const add = (family, id, value) => {
|
|
396
|
-
if (value !== null && isAllowed(allow, family, id)) {
|
|
397
|
-
sources.push({ count: 1, read: () => [materialize(family, id, value)] });
|
|
445
|
+
if ((requestedFamily === undefined || requestedFamily === family) && value !== null && isAllowed(allow, family, id)) {
|
|
446
|
+
sources.push({ family, count: 1, read: () => [materialize(family, id, value)] });
|
|
398
447
|
}
|
|
399
448
|
};
|
|
400
449
|
const query = (family, options = {}) => {
|
|
450
|
+
if (requestedFamily !== undefined && requestedFamily !== family)
|
|
451
|
+
return;
|
|
401
452
|
const selection = { ...options, family, ids: allowedIds(allow, family) };
|
|
402
453
|
const { count } = store.queryContextRecords(taskId, { ...selection, limit: 0 });
|
|
403
|
-
sources.push({ count, *read() {
|
|
454
|
+
sources.push({ family, count, *read(offset = 0) {
|
|
404
455
|
for (const { id, value } of store.queryContextRecords(taskId, {
|
|
405
|
-
...selection, limit: MAX_RECORDS
|
|
456
|
+
...selection, offset, limit: current ? 8 : MAX_RECORDS
|
|
406
457
|
}).records)
|
|
407
458
|
yield materialize(family, id, value);
|
|
408
459
|
} });
|
|
409
460
|
};
|
|
410
461
|
add("task", taskId, task);
|
|
411
462
|
add("task-brief", taskId, store.getTaskBrief(taskId));
|
|
412
|
-
if (allow === undefined)
|
|
463
|
+
if (allow === undefined && (requestedFamily === undefined || requestedFamily === "remote-delivery")) {
|
|
413
464
|
add("remote-delivery", taskId, projectTaskRemoteDeliveryFromStore(store, task));
|
|
465
|
+
}
|
|
414
466
|
for (const role of store.listRoles(taskId)) {
|
|
415
467
|
add("role", role.name, role);
|
|
416
|
-
|
|
468
|
+
if (!current)
|
|
469
|
+
add("role-profile", role.name, roleProfile(role));
|
|
417
470
|
}
|
|
418
471
|
for (const binding of task.projectBindings) {
|
|
419
472
|
const project = store.getProject(binding.projectId);
|
|
@@ -427,42 +480,48 @@ function authorizedSources(store, { task, allow, caller }) {
|
|
|
427
480
|
}
|
|
428
481
|
// Current responsibilities and unanswered inputs precede historical bulk.
|
|
429
482
|
query("input-request", { statuses: ["open"] });
|
|
430
|
-
query("work-item");
|
|
483
|
+
query("work-item", current ? { excludeStatuses: ["accepted", "retired"] } : {});
|
|
431
484
|
for (const id of allowedIds(allow, "accepted-work-item") ?? []) {
|
|
432
485
|
add("accepted-work-item", id, store.getWorkItem(taskId, id));
|
|
433
486
|
}
|
|
434
|
-
|
|
487
|
+
if (!current)
|
|
488
|
+
query("candidate");
|
|
435
489
|
// File artifacts are not enumerated into the synchronous Context listing: the
|
|
436
490
|
// core cursor does not cover file edits and an ambient directory index here
|
|
437
491
|
// would be the forbidden update-time mirror (§3.7). Current artifacts are
|
|
438
492
|
// listed on demand via the async `artifact.list` capability; frozen Candidate
|
|
439
493
|
// artifacts remain reachable as commit-pinned pointers under their Candidate.
|
|
440
|
-
|
|
494
|
+
if (!current)
|
|
495
|
+
query("environment-preparation");
|
|
441
496
|
query("managed-workspace");
|
|
442
497
|
if (caller?.currentRunId !== undefined) {
|
|
443
498
|
add("managed-workspace", `${taskId}/${caller.roleName}`, store.getRun(taskId, caller.currentRunId)?.workspace ?? null);
|
|
444
499
|
}
|
|
445
|
-
|
|
500
|
+
if (!current)
|
|
501
|
+
query("change-set");
|
|
446
502
|
query("task-message", { descending: true });
|
|
447
|
-
query("run", { descending: true });
|
|
503
|
+
query("run", { descending: true, ...(current ? { statuses: ["active"] } : {}) });
|
|
448
504
|
for (const id of allowedIds(allow, "source-turn") ?? []) {
|
|
449
505
|
const run = store.getRun(taskId, id);
|
|
450
506
|
if (run !== null)
|
|
451
507
|
add("source-run", id, sourceRunContextValue(run));
|
|
452
508
|
}
|
|
453
|
-
query("review-round", { descending: true });
|
|
454
|
-
|
|
455
|
-
|
|
509
|
+
query("review-round", { descending: true, ...(current ? { statuses: ["pending", "running"] } : {}) });
|
|
510
|
+
if (!current)
|
|
511
|
+
query("input-request", { excludeStatuses: ["open"] });
|
|
512
|
+
if (!current && allow === undefined) {
|
|
456
513
|
add("mailbox", "task", store.getWorkMailbox({ kind: "task", taskId }));
|
|
457
514
|
for (const role of store.listRoles(taskId)) {
|
|
458
515
|
add("mailbox", `role:${role.name}`, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }));
|
|
459
516
|
}
|
|
460
517
|
}
|
|
461
|
-
query("task-decision");
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
518
|
+
query("task-decision", current ? { statuses: ["active"] } : {});
|
|
519
|
+
if (!current) {
|
|
520
|
+
query("task-milestone");
|
|
521
|
+
query("publication");
|
|
522
|
+
query("task-event", { descending: true });
|
|
523
|
+
}
|
|
524
|
+
query("job", current ? { statuses: ["queued", "running", "unknown-needs-attention"] } : {});
|
|
466
525
|
return sources;
|
|
467
526
|
}
|
|
468
527
|
export function materialize(store, refId, value) {
|
|
@@ -512,7 +571,7 @@ function freeze(value) {
|
|
|
512
571
|
}
|
|
513
572
|
function summarize(value) {
|
|
514
573
|
const record = value;
|
|
515
|
-
return ["title", "objective", "summary", "body", "status"]
|
|
574
|
+
return ["title", "objective", "question", "summary", "body", "status", "type"]
|
|
516
575
|
.flatMap((key) => typeof record[key] === "string" ? [`${key}: ${record[key]}`] : [])
|
|
517
576
|
.join("; ").slice(0, 400);
|
|
518
577
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { describeCliFailure } from "../errors/cliFailure.js";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { findLiveControllerProcessForHome, inspectLiveControllerProcess } from "../core/controllerProcessIdentity.js";
|
|
4
5
|
import { readHomeFilesystemId } from "../core/homeFilesystemIdentity.js";
|
|
@@ -77,7 +78,7 @@ export async function ensureFileTaskController(home, options = {}) {
|
|
|
77
78
|
? findLiveControllerProcessForHome(readHomeFilesystemId(home))
|
|
78
79
|
: undefined;
|
|
79
80
|
const startupPid = existing?.pid
|
|
80
|
-
?? spawnController(home, controllerSpawnEnvironment(home, options.environment ?? process.env));
|
|
81
|
+
?? await spawnController(home, controllerSpawnEnvironment(home, options.environment ?? process.env));
|
|
81
82
|
const deadline = Date.now() + timeoutMs;
|
|
82
83
|
for (;;) {
|
|
83
84
|
try {
|
|
@@ -129,15 +130,15 @@ function assertControllerRuntimeProcessIdentity(value, expected) {
|
|
|
129
130
|
|| actual.controllerProtocolVersion !== expected.controllerProtocolVersion
|
|
130
131
|
|| actual.storageVersion !== expected.storageVersion
|
|
131
132
|
|| actual.minimumStorageVersion !== expected.minimumStorageVersion) {
|
|
132
|
-
throw new
|
|
133
|
+
throw new ControllerClientError("CONTROLLER_IDENTITY_MISMATCH", "Authenticated Controller runtime identity does not match the captured executable, argv, package, protocol and storage versions; refusing readiness.");
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
function assertCompatibleControllerStatus(status, expectedIdentity) {
|
|
136
137
|
const statusRecord = isJsonRecord(status) && status.running === true ? status : null;
|
|
137
138
|
const actual = statusRecord?.protocolVersion;
|
|
138
139
|
if (statusRecord === null || actual !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION) {
|
|
139
|
-
throw new
|
|
140
|
-
+ "
|
|
140
|
+
throw new ControllerClientError("CONTROLLER_PROTOCOL_MISMATCH", `Controller protocol is incompatible (expected ${FILE_TASK_CONTROLLER_PROTOCOL_VERSION}, found ${typeof actual === "number" ? actual : "unknown"}). `
|
|
141
|
+
+ "Verify Home, CLI and Controller identity before an authorized restart.");
|
|
141
142
|
}
|
|
142
143
|
const actualVersion = statusRecord.version;
|
|
143
144
|
const current = yuiVersionIdentity();
|
|
@@ -146,14 +147,21 @@ function assertCompatibleControllerStatus(status, expectedIdentity) {
|
|
|
146
147
|
version: typeof actualVersion === "string" ? actualVersion : current.version
|
|
147
148
|
} : parseControllerIdentity(expectedIdentity));
|
|
148
149
|
}
|
|
149
|
-
function spawnDetachedFileTaskController(_home, environment) {
|
|
150
|
+
async function spawnDetachedFileTaskController(_home, environment) {
|
|
150
151
|
const child = spawn(process.execPath, [fileURLToPath(new URL("./controllerMain.js", import.meta.url))], {
|
|
151
152
|
env: environment,
|
|
152
153
|
detached: true,
|
|
153
154
|
stdio: "ignore"
|
|
154
155
|
});
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// Route asynchronous spawn failures back through the command's normal error
|
|
157
|
+
// boundary instead of Node's unhandled "error" event / raw stack output.
|
|
158
|
+
return await new Promise((resolve, reject) => {
|
|
159
|
+
child.once("error", error => reject(new Error("Controller process could not start.", { cause: error })));
|
|
160
|
+
child.once("spawn", () => {
|
|
161
|
+
child.unref();
|
|
162
|
+
resolve(child.pid);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
157
165
|
}
|
|
158
166
|
/** Stops the per-home Controller and waits until its owned discovery is gone. */
|
|
159
167
|
export async function stopFileTaskController(home, options = {}) {
|
|
@@ -398,7 +406,7 @@ function classifyRefreshFailure(error, options) {
|
|
|
398
406
|
options.onError?.(error);
|
|
399
407
|
return {
|
|
400
408
|
status: "failed",
|
|
401
|
-
message: error
|
|
409
|
+
message: describeCliFailure(error).message
|
|
402
410
|
};
|
|
403
411
|
}
|
|
404
412
|
/** Foreground command bridge. It never reads or writes Agent terminal bytes. */
|
|
@@ -6,6 +6,7 @@ import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
|
6
6
|
import { interruptThenTerminalState, prepareMessageContinuations } from "../message/messageContinuation.js";
|
|
7
7
|
import { assertExecutionEnvironmentCurrent } from "../runtime/executionEnvironment.js";
|
|
8
8
|
import { deferProviderRetry, providerRetryPending, providerRetryProjection, recordProviderFailure } from "../runtime/providerRetry.js";
|
|
9
|
+
import { createTaskMessage } from "../message/message.js";
|
|
9
10
|
import { AgentHostObservationDeferred, recordAgentHostConnection, resolveAgentHostObservation } from "./agentHostObservation.js";
|
|
10
11
|
import { admitOwnedProviderInput, ownedProviderInput } from "./providerRetryAdmission.js";
|
|
11
12
|
import { RuntimeHookRunFenceError } from "./runtimeHookRunFence.js";
|
|
@@ -1243,6 +1244,29 @@ export class FileSchedulerStoreAdapter {
|
|
|
1243
1244
|
}
|
|
1244
1245
|
}
|
|
1245
1246
|
binding = admitOwnedProviderInput(store, binding, input);
|
|
1247
|
+
// The Host's human console is a user ingress. Persist its original text
|
|
1248
|
+
// before the Provider write; provider-visible userMessage items alone
|
|
1249
|
+
// are NOT proof of human authorship (managed prompts use those too).
|
|
1250
|
+
// This shares the exact writer/session admission and its transaction.
|
|
1251
|
+
if (input.authorityOwner === "human" && input.roleName === "leader"
|
|
1252
|
+
&& input.runId === undefined && input.boundedText !== undefined) {
|
|
1253
|
+
const prior = store.listEvents(input.taskId).find(event => event.type === "message.native-user-input" && event.payload.attemptId === input.attemptId);
|
|
1254
|
+
if (prior !== undefined) {
|
|
1255
|
+
const message = store.listMessages(input.taskId).find(entry => entry.id === prior.payload.messageId);
|
|
1256
|
+
if (message?.body !== input.boundedText || prior.payload.nativeSessionId !== input.nativeSessionId) {
|
|
1257
|
+
throw new AgentHostProviderTurnFenceError("Native user input identity was reused with different content.");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
else {
|
|
1261
|
+
const message = createTaskMessage(store.nextMessageId(input.taskId), input.taskId, input.boundedText, "user", { type: "user" }, input.now, { intent: "record" });
|
|
1262
|
+
store.saveMessage(input.taskId, message);
|
|
1263
|
+
store.saveEvent(input.taskId, createTaskEvent(store.nextEventId(input.taskId), input.taskId, "message.native-user-input", {
|
|
1264
|
+
messageId: message.id, attemptId: input.attemptId, nativeSessionId: input.nativeSessionId,
|
|
1265
|
+
authorityEpoch: String(input.authorityEpoch), holderId: input.holderId
|
|
1266
|
+
}, input.now));
|
|
1267
|
+
// No mailbox enqueue: this very input is already being delivered.
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1246
1270
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, beginProviderTurn(binding, {
|
|
1247
1271
|
...(input.runId === undefined ? {} : { runId: input.runId }),
|
|
1248
1272
|
attemptId: input.attemptId,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
import { archiveLeaderTask } from "../task/leaderArchive.js";
|
|
3
|
+
import { TaskWorkspaceCoordinator } from "../repository/taskWorkspaceCoordinator.js";
|
|
2
4
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, NATIVE_AGENT_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
3
5
|
import { runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
4
6
|
import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeoutSeconds, resolveControllerTaskConcurrency, resolveDeliveryTimeoutSeconds, resolveRuntimeHealth, resolveTmuxBin, resolveTmuxHistoryLimit } from "../config/yuiConfig.js";
|
|
@@ -359,6 +361,10 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
359
361
|
// the poll interval.
|
|
360
362
|
const kernel = createKernelPorts(store, createLinuxProcessPort(), (taskId) => {
|
|
361
363
|
runningRuntime?.signal(`task:${taskId}`);
|
|
364
|
+
}, [], async (taskId, environment, request) => {
|
|
365
|
+
if (!(workspacePreparer instanceof FileTaskWorkspacePreparer))
|
|
366
|
+
throw new Error("Archive workspace owner unavailable.");
|
|
367
|
+
return archiveLeaderTask(store, new TaskWorkspaceCoordinator(store, workspacePreparer, webWorkflow), taskId, environment, request);
|
|
362
368
|
});
|
|
363
369
|
closeKernel = () => kernel.close();
|
|
364
370
|
const webWorkflow = new FileTaskWorkflowRuntime(home, store, schedulerStore, planner, tmux, workspacePreparer, {
|
|
@@ -8,9 +8,11 @@ import { readHomeFilesystemId } from "./homeFilesystemIdentity.js";
|
|
|
8
8
|
import { CONTROLLER_DISCOVERY_PATH, ControllerProtocolError, encodeControllerRequest, FILE_TASK_CONTROLLER_PROTOCOL_VERSION, MAX_CONTROLLER_MESSAGE_BYTES, parseControllerDiscovery, parseControllerResponse } from "./protocol.js";
|
|
9
9
|
export class ControllerClientError extends Error {
|
|
10
10
|
code;
|
|
11
|
-
|
|
11
|
+
diagnostic;
|
|
12
|
+
constructor(code, message, cause, diagnostic) {
|
|
12
13
|
super(message, { cause });
|
|
13
14
|
this.code = code;
|
|
15
|
+
this.diagnostic = diagnostic;
|
|
14
16
|
this.name = "ControllerClientError";
|
|
15
17
|
}
|
|
16
18
|
}
|
|
@@ -33,12 +35,14 @@ export async function readControllerDiscovery(home) {
|
|
|
33
35
|
try {
|
|
34
36
|
const metadata = await lstat(discoveryPath);
|
|
35
37
|
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
36
|
-
if (!metadata.isFile()
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
if (!metadata.isFile())
|
|
39
|
+
throw invalidDiscovery(new Error("Discovery path is not a regular file."));
|
|
40
|
+
if ((metadata.mode & 0o077) !== 0)
|
|
41
|
+
throw invalidDiscovery(new Error("Discovery record permissions are not owner-only."));
|
|
42
|
+
if (metadata.size > 4_096)
|
|
43
|
+
throw invalidDiscovery(new Error("Discovery record exceeds the 4096-byte limit."));
|
|
44
|
+
if (uid !== undefined && metadata.uid !== uid)
|
|
45
|
+
throw invalidDiscovery(new Error("Discovery record belongs to a different user."));
|
|
42
46
|
const value = JSON.parse(await readFile(discoveryPath, "utf8"));
|
|
43
47
|
const socketPath = discoverySocketPath(value);
|
|
44
48
|
return parseControllerDiscovery(value, {
|
|
@@ -50,7 +54,7 @@ export async function readControllerDiscovery(home) {
|
|
|
50
54
|
if (error instanceof ControllerClientError)
|
|
51
55
|
throw error;
|
|
52
56
|
if (isNodeError(error) && error.code === "ENOENT") {
|
|
53
|
-
throw new ControllerClientError("CONTROLLER_NOT_RUNNING", withOriginalError("Controller is
|
|
57
|
+
throw new ControllerClientError("CONTROLLER_NOT_RUNNING", withOriginalError("Controller discovery record is missing; process state is unverified.", error), error, { delivery: "not-sent" });
|
|
54
58
|
}
|
|
55
59
|
if (isNodeError(error) && (error.code === "EPERM" || error.code === "EACCES")) {
|
|
56
60
|
throw controllerAccessDenied(error, "discovery record");
|
|
@@ -72,12 +76,22 @@ function discoverySocketPath(value) {
|
|
|
72
76
|
return socketPath;
|
|
73
77
|
}
|
|
74
78
|
export async function callController(home, method, params = {}, options = {}) {
|
|
75
|
-
const
|
|
79
|
+
const id = options.id ?? randomUUID();
|
|
80
|
+
let discovery;
|
|
81
|
+
try {
|
|
82
|
+
discovery = await readControllerDiscovery(home);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (!(error instanceof ControllerClientError))
|
|
86
|
+
throw error;
|
|
87
|
+
throw new ControllerClientError(error.code, error.message, error.cause, {
|
|
88
|
+
delivery: "not-sent", method, requestId: id
|
|
89
|
+
});
|
|
90
|
+
}
|
|
76
91
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
77
92
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
78
93
|
throw new TypeError("Controller timeout must be a positive integer.");
|
|
79
94
|
}
|
|
80
|
-
const id = options.id ?? randomUUID();
|
|
81
95
|
let requestLine;
|
|
82
96
|
try {
|
|
83
97
|
requestLine = encodeControllerRequest({
|
|
@@ -93,11 +107,13 @@ export async function callController(home, method, params = {}, options = {}) {
|
|
|
93
107
|
}
|
|
94
108
|
catch (error) {
|
|
95
109
|
if (error instanceof ControllerProtocolError) {
|
|
96
|
-
throw new ControllerClientError(error.code, error.message, error
|
|
110
|
+
throw new ControllerClientError(error.code, error.message, error, {
|
|
111
|
+
delivery: "not-sent", method, requestId: id
|
|
112
|
+
});
|
|
97
113
|
}
|
|
98
|
-
throw new ControllerClientError("INVALID_REQUEST", withOriginalError("Invalid controller request.", error), error);
|
|
114
|
+
throw new ControllerClientError("INVALID_REQUEST", withOriginalError("Invalid controller request.", error), error, { delivery: "not-sent", method, requestId: id });
|
|
99
115
|
}
|
|
100
|
-
return exchange(discovery.socketPath, requestLine, id, timeoutMs);
|
|
116
|
+
return exchange(discovery.socketPath, requestLine, id, timeoutMs, method);
|
|
101
117
|
}
|
|
102
118
|
/**
|
|
103
119
|
* Explicit restart recovery for an exact Controller whose discovery record
|
|
@@ -151,7 +167,7 @@ export async function stopOrphanedFileTaskController(home, timeoutMs, options =
|
|
|
151
167
|
}
|
|
152
168
|
return Object.freeze({ pid: candidate.pid });
|
|
153
169
|
}
|
|
154
|
-
function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
170
|
+
function exchange(socketPath, requestLine, expectedId, timeoutMs, method) {
|
|
155
171
|
return new Promise((resolve, reject) => {
|
|
156
172
|
const socket = createConnection(socketPath);
|
|
157
173
|
let buffer = Buffer.alloc(0);
|
|
@@ -174,7 +190,12 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
174
190
|
settled = true;
|
|
175
191
|
clearTimeout(timer);
|
|
176
192
|
socket.destroy();
|
|
177
|
-
reject(error
|
|
193
|
+
reject(new ControllerClientError(error.code, error.message, error.cause, {
|
|
194
|
+
delivery: error.diagnostic?.delivery ?? (deliveryStarted ? "unconfirmed" : "not-sent"),
|
|
195
|
+
method,
|
|
196
|
+
requestId: expectedId,
|
|
197
|
+
socketPath
|
|
198
|
+
}));
|
|
178
199
|
};
|
|
179
200
|
socket.on("connect", () => {
|
|
180
201
|
// Once write begins, a missing response cannot prove that the Controller
|
|
@@ -202,7 +223,9 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
202
223
|
if (response.ok)
|
|
203
224
|
finish(response.result);
|
|
204
225
|
else
|
|
205
|
-
fail(new ControllerClientError(response.error.code, response.error.message
|
|
226
|
+
fail(new ControllerClientError(response.error.code, response.error.message, undefined, {
|
|
227
|
+
delivery: "response-received"
|
|
228
|
+
}));
|
|
206
229
|
}
|
|
207
230
|
catch (error) {
|
|
208
231
|
fail(invalidResponse(error));
|
|
@@ -219,22 +242,22 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
219
242
|
}
|
|
220
243
|
export function classifyControllerSocketError(error, deliveryStarted) {
|
|
221
244
|
if (deliveryStarted) {
|
|
222
|
-
return new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", withOriginalError("Controller request delivery is unknown.", error), error);
|
|
245
|
+
return new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", withOriginalError("Controller request delivery is unknown.", error), error, { delivery: "unconfirmed" });
|
|
223
246
|
}
|
|
224
247
|
if (isNodeError(error) && (error.code === "EPERM" || error.code === "EACCES")) {
|
|
225
248
|
return controllerAccessDenied(error, "socket");
|
|
226
249
|
}
|
|
227
|
-
return new ControllerClientError("CONTROLLER_UNAVAILABLE", withOriginalError("Controller is unavailable.", error), error);
|
|
250
|
+
return new ControllerClientError("CONTROLLER_UNAVAILABLE", withOriginalError("Controller is unavailable.", error), error, { delivery: "not-sent" });
|
|
228
251
|
}
|
|
229
252
|
function controllerAccessDenied(error, target) {
|
|
230
253
|
return new ControllerClientError("CONTROLLER_ACCESS_DENIED", `Controller ${target} access was denied (${error.code}) before any request was sent. `
|
|
231
254
|
+ "In Codex, request sandbox escalation to rerun the same Yui CLI command outside the sandbox "
|
|
232
|
-
+ `if authorized. If escalation is denied, report that to the user.${originalErrorDetail(error)}`, error);
|
|
255
|
+
+ `if authorized. If escalation is denied, report that to the user.${originalErrorDetail(error)}`, error, { delivery: "not-sent" });
|
|
233
256
|
}
|
|
234
257
|
function invalidDiscovery(error) {
|
|
235
258
|
return new ControllerClientError("CONTROLLER_DISCOVERY_INVALID", error === undefined
|
|
236
259
|
? "Controller discovery is invalid."
|
|
237
|
-
: withOriginalError("Controller discovery is invalid.", error), error);
|
|
260
|
+
: withOriginalError("Controller discovery is invalid.", error), error, { delivery: "not-sent" });
|
|
238
261
|
}
|
|
239
262
|
function invalidResponse(error) {
|
|
240
263
|
return new ControllerClientError("INVALID_RESPONSE", error === undefined
|
|
@@ -248,7 +271,9 @@ function originalErrorDetail(error) {
|
|
|
248
271
|
const code = isNodeError(error) && typeof error.code === "string"
|
|
249
272
|
? ` (${error.code})`
|
|
250
273
|
: "";
|
|
251
|
-
return ` Original error${code}: ${error instanceof
|
|
274
|
+
return ` Original error${code}: ${error instanceof SyntaxError
|
|
275
|
+
? "Input could not be parsed; raw input omitted."
|
|
276
|
+
: error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown cause."}`;
|
|
252
277
|
}
|
|
253
278
|
function isNodeError(error) {
|
|
254
279
|
return error instanceof Error && "code" in error;
|
package/dist/core/protocol.js
CHANGED
|
@@ -129,11 +129,16 @@ export function parseControllerDiscovery(value, expected) {
|
|
|
129
129
|
"processStartIdentity",
|
|
130
130
|
"socketPath",
|
|
131
131
|
"token"
|
|
132
|
-
])
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
132
|
+
])) {
|
|
133
|
+
throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery fields are invalid.");
|
|
134
|
+
}
|
|
135
|
+
if (value.schemaVersion !== 1 || value.protocolVersion !== FILE_TASK_CONTROLLER_PROTOCOL_VERSION) {
|
|
136
|
+
throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery schema/protocol version does not match this CLI; verify the matching installation.");
|
|
137
|
+
}
|
|
138
|
+
if (value.homeFilesystemId !== expected.homeFilesystemId || value.socketPath !== expected.socketPath) {
|
|
139
|
+
throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery Home/socket identity does not match this invocation.");
|
|
140
|
+
}
|
|
141
|
+
if (!isHomeId(value.homeId)
|
|
137
142
|
|| !isHomeFilesystemId(value.homeFilesystemId)
|
|
138
143
|
|| typeof value.controllerInstanceId !== "string"
|
|
139
144
|
|| !/^[a-f0-9]{32}$/u.test(value.controllerInstanceId)
|
|
@@ -141,10 +146,10 @@ export function parseControllerDiscovery(value, expected) {
|
|
|
141
146
|
|| value.pid < 1
|
|
142
147
|
|| typeof value.processStartIdentity !== "string"
|
|
143
148
|
|| !/^[0-9]{1,32}$/u.test(value.processStartIdentity)
|
|
144
|
-
|| value.socketPath !==
|
|
149
|
+
|| typeof value.socketPath !== "string"
|
|
145
150
|
|| typeof value.token !== "string"
|
|
146
151
|
|| !/^[a-f0-9]{64}$/u.test(value.token)) {
|
|
147
|
-
throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery is invalid.");
|
|
152
|
+
throw new ControllerProtocolError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery process, Home or authentication identity is invalid; raw record omitted.");
|
|
148
153
|
}
|
|
149
154
|
return Object.freeze({
|
|
150
155
|
schemaVersion: 1,
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
|
+
import { inspectPty } from "./ptyProbe.js";
|
|
4
5
|
import { configuredAgentToDefinition, resolveAgentEnvironment } from "../agent/agent.js";
|
|
5
6
|
import { operationalAgentEnvironment } from "../agent/launchEnvironment.js";
|
|
6
7
|
import { resolveTmuxBin } from "../config/yuiConfig.js";
|
|
7
8
|
import { compileRoleSessionContext } from "../context/roleSessionContext.js";
|
|
8
9
|
import { callController, ControllerClientError } from "../core/controllerClient.js";
|
|
10
|
+
import { describeCliFailure } from "../errors/cliFailure.js";
|
|
9
11
|
import { EPHEMERAL_DOMAIN_GRACE_MS, readEphemeralDomainIdentity, readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
10
12
|
import { usageError } from "../errors/cliError.js";
|
|
11
13
|
import { inspectAgentCapabilities, resolveAgentAdapter } from "../executor/agentAdapter.js";
|
|
@@ -137,6 +139,7 @@ async function inspectDoctor(env, executor) {
|
|
|
137
139
|
controller,
|
|
138
140
|
...(domain === undefined ? [] : [domain]),
|
|
139
141
|
...toolChecks,
|
|
142
|
+
...inspectPty(),
|
|
140
143
|
...agentChecks,
|
|
141
144
|
...review.checks
|
|
142
145
|
],
|
|
@@ -160,19 +163,19 @@ export async function checkController(home, probe = callController) {
|
|
|
160
163
|
}
|
|
161
164
|
catch (error) {
|
|
162
165
|
if (error instanceof ControllerClientError && error.code === "CONTROLLER_NOT_RUNNING") {
|
|
163
|
-
return { name: "controller", status: "missing", detail:
|
|
166
|
+
return { name: "controller", status: "missing", detail: describeCliFailure(error).message };
|
|
164
167
|
}
|
|
165
168
|
if (error instanceof ControllerClientError && error.code === "CONTROLLER_UNAVAILABLE") {
|
|
166
169
|
return {
|
|
167
170
|
name: "controller",
|
|
168
171
|
status: "invalid",
|
|
169
|
-
detail:
|
|
172
|
+
detail: describeCliFailure(error).message
|
|
170
173
|
};
|
|
171
174
|
}
|
|
172
175
|
return {
|
|
173
176
|
name: "controller",
|
|
174
177
|
status: "invalid",
|
|
175
|
-
detail:
|
|
178
|
+
detail: describeCliFailure(error).message
|
|
176
179
|
};
|
|
177
180
|
}
|
|
178
181
|
}
|