@zq-silk/yui 1.1.2 → 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 +2 -0
- 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 +4 -3
- 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 +2 -2
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- 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
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { usageError } from "../errors/cliError.js";
|
|
3
|
+
export const READ_PAGE_BYTES = 32 * 1024;
|
|
4
|
+
export const INLINE_DOCUMENT_BYTES = 16 * 1024;
|
|
5
|
+
function assertSource(source) {
|
|
6
|
+
if (Buffer.byteLength(source) > 1024)
|
|
7
|
+
throw usageError("Read source identity exceeds its 1024-byte budget.");
|
|
8
|
+
}
|
|
9
|
+
export function readDigest(value) {
|
|
10
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
/** A save receipt is not another read of the submitted body. Keep the exact
|
|
13
|
+
* delivery/control facts and identity; body/history are read on demand.
|
|
14
|
+
*/
|
|
15
|
+
export function messageReceipt(message) {
|
|
16
|
+
const { body: _body, ...facts } = message;
|
|
17
|
+
return { ...facts, digest: readDigest(message), bodyBytes: Buffer.byteLength(message.body) };
|
|
18
|
+
}
|
|
19
|
+
export function encodeReadCursor(cursor) {
|
|
20
|
+
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
21
|
+
}
|
|
22
|
+
export function decodeReadCursor(raw, source, digest) {
|
|
23
|
+
let cursor;
|
|
24
|
+
try {
|
|
25
|
+
if (raw.length > 4096)
|
|
26
|
+
throw new Error();
|
|
27
|
+
cursor = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
28
|
+
if (cursor.source !== source || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0
|
|
29
|
+
|| typeof cursor.digest !== "string")
|
|
30
|
+
throw new Error();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw usageError("Invalid read cursor for this source/scope.");
|
|
34
|
+
}
|
|
35
|
+
if (cursor.digest !== digest)
|
|
36
|
+
throw usageError("Read source changed; restart the read instead of mixing versions.");
|
|
37
|
+
return cursor.offset;
|
|
38
|
+
}
|
|
39
|
+
export function readLimit(value) {
|
|
40
|
+
const limit = value ?? 20;
|
|
41
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
42
|
+
throw usageError("Read limit must be between 1 and 100.");
|
|
43
|
+
}
|
|
44
|
+
return limit;
|
|
45
|
+
}
|
|
46
|
+
/** For small in-memory authorized collections. Authorize/filter BEFORE calling.
|
|
47
|
+
* Cursors bind the selected collection, never grant access, and store no state.
|
|
48
|
+
*/
|
|
49
|
+
export function recordPage(records, source, options = {}) {
|
|
50
|
+
assertSource(source);
|
|
51
|
+
const limit = readLimit(options.limit);
|
|
52
|
+
const digest = readDigest(records);
|
|
53
|
+
const offset = options.cursor === undefined ? 0 : decodeReadCursor(options.cursor, source, digest);
|
|
54
|
+
if (offset > records.length)
|
|
55
|
+
throw usageError("Read cursor exceeds this collection.");
|
|
56
|
+
const items = [];
|
|
57
|
+
// Reserve room for identities, counts and continuation. Oversized individual
|
|
58
|
+
// records belong in a document read, not silently truncated list metadata.
|
|
59
|
+
let bytes = 0;
|
|
60
|
+
for (const record of records.slice(offset, offset + limit)) {
|
|
61
|
+
const size = Buffer.byteLength(JSON.stringify(record)) + 1;
|
|
62
|
+
if (size > READ_PAGE_BYTES / 2)
|
|
63
|
+
throw usageError("List entry exceeds its summary budget; read the exact record.");
|
|
64
|
+
if (bytes + size > READ_PAGE_BYTES - 4096)
|
|
65
|
+
break;
|
|
66
|
+
items.push(record);
|
|
67
|
+
bytes += size;
|
|
68
|
+
}
|
|
69
|
+
const end = offset + items.length;
|
|
70
|
+
return {
|
|
71
|
+
source, digest, items, total: records.length, complete: end === records.length,
|
|
72
|
+
nextCursor: end < records.length ? encodeReadCursor({ source, digest, offset: end }) : null,
|
|
73
|
+
limits: { items: limit, bytes: READ_PAGE_BYTES }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Small exact details stay native JSON. Large details explicitly become a
|
|
77
|
+
* contentPage. Concatenate text in order, then JSON.parse once, never each chunk.
|
|
78
|
+
* JSON character offsets (not bytes) preserve Unicode and escaped controls.
|
|
79
|
+
* The source is an exact authorized read identity; reauthorize every page.
|
|
80
|
+
*/
|
|
81
|
+
export function boundedDocument(value, source, cursor) {
|
|
82
|
+
assertSource(source);
|
|
83
|
+
const text = JSON.stringify(value);
|
|
84
|
+
const totalBytes = Buffer.byteLength(text);
|
|
85
|
+
if (cursor === undefined && totalBytes <= INLINE_DOCUMENT_BYTES)
|
|
86
|
+
return value;
|
|
87
|
+
const digest = readDigest(value);
|
|
88
|
+
const offset = cursor === undefined ? 0 : decodeReadCursor(cursor, source, digest);
|
|
89
|
+
if (offset >= text.length && offset !== 0)
|
|
90
|
+
throw usageError("Read cursor exceeds this document.");
|
|
91
|
+
let end = Math.min(offset + 4096, text.length);
|
|
92
|
+
// Never cut between UTF-16 surrogate halves.
|
|
93
|
+
if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1]))
|
|
94
|
+
end--;
|
|
95
|
+
return { contentPage: {
|
|
96
|
+
source, digest, encoding: "json", offset, totalCharacters: text.length,
|
|
97
|
+
totalBytes, text: text.slice(offset, end), complete: end === text.length,
|
|
98
|
+
nextCursor: end < text.length ? encodeReadCursor({ source, digest, offset: end }) : null
|
|
99
|
+
} };
|
|
100
|
+
}
|
|
101
|
+
/** Shared parser for list/detail command tails; no options reach mutations. */
|
|
102
|
+
export function readOptions(args, flags = ["--cursor", "--limit"]) {
|
|
103
|
+
const positionals = [];
|
|
104
|
+
const values = new Map();
|
|
105
|
+
for (let i = 0; i < args.length; i++) {
|
|
106
|
+
const arg = args[i];
|
|
107
|
+
if (!arg.startsWith("--")) {
|
|
108
|
+
positionals.push(arg);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!flags.includes(arg) || values.has(arg) || args[i + 1] === undefined || args[i + 1].startsWith("--")) {
|
|
112
|
+
throw usageError(`Invalid read option: ${arg}.`);
|
|
113
|
+
}
|
|
114
|
+
values.set(arg, args[++i]);
|
|
115
|
+
}
|
|
116
|
+
return { positionals, values, cursor: values.get("--cursor"),
|
|
117
|
+
...(values.has("--limit") ? { limit: Number(values.get("--limit")) } : {}) };
|
|
118
|
+
}
|
|
@@ -228,7 +228,8 @@ async function runReleaseWorkflowLocked(store, taskId, workflowId, ports, option
|
|
|
228
228
|
if (grant === null) {
|
|
229
229
|
return finish(workflow, "unauthorized", "unauthorized:grant-missing", attempted);
|
|
230
230
|
}
|
|
231
|
-
const params = resolveParams(workflow, plan)
|
|
231
|
+
const params = grant.authorizationSource === undefined ? resolveParams(workflow, plan)
|
|
232
|
+
: Object.freeze({ ...resolveParams(workflow, plan), sourceCommit: workflow.source.commit });
|
|
232
233
|
const effectiveIrreversibility = effectiveStepIrreversibility(plan);
|
|
233
234
|
const decision = checkGrant(grant, {
|
|
234
235
|
action: grantAction(plan),
|
|
@@ -615,6 +616,12 @@ function cliUpdateVersionDenial(plan, params) {
|
|
|
615
616
|
function versionTagCheckoutDenial(grant, plan, params) {
|
|
616
617
|
if (plan.kind !== "version-tag")
|
|
617
618
|
return undefined;
|
|
619
|
+
// Leader authorization names a release version; the adapter pushes tag,
|
|
620
|
+
// not version. Validate that actual effect before consuming a grant use.
|
|
621
|
+
if (grant.authorizationSource !== undefined
|
|
622
|
+
&& (!params.version || ![params.version, `v${params.version}`].includes(params.tag ?? ""))) {
|
|
623
|
+
return "grant-version-tag-mismatch";
|
|
624
|
+
}
|
|
618
625
|
const repositories = grant.scope.repositories;
|
|
619
626
|
if (repositories === undefined || repositories.length === 0)
|
|
620
627
|
return undefined;
|
|
@@ -30,8 +30,8 @@ export function requireManagedGlobalCaller(store, env) {
|
|
|
30
30
|
*/
|
|
31
31
|
export class ManagedRuntimeDriftError extends Error {
|
|
32
32
|
name = "ManagedRuntimeDriftError";
|
|
33
|
-
constructor(message) {
|
|
34
|
-
super(message);
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
/** Reads the process's own immutable managed identity, or undefined when unmanaged. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { StorageSchemaError } from "../storage/storageSchema.js";
|
|
2
3
|
import { isStorageVersion, compareStorageVersions } from "../storage/storageVersions.js";
|
|
3
4
|
import { callController as defaultCallController, ControllerClientError } from "../core/controllerClient.js";
|
|
4
5
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
@@ -8,10 +9,12 @@ export async function assertRuntimeCoherence(input, options = {}) {
|
|
|
8
9
|
const identity = validateVersionIdentity(options.identity ?? yuiVersionIdentity());
|
|
9
10
|
const storage = (options.inspectStorage ?? inspectStorageSchema)(home);
|
|
10
11
|
if (storage.status !== "current") {
|
|
11
|
-
throw new
|
|
12
|
+
throw new StorageSchemaError(storage.status === "uninitialized" ? "STORAGE_UNINITIALIZED"
|
|
13
|
+
: storage.status === "unsupported" ? "STORAGE_SCHEMA_UNSUPPORTED" : "STORAGE_SCHEMA_INVALID", `Managed control-plane storage is not current: ${storage.status}.`
|
|
14
|
+
+ ("detail" in storage && typeof storage.detail === "string" ? ` ${storage.detail}` : ""));
|
|
12
15
|
}
|
|
13
16
|
if (storage.currentVersion !== identity.storageVersion) {
|
|
14
|
-
throw new
|
|
17
|
+
throw new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", "Managed control-plane storage version is incompatible "
|
|
15
18
|
+ `(expected ${identity.storageVersion}, found `
|
|
16
19
|
+ `${storage.currentVersion ?? "unknown"}).`);
|
|
17
20
|
}
|
|
@@ -37,7 +40,7 @@ export async function assertRuntimeCoherence(input, options = {}) {
|
|
|
37
40
|
}
|
|
38
41
|
export function assertControllerStatusIdentity(status, expected = yuiVersionIdentity()) {
|
|
39
42
|
if (!isRecord(status) || status.running !== true) {
|
|
40
|
-
throw new
|
|
43
|
+
throw new ControllerClientError("INVALID_RESPONSE", "Controller status does not describe a running Controller.");
|
|
41
44
|
}
|
|
42
45
|
assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
|
|
43
46
|
assertControllerField(status.version, expected.version, "version");
|
|
@@ -66,20 +69,19 @@ function validateVersionIdentity(value) {
|
|
|
66
69
|
}
|
|
67
70
|
function assertControllerContinuityIdentity(status, expected) {
|
|
68
71
|
if (!isRecord(status) || status.running !== true) {
|
|
69
|
-
throw new
|
|
72
|
+
throw new ControllerClientError("INVALID_RESPONSE", "Controller status does not describe a running Controller.");
|
|
70
73
|
}
|
|
71
74
|
if (typeof status.version !== "string" || status.version.trim().length === 0) {
|
|
72
|
-
throw new
|
|
75
|
+
throw new ControllerClientError("CONTROLLER_PROTOCOL_MISMATCH", "Controller version is invalid at the managed continuity gate.");
|
|
73
76
|
}
|
|
74
77
|
assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
|
|
75
78
|
assertControllerField(status.storageVersion, expected.storageVersion, "storage version");
|
|
76
79
|
}
|
|
77
80
|
function assertControllerField(actual, expected, label) {
|
|
78
81
|
if (actual !== expected) {
|
|
79
|
-
throw new
|
|
82
|
+
throw new ControllerClientError("CONTROLLER_PROTOCOL_MISMATCH", `Controller ${label} is incompatible with the exact control plane `
|
|
80
83
|
+ `(expected ${expected}, found ${typeof actual === "string" || typeof actual === "number" ? actual : "unknown"}). `
|
|
81
|
-
+ "
|
|
82
|
-
+ "before writing new Task records.");
|
|
84
|
+
+ "Verify Home, CLI and Controller identity before an authorized restart through the matching invocation.");
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
function requireText(value, label) {
|
|
@@ -24,6 +24,16 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
24
24
|
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
25
25
|
sql: "",
|
|
26
26
|
dataMigration: "task-main-workspace"
|
|
27
|
+
}, {
|
|
28
|
+
fromVersion: "1.1",
|
|
29
|
+
toVersion: "1.2",
|
|
30
|
+
name: "task-authorization-source",
|
|
31
|
+
introducedIn: "next",
|
|
32
|
+
sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
33
|
+
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
34
|
+
// New grants can carry source evidence. Valid historical Operator grants
|
|
35
|
+
// remain unchanged; migration must not invent authorization provenance.
|
|
36
|
+
sql: ""
|
|
27
37
|
}]);
|
|
28
38
|
export function storageMinorUpgradePlan(from) {
|
|
29
39
|
if (from === CURRENT_STORAGE_VERSION)
|
|
@@ -180,6 +180,8 @@ export function storedCapabilityGrant(value) {
|
|
|
180
180
|
fields.push("revokedAt");
|
|
181
181
|
if (grant.revokedBy !== undefined)
|
|
182
182
|
fields.push("revokedBy");
|
|
183
|
+
if (grant.authorizationSource !== undefined)
|
|
184
|
+
fields.push("authorizationSource");
|
|
183
185
|
exact(grant, fields, "Capability grant");
|
|
184
186
|
requireRecordIdentity(grant.id, "Capability grant id");
|
|
185
187
|
requireRecordIdentity(grant.taskId, "Capability grant Task id");
|
|
@@ -303,6 +305,7 @@ export function isValidCapabilityGrantTransition(existing, candidate) {
|
|
|
303
305
|
const immutable = candidate.id === existing.id
|
|
304
306
|
&& candidate.taskId === existing.taskId
|
|
305
307
|
&& candidate.granter === existing.granter
|
|
308
|
+
&& isDeepStrictEqual(candidate.authorizationSource, existing.authorizationSource)
|
|
306
309
|
&& isDeepStrictEqual(candidate.scope, existing.scope)
|
|
307
310
|
&& isDeepStrictEqual(candidate.actions, existing.actions)
|
|
308
311
|
&& isDeepStrictEqual(candidate.parameterBounds, existing.parameterBounds)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { runTaskCommand } from "../commands/taskCommands.js";
|
|
4
|
+
import { archiveExecutionChecks, archiveSettlementChecks } from "./archivePreflight.js";
|
|
5
|
+
import { admitLeaderArchive, assertLeaderArchiveAdmission } from "./leaderArchiveAuthority.js";
|
|
6
|
+
import { assertTaskRemoteDeliveryIntegrated, createTaskRemoteDeliveryProof } from "./remoteDeliveryService.js";
|
|
7
|
+
import { selectNewTaskRoleSession } from "../executor/agentExecutor.js";
|
|
8
|
+
/** A single foreground Controller operation survives stopping its requesting
|
|
9
|
+
* Session. It has no retry worker: uncertain receipts are inspected, not
|
|
10
|
+
* replayed. All existing settlement, delivery and cleanup checks remain. */
|
|
11
|
+
export async function archiveLeaderTask(store, coordinator, taskId, environment, request) {
|
|
12
|
+
const inputDigest = createHash("sha256").update(JSON.stringify(request)).digest("hex");
|
|
13
|
+
const previous = store.listEvents(taskId).find(event => event.type === "task.leader-archive-started" && event.payload.requestId === request.requestId);
|
|
14
|
+
if (previous) {
|
|
15
|
+
if (previous.payload.inputDigest !== inputDigest)
|
|
16
|
+
throw new Error("Archive request id names different input.");
|
|
17
|
+
const result = store.listEvents(taskId).find(event => event.type === "task.leader-archive-result" && event.payload.requestId === request.requestId);
|
|
18
|
+
return { taskId, requestId: request.requestId, status: result?.payload.status ?? "unknown",
|
|
19
|
+
detail: result?.payload.detail, replayed: true };
|
|
20
|
+
}
|
|
21
|
+
const admission = admitLeaderArchive(store, taskId, environment, request.sourceMessage, request.purpose);
|
|
22
|
+
const task = store.getTask(taskId);
|
|
23
|
+
const settlement = archiveSettlementChecks(store, task);
|
|
24
|
+
if (settlement.length)
|
|
25
|
+
throw new Error(`Archive is unsettled: ${JSON.stringify(settlement)}`);
|
|
26
|
+
const session = store.getTaskRoleSessionSet(taskId, "leader");
|
|
27
|
+
const ownInput = session?.providerBinding?.run;
|
|
28
|
+
// Only the requesting no-Run Leader may be stopped as part of this action.
|
|
29
|
+
// Another Run, Job, unknown input, or then handoff is not archive authority.
|
|
30
|
+
const ownResources = `role:${taskId}/leader`;
|
|
31
|
+
const execution = archiveExecutionChecks(store, taskId).filter(check => !(check.resource === ownResources
|
|
32
|
+
&& ownInput?.runId === undefined && ownInput?.status === "accepted"
|
|
33
|
+
&& ["unresolved-provider-input", "unresolved-mailbox"].includes(check.reason)));
|
|
34
|
+
if (execution.length)
|
|
35
|
+
throw new Error(`Archive execution is unresolved: ${JSON.stringify(execution)}`);
|
|
36
|
+
if (store.listMessages(taskId).some(message => message.interruptThen?.targetNativeSessionId === admission.source.nativeSessionId
|
|
37
|
+
&& message.continuation === undefined && message.interruptThen.notDeliveredReason === undefined)) {
|
|
38
|
+
throw new Error("Settle the original input handoff before archive.");
|
|
39
|
+
}
|
|
40
|
+
let candidate = null;
|
|
41
|
+
const workspace = store.getTaskWorkspace(taskId);
|
|
42
|
+
if (task.projectBindings.length && workspace) {
|
|
43
|
+
const snapshot = await coordinator.preparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(binding => binding.projectId));
|
|
44
|
+
candidate = { schemaVersion: 1, projects: snapshot.projects.map(project => ({
|
|
45
|
+
projectId: project.projectId, commit: project.headCommit
|
|
46
|
+
})) };
|
|
47
|
+
}
|
|
48
|
+
const proof = createTaskRemoteDeliveryProof(store, task, candidate);
|
|
49
|
+
assertTaskRemoteDeliveryIntegrated(proof.delivery);
|
|
50
|
+
for (const owned of store.listManagedWorkspaces(taskId)) {
|
|
51
|
+
const disposition = owned.owner.type === "work-item"
|
|
52
|
+
&& store.getWorkItem(taskId, owned.owner.workItemId)?.status === "retired" ? "abandoned" : "integrated";
|
|
53
|
+
const checks = await coordinator.preparer.inspectWorkspaceCleanup(owned, disposition);
|
|
54
|
+
if (checks.some(check => !(owned.owner.type === "task" && check.reason === "git-worktree-registrations"))) {
|
|
55
|
+
throw new Error(`Archive workspace is not clean/removable: ${JSON.stringify(checks)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Reauthenticate after asynchronous inspection, before the first effect.
|
|
59
|
+
admitLeaderArchive(store, taskId, environment, request.sourceMessage, request.purpose);
|
|
60
|
+
assertLeaderArchiveAdmission(store, taskId, admission);
|
|
61
|
+
const record = (type, payload) => store.transaction(tx => tx.saveEvent(taskId, createTaskEvent(tx.nextEventId(taskId), taskId, type, { requestId: request.requestId, ...payload }, new Date())));
|
|
62
|
+
const started = store.transaction(tx => {
|
|
63
|
+
assertLeaderArchiveAdmission(tx, taskId, admission);
|
|
64
|
+
const events = tx.listEvents(taskId);
|
|
65
|
+
const prior = events.find(event => event.type === "task.leader-archive-started"
|
|
66
|
+
&& event.payload.requestId === request.requestId);
|
|
67
|
+
if (prior) {
|
|
68
|
+
if (prior.payload.inputDigest !== inputDigest)
|
|
69
|
+
throw new Error("Archive request id names different input.");
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const unsettled = events.find(event => event.type === "task.leader-archive-started"
|
|
73
|
+
&& !events.some(result => result.type === "task.leader-archive-result"
|
|
74
|
+
&& result.payload.requestId === event.payload.requestId));
|
|
75
|
+
if (unsettled)
|
|
76
|
+
throw new Error(`Inspect unresolved archive ${unsettled.payload.requestId} before another operation.`);
|
|
77
|
+
tx.saveEvent(taskId, createTaskEvent(tx.nextEventId(taskId), taskId, "task.leader-archive-started", { requestId: request.requestId, inputDigest, authorizationSource: JSON.stringify(admission.source) }, new Date()));
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
if (!started)
|
|
81
|
+
return { taskId, requestId: request.requestId, status: "unknown", replayed: true };
|
|
82
|
+
try {
|
|
83
|
+
await coordinator.runtime.stopTaskRoleSessions(taskId, ["leader"]);
|
|
84
|
+
store.transaction(tx => {
|
|
85
|
+
assertLeaderArchiveAdmission(tx, taskId, admission);
|
|
86
|
+
const stopped = tx.getTaskRoleSessionSet(taskId, "leader");
|
|
87
|
+
const current = stopped?.sessions[stopped.activeAgentId];
|
|
88
|
+
if (!stopped || current?.nativeSessionId !== admission.source.nativeSessionId
|
|
89
|
+
|| current.status !== "ended" || current.endReason !== "stopped") {
|
|
90
|
+
throw new Error("Leader Session changed before archive runtime retirement.");
|
|
91
|
+
}
|
|
92
|
+
// Retain the exact stopped caller in history and clear its resumable
|
|
93
|
+
// binding. Generic workspace cleanup must not stop that caller again:
|
|
94
|
+
// the first verified stop already released its physical owner records.
|
|
95
|
+
tx.saveTaskRoleSessionSet(selectNewTaskRoleSession(stopped, stopped.activeAgentId, new Date()));
|
|
96
|
+
});
|
|
97
|
+
const cleanup = await coordinator.cleanupTaskForArchive(taskId, "integrated");
|
|
98
|
+
if (cleanup.status !== "removed")
|
|
99
|
+
throw new Error(cleanup.error ?? `Archive cleanup ${cleanup.status}.`);
|
|
100
|
+
runTaskCommand(["archive", taskId, "--integrated"], store, {
|
|
101
|
+
environment, archiveLeaderAdmission: admission, archiveRemoteDeliveryProof: proof
|
|
102
|
+
});
|
|
103
|
+
record("task.leader-archive-result", { status: "archived" });
|
|
104
|
+
return { taskId, requestId: request.requestId, status: "archived" };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
record("task.leader-archive-result", { status: store.getTask(taskId)?.status === "archived" ? "archived" : "failed",
|
|
108
|
+
detail: error instanceof Error ? error.message : String(error) });
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { taskAuthorizationSource } from "../grant/taskAuthorization.js";
|
|
2
|
+
const admissions = new WeakSet();
|
|
3
|
+
const terminalIdentity = (store, taskId) => {
|
|
4
|
+
const task = store.getTask(taskId);
|
|
5
|
+
if (!task || !["completed", "cancelled"].includes(task.status))
|
|
6
|
+
throw new Error("Ordinary Leader archive requires a terminal Task.");
|
|
7
|
+
return JSON.stringify({
|
|
8
|
+
status: task.status, completedAt: task.completedAt, completionSummary: task.completionSummary,
|
|
9
|
+
retiredAt: task.retiredAt, retirementSummary: task.retirementSummary,
|
|
10
|
+
projectBindings: task.projectBindings
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export function admitLeaderArchive(store, taskId, environment, messageId, purpose) {
|
|
14
|
+
const source = taskAuthorizationSource(store, taskId, environment, messageId, purpose);
|
|
15
|
+
const admission = Object.freeze({ taskId, source, terminalIdentity: terminalIdentity(store, taskId) });
|
|
16
|
+
admissions.add(admission);
|
|
17
|
+
return admission;
|
|
18
|
+
}
|
|
19
|
+
export function assertLeaderArchiveAdmission(store, taskId, admission) {
|
|
20
|
+
if (!admissions.has(admission) || admission.taskId !== taskId
|
|
21
|
+
|| terminalIdentity(store, taskId) !== admission.terminalIdentity) {
|
|
22
|
+
throw new Error("Archive admission is untrusted or its terminal Task changed.");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -4,8 +4,8 @@ export class CommandExecutionError extends Error {
|
|
|
4
4
|
code;
|
|
5
5
|
exitStatus;
|
|
6
6
|
stderr;
|
|
7
|
-
constructor(code, exitStatus, stderr = "") {
|
|
8
|
-
super(commandExecutionMessage(code));
|
|
7
|
+
constructor(code, exitStatus, stderr = "", options) {
|
|
8
|
+
super(commandExecutionMessage(code), options);
|
|
9
9
|
this.code = code;
|
|
10
10
|
this.exitStatus = exitStatus;
|
|
11
11
|
this.stderr = stderr;
|
|
@@ -102,15 +102,17 @@ export class NodeCommandExecutor {
|
|
|
102
102
|
}
|
|
103
103
|
function stableCommandExecutionError(error) {
|
|
104
104
|
const details = errorDetails(error);
|
|
105
|
+
const cause = typeof error === "object" && error !== null && "error" in error
|
|
106
|
+
&& error.error instanceof Error ? { cause: error.error } : undefined;
|
|
105
107
|
if (details.code === "ENOENT") {
|
|
106
|
-
return new CommandExecutionError("COMMAND_NOT_FOUND", details.exitStatus, details.stderr);
|
|
108
|
+
return new CommandExecutionError("COMMAND_NOT_FOUND", details.exitStatus, details.stderr, cause);
|
|
107
109
|
}
|
|
108
110
|
if (details.code === "ETIMEDOUT"
|
|
109
111
|
|| details.signal === "SIGTERM"
|
|
110
112
|
|| details.signal === "SIGKILL") {
|
|
111
|
-
return new CommandExecutionError("COMMAND_TIMED_OUT", details.exitStatus, details.stderr);
|
|
113
|
+
return new CommandExecutionError("COMMAND_TIMED_OUT", details.exitStatus, details.stderr, cause);
|
|
112
114
|
}
|
|
113
|
-
return new CommandExecutionError("COMMAND_FAILED", details.exitStatus, details.stderr);
|
|
115
|
+
return new CommandExecutionError("COMMAND_FAILED", details.exitStatus, details.stderr, cause);
|
|
114
116
|
}
|
|
115
117
|
function errorDetails(error) {
|
|
116
118
|
if (typeof error !== "object" || error === null)
|
|
@@ -24,6 +24,7 @@ not a claim that every real Provider scenario has been validated.
|
|
|
24
24
|
| Who consumes results, synthesis and review? | [Result consumption](../agent-result-consumption.md) |
|
|
25
25
|
| When is a WorkItem dependency satisfied? | [Task dependencies](../task-dag-semantics.md) |
|
|
26
26
|
| How are records referenced inside a Task? | [Task-local identity](../task-local-identity.md) |
|
|
27
|
+
| How do bounded discovery, full originals and mutation receipts work? | [CLI information contract](../cli-information-contract.md) |
|
|
27
28
|
| How do Roles, Profiles and run configuration take effect? | [Roles and configuration](../roles-and-configuration.md) |
|
|
28
29
|
| How do delivery, integration and archive work? | [Task delivery](../task-delivery.md) |
|
|
29
30
|
| What does Project refresh synchronize, and how are partial failures reported? | [Project refresh](../project-refresh.md) |
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
| 谁消费结果、综合与审查? | [结果消费](../agent-result-consumption.zh-CN.md) |
|
|
21
21
|
| WorkItem 依赖何时满足? | [Task 依赖](../task-dag-semantics.zh-CN.md) |
|
|
22
22
|
| Task 内记录如何引用? | [局部身份](../task-local-identity.zh-CN.md) |
|
|
23
|
+
| 有界发现、完整原文与变更回执如何配合? | [CLI 信息契约](../cli-information-contract.zh-CN.md) |
|
|
23
24
|
| Role、Profile 与运行配置如何生效? | [角色与配置](../roles-and-configuration.zh-CN.md) |
|
|
24
25
|
| 怎样交付、集成和归档? | [交付生命周期](../task-delivery.zh-CN.md) |
|
|
25
26
|
| Project refresh 同步什么,怎样报告部分失败? | [Project refresh](../project-refresh.zh-CN.md) |
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# CLI information contracts
|
|
2
|
+
|
|
3
|
+
Yui separates a command's effect from discovery and full evidence. Queries
|
|
4
|
+
never consume Messages or start subsequent work. Mutation receipts describe
|
|
5
|
+
saved/requested/accepted/unknown facts; `ok: true` means the CLI invocation
|
|
6
|
+
succeeded, not that a Task, native Turn or remote delivery completed.
|
|
7
|
+
|
|
8
|
+
This is the current wire contract, not a compatibility mode. Stored Tasks,
|
|
9
|
+
Messages, results, Snapshots and Session histories are unchanged. No persistent
|
|
10
|
+
schema migration or new snapshot/cache store is introduced.
|
|
11
|
+
|
|
12
|
+
## Daily read path
|
|
13
|
+
|
|
14
|
+
| Question | Entry | Default information / further reading |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Which Task? | `task list` | Existing bounded catalog, filters, attention and cursor |
|
|
17
|
+
| What is current? | `task context <task>` | Current Task/Brief/Role/Project/workspace facts, active work and Runs, open inputs, active decisions, unresolved jobs, recent Message references; no event or terminal-Run dump |
|
|
18
|
+
| What records exist? | `task context list <task> --store <family>` | One authorized family, summaries and exact digest-bearing references |
|
|
19
|
+
| What does it actually say? | `task context inspect <task> --store <family> --ref <id> --digest <digest>` | Exact current record, including original result expansion; long documents are paged |
|
|
20
|
+
| What changed? | `task context delta <task> --after <cursor>` | Immutable events with a fixed upper bound, not a snapshot of current mutable state |
|
|
21
|
+
| Which Global input? | `session context <role>` | Identity, profile, authority, current native Turn/retry and separate bounded pending/recent Message pages |
|
|
22
|
+
| Read Global input | `role message list <role> [--pending]`, `role message show <role> <id>` | Scoped discovery, then exact original; queue acceptance never erases history |
|
|
23
|
+
| What arrived in this wake? | `task wake show <task> <wake>` | Fixed window and Message/Run/Event read pointers, not duplicated bodies |
|
|
24
|
+
| What was assigned? | `task run context <task>/<run>` | Frozen authority and a single `pointers` inventory with summaries; changed identities in `deltaRefs`; current observations remain separate |
|
|
25
|
+
| Which configuration is real? | `task role session inspect <task> <role>` | Task/Role identity, desired binding, frozen Session, current Provider binding, retry and explicit Host observation; no full Task/Role copies or Provider conversation history |
|
|
26
|
+
|
|
27
|
+
Domain lists for Task Messages, events, WorkItems, Runs, decisions, milestones,
|
|
28
|
+
publications and InputRequests use the same summary/reference paging contract.
|
|
29
|
+
Task Role-status and wake-history lists also page, with explicit detail commands.
|
|
30
|
+
Role discovery reports `recordedHealth` and `hostObservation: "not-requested"`,
|
|
31
|
+
not live Host health. Even a normal recorded status cannot rule out a failed Host
|
|
32
|
+
or unacknowledged terminal; follow the row's `task role status` read to inspect it.
|
|
33
|
+
Context lists additionally expose candidates, reviews, jobs, Project Knowledge,
|
|
34
|
+
workspaces and other authorized record families. `--status`, `--after` and
|
|
35
|
+
`--work-item` narrow Context discovery; every continuation must retain filters.
|
|
36
|
+
Run listing accepts a Task or `task/work` target.
|
|
37
|
+
|
|
38
|
+
Current Context's `attention.messages` counts all authorized Messages, not only
|
|
39
|
+
undelivered input. `collections` counts and `omitted` explicitly describe
|
|
40
|
+
incomplete discovery. Recent samples are not proof that no older pending or
|
|
41
|
+
relevant input exists. Read the relevant fixed wake and original Messages;
|
|
42
|
+
use family discovery when a broader requirement/history audit is needed.
|
|
43
|
+
|
|
44
|
+
## Budgets and continuations
|
|
45
|
+
|
|
46
|
+
- Discovery pages default to 20 items, accept 1–100, and reserve a 32 KiB
|
|
47
|
+
compact-JSON budget. Items contain summaries, never full Message/report bodies.
|
|
48
|
+
- Current Task Context has at most 64 records, with a 24 KiB record/attention
|
|
49
|
+
budget inside a 32 KiB response budget. Single inline values are at most
|
|
50
|
+
2 KiB; large values and Message/Run/WorkItem/Knowledge bodies use references.
|
|
51
|
+
Each collection contributes at most eight sampled records at entry.
|
|
52
|
+
- Global entry samples up to eight pending and eight recent Messages separately.
|
|
53
|
+
They have independent continuations, so a long historical list cannot crowd
|
|
54
|
+
pending input out of the entry view.
|
|
55
|
+
- Detail documents up to 16 KiB remain ordinary JSON. Larger documents return
|
|
56
|
+
`contentPage`: exact source, SHA-256 digest, JSON-character offset, total bytes
|
|
57
|
+
and characters, text, completeness and a next cursor. Chunks contain at most
|
|
58
|
+
4096 UTF-16 code units without splitting surrogate pairs, keeping escaped
|
|
59
|
+
wire output below 32 KiB. There is no 4 MiB cutoff that makes a legal original
|
|
60
|
+
permanently unreadable.
|
|
61
|
+
- Task metadata/next-action/remote-delivery, Brief, WorkItem, Message, Run,
|
|
62
|
+
decision/milestone/event, Role show/status/Session inspection, wake detail,
|
|
63
|
+
InputRequest, Run context and frozen expansion use bounded detail reads.
|
|
64
|
+
|
|
65
|
+
Use `--json` and read the top-level `data`. Run context and expansion use
|
|
66
|
+
`data.context`; Run Context delta uses `data.contextDelta`. For a long detail, repeat that same read with
|
|
67
|
+
`--cursor <contentPage.nextCursor>`. Concatenate `text` in offset order, checking
|
|
68
|
+
the same source and digest, and parse the combined JSON once. Do not parse
|
|
69
|
+
chunks individually or treat a first chunk as the complete report.
|
|
70
|
+
|
|
71
|
+
List responses carry `items`, `total`, `complete`, and `nextCursor`. Counts,
|
|
72
|
+
items and cursor validation occur inside the same authorized scope. Cursors
|
|
73
|
+
are opaque positions, not access tokens: every page reauthorizes. A changed
|
|
74
|
+
collection or document returns an explicit source-changed error; restart rather
|
|
75
|
+
than mix versions. No persisted pagination session is needed. Mutable collection
|
|
76
|
+
continuations intentionally do not promise uninterrupted traversal during
|
|
77
|
+
concurrent writes. For append-only event traversal use fixed-bound Context delta.
|
|
78
|
+
Discovery currently scans the selected authorized family to fingerprint it;
|
|
79
|
+
its output, not total storage-reading cost, is bounded.
|
|
80
|
+
|
|
81
|
+
## Effects and exceptions
|
|
82
|
+
|
|
83
|
+
Message send/queue/steer receipts preserve identity, digest, body byte count and
|
|
84
|
+
the original submission/delivery/control states without echoing the body.
|
|
85
|
+
Brief updates return a saved reference. No new wait, retry, acknowledgement or
|
|
86
|
+
approval phase is added. Repeating a read cursor must never repeat a mutation.
|
|
87
|
+
|
|
88
|
+
Whole-transaction operations (activation, completion, integration and input
|
|
89
|
+
control) remain atomic business operations even when their mechanics have
|
|
90
|
+
several steps. Their state-specific receipts and existing unknown-effect
|
|
91
|
+
diagnostics are retained; they are not flattened into a generic “executed” flag.
|
|
92
|
+
|
|
93
|
+
This change covers daily Agent context, collaboration discovery and original
|
|
94
|
+
evidence reads, not every diagnostic or export in the product. Existing
|
|
95
|
+
specialized log-tail/artifact limits, Task catalog pagination and bounded error
|
|
96
|
+
diagnostics retain their contracts. Configuration catalogs, Project/config
|
|
97
|
+
administration, resource inventories, archive diagnostics, release operations,
|
|
98
|
+
and specialized integration/changeset reads remain purpose-specific; this
|
|
99
|
+
document does not claim a universal 32 KiB cap for them. Full-detail Web
|
|
100
|
+
projections are separate consumers, not silently replaced with CLI summaries.
|
|
101
|
+
Use targeted Context discovery for large Project Knowledge and Task evidence.
|
|
102
|
+
|
|
103
|
+
For a typical notification, read current Context once, read the fixed wake once,
|
|
104
|
+
then read each relevant original once (or its complete document pages).
|
|
105
|
+
Do not reread an aggregate to obtain a detail it intentionally omits.
|
|
106
|
+
Full-original reading costs additional calls only when the original exceeds
|
|
107
|
+
the inline budget; it is never replaced by a summary.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# CLI 信息契约
|
|
2
|
+
|
|
3
|
+
Yui 将命令的效果、发现和完整证据读取分开。查询不会消费 Message,也不启动后续工作。
|
|
4
|
+
变更回执说明 saved/requested/accepted/unknown 等真实事实;`ok: true` 只说明这次
|
|
5
|
+
CLI 调用成功,不代表 Task、原生 Turn 或远端投递完成。
|
|
6
|
+
|
|
7
|
+
这是当前通信契约,不是兼容模式。已保存的 Task、Message、结果、Snapshot 和 Session
|
|
8
|
+
历史保持不变;没有持久 schema 迁移,也没有新增快照或缓存存储。
|
|
9
|
+
|
|
10
|
+
## 日常读取路径
|
|
11
|
+
|
|
12
|
+
| 问题 | 入口 | 默认信息与下一步 |
|
|
13
|
+
| --- | --- | --- |
|
|
14
|
+
| 找哪个 Task? | `task list` | 既有有界目录、过滤器、attention 与游标 |
|
|
15
|
+
| 当前有什么? | `task context <task>` | 当前 Task/Brief/Role/Project/工作区事实、活跃工作与 Run、开放输入、有效决策、未解决 Job、近期 Message 引用;不倾倒事件或终态 Run |
|
|
16
|
+
| 有哪些记录? | `task context list <task> --store <family>` | 一种获授权的记录、摘要与带 digest 的确切引用 |
|
|
17
|
+
| 原文是什么? | `task context inspect <task> --store <family> --ref <id> --digest <digest>` | 确切当前记录,含原始结果展开;长文档分页 |
|
|
18
|
+
| 发生了什么变化? | `task context delta <task> --after <cursor>` | 固定上界内的不可变事件,不是可变当前状态的快照 |
|
|
19
|
+
| Global 收到了什么? | `session context <role>` | 身份、Profile、权限、当前原生 Turn/retry,以及分别有界的 pending/recent Message 页 |
|
|
20
|
+
| 读取 Global 输入 | `role message list <role> [--pending]`、`role message show <role> <id>` | 在自身范围内发现,再读完整原文;队列接受不删除历史 |
|
|
21
|
+
| 本次唤醒带来什么? | `task wake show <task> <wake>` | 固定窗口与 Message/Run/Event 读取指针,不重复正文 |
|
|
22
|
+
| 分配了什么? | `task run context <task>/<run>` | 冻结授权与一份带摘要的 `pointers` 目录;`deltaRefs` 表示变化身份;当前观察独立 |
|
|
23
|
+
| 实际配置是什么? | `task role session inspect <task> <role>` | Task/Role 身份、期望绑定、冻结 Session、当前 Provider 绑定、retry 与显式 Host 观察;不复制整个 Task/Role 或 Provider 会话历史 |
|
|
24
|
+
|
|
25
|
+
Task Message、事件、WorkItem、Run、决策、里程碑、publication 和 InputRequest 列表
|
|
26
|
+
共用摘要/引用分页。Role 状态与 wake 历史列表也分页,并给出详情命令。
|
|
27
|
+
Role 发现返回 `recordedHealth` 与 `hostObservation: "not-requested"`,不是实时 Host
|
|
28
|
+
健康度。记录状态正常也不能排除 Host 失败或终态未确认;需沿条目的 `task role status`
|
|
29
|
+
读取指针检查。
|
|
30
|
+
Context 列表还暴露候选、Review、Job、Project Knowledge、工作区等获授权的记录。
|
|
31
|
+
`--status`、`--after`、`--work-item` 缩小发现范围;续读必须保留过滤条件。
|
|
32
|
+
Run 列表接受 Task 或 `task/work` 目标。
|
|
33
|
+
|
|
34
|
+
当前 Context 的 `attention.messages` 统计全部获授权 Message,不仅是未投递输入。
|
|
35
|
+
`collections` 计数与 `omitted` 明确表达发现不完整。近期抽样不能证明没有更早的待处理
|
|
36
|
+
或相关输入。读取相关固定 wake 与原始 Message;更广的需求/历史审计使用分类发现。
|
|
37
|
+
|
|
38
|
+
## 预算与续读
|
|
39
|
+
|
|
40
|
+
- 发现页默认 20 条,可选 1–100,紧凑 JSON 预算 32 KiB。条目包含摘要,不含完整报告正文。
|
|
41
|
+
- 当前 Task Context 至多 64 条,记录与 attention 共 24 KiB,响应预算 32 KiB。
|
|
42
|
+
单个内联值至多 2 KiB;大值及 Message/Run/WorkItem/Knowledge 正文用引用。
|
|
43
|
+
入口每个集合最多抽样八条。
|
|
44
|
+
- Global 入口分别抽样八条 pending 与八条 recent;独立续读,历史不能挤掉待处理输入。
|
|
45
|
+
- 不超过 16 KiB 的详情保持普通 JSON。更大详情返回 `contentPage`:确切来源、
|
|
46
|
+
SHA-256 digest、JSON 字符偏移、总字节数/字符数、文本、完整性与下一游标。
|
|
47
|
+
每块最多 4096 个 UTF-16 单元,不拆代理对,转义后的输出低于 32 KiB。
|
|
48
|
+
不再因 4 MiB 上限而永久无法读取合法原文。
|
|
49
|
+
- Task 元信息/next-action/remote-delivery、Brief、WorkItem、Message、Run、
|
|
50
|
+
决策/里程碑/事件、Role show/status/Session 检查、wake 详情、InputRequest、
|
|
51
|
+
Run Context 与冻结展开使用有界详情读取。
|
|
52
|
+
|
|
53
|
+
使用 `--json` 读取顶层 `data`;Run Context 和展开使用 `data.context`,Run Context delta 使用 `data.contextDelta`。
|
|
54
|
+
长详情用同一读取命令加 `--cursor <contentPage.nextCursor>` 继续。按 offset 顺序拼接
|
|
55
|
+
`text`,校验相同 source 与 digest,最后一次性解析 JSON。不要分别解析块,也不要把
|
|
56
|
+
第一块当成完整报告。
|
|
57
|
+
|
|
58
|
+
列表包含 `items`、`total`、`complete`、`nextCursor`。计数、条目和游标校验处于同一
|
|
59
|
+
授权范围。游标是不透明位置,不是通行令牌:每页重新授权。集合或文档改变时明确报错,
|
|
60
|
+
重新读取,不混合版本;无需持久分页 Session。可变集合不保证并发写入期间无中断遍历。
|
|
61
|
+
追加事件遍历使用固定上界 Context delta。目前分类发现扫描选定的授权集合来计算指纹;
|
|
62
|
+
有界的是输出,不是总存储读取成本。
|
|
63
|
+
|
|
64
|
+
## 效果与例外
|
|
65
|
+
|
|
66
|
+
Message send/queue/steer 回执保留身份、digest、正文字节数及原有提交/投递/control
|
|
67
|
+
状态,不回显正文。Brief 更新返回 saved 引用。没有新增等待、重试、确认或审批阶段。
|
|
68
|
+
重复读游标绝不能重放变更。
|
|
69
|
+
|
|
70
|
+
激活、完成、集成和输入控制仍是完整事务业务操作,即使内部有多步工程机制。
|
|
71
|
+
其特定状态回执与已有未知效果诊断保留,不压平成通用“已执行”标志。
|
|
72
|
+
|
|
73
|
+
本次覆盖日常 Agent Context、协作发现和原始证据读取,并非产品全部诊断或导出。
|
|
74
|
+
既有日志尾部/artifact 限制、Task 目录分页、有界错误诊断保留各自契约。
|
|
75
|
+
配置目录、Project/config 管理、资源清单、归档诊断、发布操作和专用集成/change-set
|
|
76
|
+
读取仍有自己的用途契约;不宣称它们全部受 32 KiB 限制。Web 全详情投影是独立消费者,
|
|
77
|
+
不会被 CLI 摘要悄悄替代。大 Project Knowledge 与 Task 证据用定向 Context 发现。
|
|
78
|
+
|
|
79
|
+
典型通知读取一次当前 Context、一次固定 wake,再逐条读取相关原文(或其全部文档页)。
|
|
80
|
+
不要为了入口故意省略的详情重复读聚合。只有原文超过内联预算才增加原文分页调用;
|
|
81
|
+
摘要不能代替完整原文。
|