@zq-silk/yui 2.10.0 → 2.12.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/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/callerDiscovery.js +18 -0
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +2 -3
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +12 -12
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +7 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +22 -12
- package/dist/runtime/managedIdentity.js +5 -3
- package/dist/runtime/sessionCaller.js +9 -10
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/maintenanceCaller.js +17 -25
- package/dist/storage/nativeSessionLookup.js +23 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sessionHistorySchema.js +17 -0
- package/dist/storage/sqliteSchema.js +96 -11
- package/dist/storage/sqliteStore.js +71 -4
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +4 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/i18n/en.js +29 -2
- package/dist/web/assets/client/i18n/zh-CN.js +29 -2
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- package/dist/web/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webServer.js +6 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +130 -0
- package/docs/job-runs.md +392 -17
- package/docs/managed-turn-and-session-runtime.md +10 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +12 -5
- package/docs/release-workflow.md +79 -0
- package/docs/release-workflow.zh-CN.md +58 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +45 -1
- package/docs/storage-baseline.zh-CN.md +33 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-runtime/SKILL.md +12 -5
- package/skills/yui-runtime/VERSION +1 -1
|
@@ -33,6 +33,15 @@ function requireTextContent(content) {
|
|
|
33
33
|
* commit exactly that path, and return the resulting commit-pinned reference.
|
|
34
34
|
*/
|
|
35
35
|
export async function saveArtifactCapability(home, taskId, input) {
|
|
36
|
+
const bytes = artifactInputBytes(input);
|
|
37
|
+
return saveArtifactFile(home, taskId, {
|
|
38
|
+
relativePath: input.relativePath, bytes,
|
|
39
|
+
...(input.message === undefined ? {} : { message: input.message }),
|
|
40
|
+
...(input.expectedHead === undefined ? {} : { expectedHead: input.expectedHead })
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Shared bounded text/static-image input; never executes HTML or scripts. */
|
|
44
|
+
export function artifactInputBytes(input) {
|
|
36
45
|
if (input.encoding !== undefined && input.encoding !== "utf8" && input.encoding !== "base64") {
|
|
37
46
|
throw new Error("Artifact encoding must be utf8 or base64.");
|
|
38
47
|
}
|
|
@@ -50,12 +59,7 @@ export async function saveArtifactCapability(home, taskId, input) {
|
|
|
50
59
|
else {
|
|
51
60
|
bytes = requireTextContent(input.content);
|
|
52
61
|
}
|
|
53
|
-
return
|
|
54
|
-
relativePath: input.relativePath,
|
|
55
|
-
bytes,
|
|
56
|
-
...(input.message === undefined ? {} : { message: input.message }),
|
|
57
|
-
...(input.expectedHead === undefined ? {} : { expectedHead: input.expectedHead })
|
|
58
|
-
});
|
|
62
|
+
return bytes;
|
|
59
63
|
}
|
|
60
64
|
/**
|
|
61
65
|
* `artifact.read`: read `relativePath` at HEAD, or at a pinned `commit` for
|
|
@@ -68,7 +72,10 @@ export async function readArtifactCapability(home, taskId, input) {
|
|
|
68
72
|
throw new Error(`Artifact repository is unavailable for ${taskId}.`);
|
|
69
73
|
const safeRelative = safeRelativeArtifactPath(input.relativePath);
|
|
70
74
|
const content = await repo.read(safeRelative, input.commit === undefined ? undefined : requireCommitId(input.commit));
|
|
71
|
-
|
|
75
|
+
return { taskId, ...presentArtifactContent(content) };
|
|
76
|
+
}
|
|
77
|
+
export function presentArtifactContent(content) {
|
|
78
|
+
const image = isRasterArtifact(content.relativePath) ? rasterMetadata(content.bytes) : null;
|
|
72
79
|
let text;
|
|
73
80
|
if (image)
|
|
74
81
|
text = content.bytes.toString("base64");
|
|
@@ -83,7 +90,6 @@ export async function readArtifactCapability(home, taskId, input) {
|
|
|
83
90
|
throw new Error("Binary artifact is not supported as text.");
|
|
84
91
|
}
|
|
85
92
|
return {
|
|
86
|
-
taskId,
|
|
87
93
|
relativePath: content.relativePath,
|
|
88
94
|
commit: content.commit,
|
|
89
95
|
content: text,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { setTimeout } from "node:timers/promises";
|
|
4
|
+
import { safePathSegment } from "./artifactPaths.js";
|
|
3
5
|
import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner.js";
|
|
4
6
|
/**
|
|
5
7
|
* Short per-Task commit fence for artifact repositories.
|
|
@@ -11,8 +13,8 @@ import { currentFileLockOwner, fileLockOwnerIsLive } from "../core/fileLockOwner
|
|
|
11
13
|
* the commit critical section for one Task.
|
|
12
14
|
*
|
|
13
15
|
* The Project maintenance fence waits asynchronously for longer operations.
|
|
14
|
-
* An artifact save is short, so
|
|
15
|
-
*
|
|
16
|
+
* An artifact save is short, so acquisition asynchronously waits for a contended
|
|
17
|
+
* fence, allowing the live holder's Git subprocess to finish. It shares the same
|
|
16
18
|
* crash-safe design: an exclusive directory under the Home's `locks/` area
|
|
17
19
|
* (O_EXCL via mkdir), an owner file recording PID + process start time so a
|
|
18
20
|
* recycled PID can never pass for the original holder, and reclaim of a stale
|
|
@@ -37,7 +39,7 @@ export class ArtifactCommitLockedError extends Error {
|
|
|
37
39
|
}
|
|
38
40
|
/** Directory of one Task's artifact commit fence, below the Home's locks area. */
|
|
39
41
|
export function artifactCommitLockPath(home, taskId) {
|
|
40
|
-
return join(home, "locks", "task-artifacts", `${taskId}.lock`);
|
|
42
|
+
return join(home, "locks", "task-artifacts", `${safePathSegment(taskId)}.lock`);
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Acquire one Task's artifact commit fence. Returns the release function;
|
|
@@ -46,7 +48,7 @@ export function artifactCommitLockPath(home, taskId) {
|
|
|
46
48
|
* a stale (dead) holder is reclaimed. There is no in-process reentrancy: every
|
|
47
49
|
* acquisition contends.
|
|
48
50
|
*/
|
|
49
|
-
export function acquireArtifactCommitLock(home, taskId) {
|
|
51
|
+
export async function acquireArtifactCommitLock(home, taskId) {
|
|
50
52
|
const lock = artifactCommitLockPath(home, taskId);
|
|
51
53
|
const ownerIdentity = currentFileLockOwner();
|
|
52
54
|
mkdirSync(join(home, "locks", "task-artifacts"), { recursive: true, mode: 0o700 });
|
|
@@ -69,7 +71,7 @@ export function acquireArtifactCommitLock(home, taskId) {
|
|
|
69
71
|
reclaimStaleArtifactCommitLock(lock);
|
|
70
72
|
if (Date.now() >= deadline)
|
|
71
73
|
throw new ArtifactCommitLockedError(taskId);
|
|
72
|
-
|
|
74
|
+
await setTimeout(ARTIFACT_COMMIT_LOCK_RETRY_MS);
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { requireKnownFields } from "../domain/validation.js";
|
|
5
|
+
import { jobScopeKey } from "../job/jobDefinition.js";
|
|
6
|
+
import { runReference } from "../job/jobBusinessResult.js";
|
|
7
|
+
import { requireCommitId } from "./managedGit.js";
|
|
8
|
+
import { safeRelativeArtifactPath } from "./artifactPaths.js";
|
|
9
|
+
import { openTaskArtifactRepository, openVersionedArtifactRepository } from "./taskArtifactRepository.js";
|
|
10
|
+
function identity(value) {
|
|
11
|
+
if (typeof value !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/u.test(value)) {
|
|
12
|
+
throw new Error("Invalid artifact owner/Run identity.");
|
|
13
|
+
}
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function validateScope(scope) {
|
|
17
|
+
requireKnownFields(scope, scope?.kind === "task" ? ["kind", "taskId"] : ["kind"], "Artifact scope");
|
|
18
|
+
if (scope.kind !== "global" && scope.kind !== "task")
|
|
19
|
+
throw new Error("Invalid artifact scope.");
|
|
20
|
+
jobScopeKey(scope);
|
|
21
|
+
}
|
|
22
|
+
export function validateJobArtifactReference(ref, run) {
|
|
23
|
+
requireKnownFields(ref, ["kind", "owner", "sourceRun", "version", "relativePath", "digest"], "Artifact reference");
|
|
24
|
+
if (ref.kind !== "artifact")
|
|
25
|
+
throw new Error("Expected an explicit artifact reference.");
|
|
26
|
+
requireKnownFields(ref.sourceRun, ["scope", "runId"], "Artifact source Run");
|
|
27
|
+
validateScope(ref.sourceRun.scope);
|
|
28
|
+
identity(ref.sourceRun.runId);
|
|
29
|
+
if (ref.owner.kind === "job") {
|
|
30
|
+
requireKnownFields(ref.owner, ["kind", "scope", "jobId"], "Artifact Job owner");
|
|
31
|
+
validateScope(ref.owner.scope);
|
|
32
|
+
identity(ref.owner.jobId);
|
|
33
|
+
if (!isDeepStrictEqual(ref.owner.scope, ref.sourceRun.scope))
|
|
34
|
+
throw new Error("Artifact owner/source scope mismatch.");
|
|
35
|
+
if (run && ref.owner.jobId !== run.definitionId)
|
|
36
|
+
throw new Error("Artifact Job owner mismatch.");
|
|
37
|
+
}
|
|
38
|
+
else if (ref.owner.kind === "task") {
|
|
39
|
+
requireKnownFields(ref.owner, ["kind", "taskId"], "Artifact Task owner");
|
|
40
|
+
jobScopeKey({ kind: "task", taskId: ref.owner.taskId });
|
|
41
|
+
}
|
|
42
|
+
else
|
|
43
|
+
throw new Error("Invalid artifact owner.");
|
|
44
|
+
if (run && !isDeepStrictEqual(ref.sourceRun, runReference(run)))
|
|
45
|
+
throw new Error("Artifact source Run mismatch.");
|
|
46
|
+
if (requireCommitId(ref.version) !== ref.version)
|
|
47
|
+
throw new Error("Artifact version must be an exact commit.");
|
|
48
|
+
safeRelativeArtifactPath(ref.relativePath);
|
|
49
|
+
if (ref.relativePath.length > 512)
|
|
50
|
+
throw new Error("Artifact reference path exceeds 512 characters.");
|
|
51
|
+
if (typeof ref.digest !== "string" || !/^[a-f0-9]{64}$/u.test(ref.digest))
|
|
52
|
+
throw new Error("Invalid artifact digest.");
|
|
53
|
+
return ref;
|
|
54
|
+
}
|
|
55
|
+
export function jobArtifactOwner(run) {
|
|
56
|
+
return { kind: "job", scope: runReference(run).scope, jobId: run.definitionId };
|
|
57
|
+
}
|
|
58
|
+
/** Each Run has a distinct working tree and commit fence even for identical paths.
|
|
59
|
+
* The retained storage is unrelated to execution workspaces/logs/checkpoints. */
|
|
60
|
+
export function openJobArtifactRepository(home, owner, sourceRun) {
|
|
61
|
+
validateScope(owner.scope);
|
|
62
|
+
validateScope(sourceRun.scope);
|
|
63
|
+
if (!isDeepStrictEqual(owner.scope, sourceRun.scope))
|
|
64
|
+
throw new Error("Artifact scope mismatch.");
|
|
65
|
+
const scopePath = owner.scope.kind === "global" ? ["global"] : ["task", owner.scope.taskId];
|
|
66
|
+
const path = join(home, "job-artifacts", ...scopePath, identity(owner.jobId), identity(sourceRun.runId));
|
|
67
|
+
const lockKey = `job-${createHash("sha256").update(JSON.stringify([jobScopeKey(owner.scope), owner.jobId, sourceRun.runId])).digest("hex")}`;
|
|
68
|
+
return openVersionedArtifactRepository(home, lockKey, path);
|
|
69
|
+
}
|
|
70
|
+
export async function readJobArtifactReference(home, ref) {
|
|
71
|
+
validateJobArtifactReference(ref);
|
|
72
|
+
const repo = ref.owner.kind === "task"
|
|
73
|
+
? openTaskArtifactRepository(home, ref.owner.taskId)
|
|
74
|
+
: openJobArtifactRepository(home, ref.owner, ref.sourceRun);
|
|
75
|
+
const content = await repo.read(ref.relativePath, ref.version);
|
|
76
|
+
if (content.digest !== ref.digest)
|
|
77
|
+
throw new Error("Artifact digest mismatch; preserved evidence was not replaced.");
|
|
78
|
+
return content;
|
|
79
|
+
}
|
|
80
|
+
export function validateRunArtifacts(run) {
|
|
81
|
+
if (run.artifacts === undefined)
|
|
82
|
+
return;
|
|
83
|
+
if (!Array.isArray(run.artifacts) || run.artifacts.length > 256
|
|
84
|
+
|| Buffer.byteLength(JSON.stringify(run.artifacts)) > 128 * 1024)
|
|
85
|
+
throw new Error("Run artifacts exceed the bounded reference limit.");
|
|
86
|
+
for (const ref of run.artifacts)
|
|
87
|
+
validateJobArtifactReference(ref, run);
|
|
88
|
+
}
|
|
89
|
+
export function artifactsAppendOnly(before, after) {
|
|
90
|
+
const old = before.artifacts ?? [];
|
|
91
|
+
const next = after.artifacts ?? [];
|
|
92
|
+
return next.length >= old.length && old.every((ref, i) => isDeepStrictEqual(ref, next[i]));
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Conservative encoded payload bound inside the 1 MiB Controller envelope.
|
|
2
|
+
* JSON escaping/base64 and reference metadata count, not just raw file bytes.
|
|
3
|
+
* Repository limits stay independent; large retained text uses paged preview. */
|
|
4
|
+
export const MAX_JOB_ARTIFACT_JSON_BYTES = 256 * 1024;
|
|
5
|
+
// Leave room for the fixed reference and presentation metadata added on reads.
|
|
6
|
+
export const MAX_JOB_ARTIFACT_RESPONSE_BYTES = 512 * 1024;
|
|
7
|
+
export function boundedJobArtifactPayload(value, label, limit = MAX_JOB_ARTIFACT_JSON_BYTES) {
|
|
8
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > limit) {
|
|
9
|
+
throw new Error(`${label} exceeds the Job artifact ${limit / 1024} KiB encoded JSON transport limit. `
|
|
10
|
+
+ "Use smaller material or paged artifact-preview for retained text; source Task material remains available through its owning surface.");
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { lstat, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { supportedGit } from "../repository/gitCompatibility.js";
|
|
6
6
|
import { acquireArtifactCommitLock } from "./artifactCommitLock.js";
|
|
7
7
|
import { resolveContainedArtifactPath, safeRelativeArtifactPath, taskArtifactRepoPath } from "./artifactPaths.js";
|
|
@@ -90,13 +90,35 @@ export class ArtifactHeadConflictError extends Error {
|
|
|
90
90
|
*/
|
|
91
91
|
export function openTaskArtifactRepository(home, taskId) {
|
|
92
92
|
const repoPath = taskArtifactRepoPath(home, taskId);
|
|
93
|
+
const repo = openVersionedArtifactRepository(home, taskId, repoPath);
|
|
94
|
+
return { ...repo, taskId, save: async (input) => ({ ...await repo.save(input), taskId }) };
|
|
95
|
+
}
|
|
96
|
+
/** Shared local-only storage. Callers derive paths from validated owning identities,
|
|
97
|
+
* never from a user-supplied filesystem location. Task layout/commits stay intact. */
|
|
98
|
+
export function openVersionedArtifactRepository(home, key, repoPath) {
|
|
99
|
+
const taskId = key; // Existing error fields retain their public spelling.
|
|
100
|
+
const checkBoundary = async () => {
|
|
101
|
+
await resolveContainedArtifactPath(home, `${relative(home, repoPath).split("\\").join("/")}/boundary-check`);
|
|
102
|
+
try {
|
|
103
|
+
const metadata = await lstat(join(repoPath, ".git"));
|
|
104
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
105
|
+
throw new Error("Artifact Git metadata must be its owned local directory, not a link/worktree.");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
93
113
|
const exists = () => existsSync(join(repoPath, ".git"));
|
|
94
114
|
const head = async () => {
|
|
115
|
+
await checkBoundary();
|
|
95
116
|
if (!exists())
|
|
96
117
|
return null;
|
|
97
118
|
return currentHead(repoPath);
|
|
98
119
|
};
|
|
99
|
-
const
|
|
120
|
+
const ensureUnlocked = async () => {
|
|
121
|
+
await checkBoundary();
|
|
100
122
|
supportedGit();
|
|
101
123
|
if (exists()) {
|
|
102
124
|
await assertNoRemote(taskId, repoPath);
|
|
@@ -109,6 +131,15 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
109
131
|
await managedGit(repoPath, ["commit", "--allow-empty", "-m", "init artifact repo"]);
|
|
110
132
|
await assertNoRemote(taskId, repoPath);
|
|
111
133
|
};
|
|
134
|
+
const ensure = async () => {
|
|
135
|
+
const release = await acquireArtifactCommitLock(home, key);
|
|
136
|
+
try {
|
|
137
|
+
await ensureUnlocked();
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
release();
|
|
141
|
+
}
|
|
142
|
+
};
|
|
112
143
|
const save = async (input) => {
|
|
113
144
|
if (input.files.length === 0) {
|
|
114
145
|
throw new Error("An artifact save must include at least one file.");
|
|
@@ -135,9 +166,9 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
135
166
|
}
|
|
136
167
|
seen.add(file.relativePath);
|
|
137
168
|
}
|
|
138
|
-
await
|
|
139
|
-
const release = acquireArtifactCommitLock(home, taskId);
|
|
169
|
+
const release = await acquireArtifactCommitLock(home, key);
|
|
140
170
|
try {
|
|
171
|
+
await ensureUnlocked();
|
|
141
172
|
await assertNoRemote(taskId, repoPath);
|
|
142
173
|
// Expected-HEAD check under the lock: a conflict stops without writing.
|
|
143
174
|
if (input.expectedHead !== undefined) {
|
|
@@ -161,22 +192,23 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
161
192
|
"diff", "--cached", "--quiet", "--", ...pathspecs
|
|
162
193
|
]);
|
|
163
194
|
if (noChanges) {
|
|
164
|
-
return {
|
|
195
|
+
return { commit: await currentHead(repoPath), savedPaths: pathspecs };
|
|
165
196
|
}
|
|
166
197
|
// `--only <paths>` records exactly these paths; any other WIP stays WIP.
|
|
167
198
|
await managedGit(repoPath, ["commit", "--only", "-m", message, "--", ...pathspecs]);
|
|
168
|
-
return {
|
|
199
|
+
return { commit: await currentHead(repoPath), savedPaths: pathspecs };
|
|
169
200
|
}
|
|
170
201
|
finally {
|
|
171
202
|
release();
|
|
172
203
|
}
|
|
173
204
|
};
|
|
174
205
|
const read = async (relativePath, commit) => {
|
|
206
|
+
await checkBoundary();
|
|
175
207
|
const safeRelative = safeRelativeArtifactPath(relativePath);
|
|
176
208
|
// A pinned commit reads frozen evidence; otherwise read the current HEAD.
|
|
177
209
|
// Both resolve `<commit>:<path>`, an OBJECT spec (never the working tree, so
|
|
178
210
|
// a read never follows a symlink or sees unrelated WIP).
|
|
179
|
-
const pinned =
|
|
211
|
+
const pinned = await artifactCommit(repoPath, commit);
|
|
180
212
|
const objectSpec = `${pinned}:${safeRelative}`;
|
|
181
213
|
// `<commit>:<path>` for a DIRECTORY resolves to a tree, and `git show` would
|
|
182
214
|
// print a tree listing rather than fail — silently returning directory
|
|
@@ -211,9 +243,10 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
211
243
|
};
|
|
212
244
|
};
|
|
213
245
|
const list = async (commit) => {
|
|
246
|
+
await checkBoundary();
|
|
214
247
|
if (!exists())
|
|
215
248
|
return [];
|
|
216
|
-
const pinned =
|
|
249
|
+
const pinned = await artifactCommit(repoPath, commit);
|
|
217
250
|
const output = await managedGit(repoPath, [
|
|
218
251
|
"ls-tree", "-r", "-z", "--long", "--full-tree", pinned
|
|
219
252
|
]);
|
|
@@ -233,6 +266,7 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
233
266
|
return entries;
|
|
234
267
|
};
|
|
235
268
|
const workingChanges = async () => {
|
|
269
|
+
await checkBoundary();
|
|
236
270
|
if (!exists())
|
|
237
271
|
return [];
|
|
238
272
|
const output = await managedGit(repoPath, [
|
|
@@ -247,11 +281,25 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
247
281
|
}
|
|
248
282
|
return changes;
|
|
249
283
|
};
|
|
250
|
-
return {
|
|
284
|
+
return { repoPath, ensure, exists, head, save, read, list, workingChanges };
|
|
251
285
|
}
|
|
252
286
|
async function currentHead(repoPath) {
|
|
253
287
|
return requireCommitId(await managedGit(repoPath, ["rev-parse", "HEAD^{commit}"]));
|
|
254
288
|
}
|
|
289
|
+
async function artifactCommit(repoPath, value) {
|
|
290
|
+
if (value === undefined)
|
|
291
|
+
return currentHead(repoPath);
|
|
292
|
+
const commit = requireCommitId(value);
|
|
293
|
+
const type = await managedGit(repoPath, ["cat-file", "-t", commit]).catch(error => {
|
|
294
|
+
if (error instanceof ManagedGitError)
|
|
295
|
+
throw new Error(`Artifact commit ${commit} is unavailable.`, { cause: error });
|
|
296
|
+
throw error;
|
|
297
|
+
});
|
|
298
|
+
if (type.trim() !== "commit") {
|
|
299
|
+
throw new Error("Artifact version must name a commit, not another Git object.");
|
|
300
|
+
}
|
|
301
|
+
return commit;
|
|
302
|
+
}
|
|
255
303
|
/**
|
|
256
304
|
* Verify the repository is within the managed boundary before a write: NO remote
|
|
257
305
|
* (network or file sync) AND no repo-local config that can execute an external
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { maintenanceCallerEnvironment } from "../storage/maintenanceCaller.js";
|
|
2
|
+
import { discoveryAudience } from "./commandDiscovery.js";
|
|
3
|
+
/** Complete role hints retain their offline template, not authentication.
|
|
4
|
+
* Native-ID-only callers need local lookup to select a template. Read only the
|
|
5
|
+
* selected Home's registry; failure yields unbound, never a public fallback.
|
|
6
|
+
* This observation is deliberately not written back to the process environment. */
|
|
7
|
+
export function callerDiscoveryAudience(home, env) {
|
|
8
|
+
if ((env.YUI_SESSION_SCOPE !== undefined && env.YUI_ROLE !== undefined)
|
|
9
|
+
|| (env.CODEX_THREAD_ID === undefined && env.YUI_NATIVE_SESSION_ID === undefined)) {
|
|
10
|
+
return discoveryAudience(env);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return discoveryAudience(maintenanceCallerEnvironment(home, env, false));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return "unbound";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -397,8 +397,8 @@ const taskChildren = [
|
|
|
397
397
|
{
|
|
398
398
|
name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
399
399
|
summary: "Discover Tasks through a bounded, filterable catalog.",
|
|
400
|
-
usage: "yui task list [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
|
|
401
|
-
options: ["--all", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
|
|
400
|
+
usage: "yui task list [--all] [--source <ordinary|job>] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
|
|
401
|
+
options: ["--all", "--source", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
|
|
402
402
|
},
|
|
403
403
|
{
|
|
404
404
|
name: "search", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
@@ -1653,18 +1653,47 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1653
1653
|
]
|
|
1654
1654
|
},
|
|
1655
1655
|
{
|
|
1656
|
-
name: "job", discovery: { surface: "
|
|
1656
|
+
name: "job", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
|
|
1657
1657
|
summary: "Manage stable Job definitions and exact execution Runs (not scheduler wakes).",
|
|
1658
|
-
sections: [{ id: "manage", title: "Commands", entries: ["define", "update", "list", "show", "run", "runs", "run-get", "logs", "run-cancel", "run-acknowledge", "start", "get", "cancel", "acknowledge"] }],
|
|
1658
|
+
sections: [{ id: "manage", title: "Commands", entries: ["define", "update", "list", "show", "run", "runs", "run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "run-claim", "run-context", "run-report", "run-continue", "session-inspect", "session-create", "session-stop", "session-new", "logs", "run-cancel", "run-acknowledge", "artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register", "start", "get", "cancel", "acknowledge"] }],
|
|
1659
1659
|
children: [
|
|
1660
|
-
...["
|
|
1661
|
-
name, discovery: { surface: "
|
|
1662
|
-
summary:
|
|
1660
|
+
...["run-claim", "run-context", "run-report", "run-continue", "session-inspect", "session-create", "session-stop", "session-new"].map(name => ({
|
|
1661
|
+
name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
|
|
1662
|
+
summary: name === "run-report" ? "Record the exact native attempt's explicit Job business result."
|
|
1663
|
+
: name === "run-continue" ? "Continue an unfinished Run only after its exact prior native terminal."
|
|
1664
|
+
: name === "session-new" ? "Replace a proven-ended Job-owned Session, retaining Job input and results."
|
|
1665
|
+
: `${name} for a bounded Agent Job execution.`,
|
|
1666
|
+
usage: `yui job ${name} (--task <id>|--global) --job <id>`
|
|
1667
|
+
+ (name.startsWith("run-") ? " --run <run-id>" : " [--run <isolated-run-id>]")
|
|
1668
|
+
+ (name === "run-report" ? " --result <business-result-json>" : "")
|
|
1669
|
+
+ (name === "run-continue" ? " --expected-attempt <prior-native-attempt-id>" : "")
|
|
1670
|
+
+ (name === "session-new" || name === "session-stop" ? " --expected-session <native-id>" : ""),
|
|
1671
|
+
options: ["--task", "--global", "--job", "--run", "--result", "--expected-session", "--expected-attempt"]
|
|
1672
|
+
})),
|
|
1673
|
+
...["artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register"].map(name => ({
|
|
1674
|
+
name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
|
|
1675
|
+
summary: name === "artifact-save" ? "Save bounded formal material and publish an exact owned Run reference."
|
|
1676
|
+
: name === "artifact-register" ? "Register already-saved evidence without replaying execution."
|
|
1677
|
+
: name === "artifact-read" || name === "artifact-preview" ? "Read/preview one registered immutable artifact (never HEAD)."
|
|
1678
|
+
: "List published Run history and retained repository evidence.",
|
|
1679
|
+
usage: `yui job ${name} (--task <id>|--global) --job <id> --run <run-id>`
|
|
1680
|
+
+ (name === "artifact-save" ? " --artifact <save-json>" : "")
|
|
1681
|
+
+ (["artifact-read", "artifact-preview", "artifact-register"].includes(name) ? " --reference <reference-json>" : "")
|
|
1682
|
+
+ (name === "artifact-preview" ? " [--offset <text-offset>]" : ""),
|
|
1683
|
+
options: ["--task", "--global", "--job", "--run", "--artifact", "--reference", "--offset"]
|
|
1684
|
+
})),
|
|
1685
|
+
...["define", "update", "list", "show", "run", "runs", "run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "logs", "run-cancel", "run-acknowledge"].map(name => ({
|
|
1686
|
+
name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment", "global"] },
|
|
1687
|
+
summary: name === "run-result" ? "Read an exact Run's business outcome and admitted snapshot."
|
|
1688
|
+
: name === "run-evidence" ? "Read original process evidence separately from the business outcome."
|
|
1689
|
+
: name === "run-confirm" ? "Collect retained results for an exact Run without starting or replaying work."
|
|
1690
|
+
: name === "run-delegation" ? "Read the exact execution Task/WorkItem and attention projection; never a grant."
|
|
1691
|
+
: `${name} a stable Job or exact Run.`,
|
|
1663
1692
|
usage: `yui job ${name} (--task <id>|--global)${name === "list" ? "" : " --job <id>"}`
|
|
1664
1693
|
+ (name === "define" || name === "update" ? " --title <text> --input <configuration-json>" : "")
|
|
1665
1694
|
+ (name === "update" ? " --revision <current>" : "")
|
|
1666
1695
|
+ (name === "run" ? " --request-id <key> [--retry-of <run-id>]" : "")
|
|
1667
|
-
+ (["run-get", "logs", "run-cancel", "run-acknowledge"].includes(name) ? " --run <run-id>" : "")
|
|
1696
|
+
+ (["run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "logs", "run-cancel", "run-acknowledge"].includes(name) ? " --run <run-id>" : "")
|
|
1668
1697
|
})),
|
|
1669
1698
|
{ name: "start", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Start a DurableJob under an explicit request identity.", usage: "yui job start --request-id <id> --task <id> --project <project> --head <sha> --workspace <dir> --step <name>=<command> [--step ...] [--owner task|work-item:<id>|integration-attempt:<id>] [--env <k=v>...]" },
|
|
1670
1699
|
{ name: "get", discovery: { surface: "managed", audiences: ["operator", "leader", "assignment"] }, summary: "Show a DurableJob record and its terminal result.", usage: "yui job get --task <id> --job <job-id>" },
|
|
@@ -1673,16 +1702,18 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1673
1702
|
]
|
|
1674
1703
|
},
|
|
1675
1704
|
{
|
|
1676
|
-
name: "schedule", discovery: { surface: "
|
|
1705
|
+
name: "schedule", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] },
|
|
1677
1706
|
summary: "Manage persistent time plans referencing scoped Jobs.",
|
|
1678
|
-
sections: [{ id: "manage", title: "Commands", entries: ["create", "list", "show", "update", "enable", "disable"] }],
|
|
1679
|
-
children: ["create", "list", "show", "update", "enable", "disable"].map(name => ({
|
|
1680
|
-
name, discovery: { surface: "
|
|
1681
|
-
summary: `${name} a Schedule;
|
|
1707
|
+
sections: [{ id: "manage", title: "Commands", entries: ["create", "list", "show", "update", "enable", "disable", "revoke"] }],
|
|
1708
|
+
children: ["create", "list", "show", "update", "enable", "disable", "revoke"].map(name => ({
|
|
1709
|
+
name, discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] },
|
|
1710
|
+
summary: `${name} a Schedule; disable stops future dispatch, revoke also denies queued service starts.`,
|
|
1682
1711
|
usage: `yui schedule ${name} (--global|--task <id>)${name === "list" ? "" : " --schedule <id>"}`
|
|
1683
1712
|
+ (name === "create" ? " --job <id> --rule <json>" : "")
|
|
1684
1713
|
+ (name === "update" ? " --rule <json> [--job <id>]" : "")
|
|
1685
|
-
+ (["
|
|
1714
|
+
+ (["create", "update"].includes(name) ? " [--target <json>]" : "")
|
|
1715
|
+
+ (["create", "update", "enable"].includes(name) ? " [--authorization session|service]" : "")
|
|
1716
|
+
+ (["update", "enable", "disable", "revoke"].includes(name) ? " --revision <observed>" : "")
|
|
1686
1717
|
}))
|
|
1687
1718
|
},
|
|
1688
1719
|
{
|
|
@@ -16,8 +16,7 @@ export function discoveryAudience(env) {
|
|
|
16
16
|
return env.YUI_ROLE === "leader" ? "leader" : "assignment";
|
|
17
17
|
return "unbound";
|
|
18
18
|
}
|
|
19
|
-
export function discoveryCommandTree(env) {
|
|
20
|
-
const audience = discoveryAudience(env);
|
|
19
|
+
export function discoveryCommandTree(env, audience = discoveryAudience(env)) {
|
|
21
20
|
const project = (node) => {
|
|
22
21
|
const children = node.children.map(project).filter((child) => child !== undefined);
|
|
23
22
|
const eligible = node.discovery?.surface !== "runtime" && node.discovery?.audiences.includes(audience);
|
|
@@ -48,7 +47,7 @@ export function discoveryCommandTree(env) {
|
|
|
48
47
|
const sections = [
|
|
49
48
|
{ id: "start", title: "Start work", entries: ["operator", "project"] },
|
|
50
49
|
{ id: "progress", title: "Inspect progress", entries: ["task", "jobs", "execution", "telemetry"] },
|
|
51
|
-
{ id: "participate", title: "Participate and control", entries: ["role", "session"] },
|
|
50
|
+
{ id: "participate", title: "Participate and control", entries: ["role", "session", "job"] },
|
|
52
51
|
{ id: "maintain", title: "Configure and maintain", entries: ["help", "version", "setup", "start", "doctor", "config", "web", "controller", "resources", "update", "upgrade", "release"] }
|
|
53
52
|
];
|
|
54
53
|
return { ...tree, sections: sections.map(section => ({ ...section,
|
|
@@ -3,6 +3,7 @@ import { ensureFileTaskController } from "../controller/clientRuntime.js";
|
|
|
3
3
|
import { callFileTaskController } from "../controller/clientRuntime.js";
|
|
4
4
|
import { acknowledgeDurableJob, cancelDurableJob, getDurableJob, startDurableJob } from "../controller/jobClient.js";
|
|
5
5
|
import { resolveJobCaller, taskLocalActor } from "../task/taskAuthority.js";
|
|
6
|
+
import { boundedJobArtifactPayload } from "../artifacts/jobArtifactTransport.js";
|
|
6
7
|
/**
|
|
7
8
|
* The textual `--owner` forms accepted by `job start`. The public help text in
|
|
8
9
|
* the command catalog must document exactly these forms.
|
|
@@ -15,7 +16,7 @@ export const JOB_OWNER_USAGE = "task|work-item:<id>|integration-attempt:<id>";
|
|
|
15
16
|
*/
|
|
16
17
|
export async function runDurableJobCommand(args, options) {
|
|
17
18
|
const [command, ...rest] = args;
|
|
18
|
-
if (command !== undefined && ["define", "update", "list", "show", "run", "runs", "run-get", "run-cancel", "run-acknowledge", "logs"].includes(command)) {
|
|
19
|
+
if (command !== undefined && ["define", "update", "list", "show", "run", "runs", "run-get", "run-result", "run-evidence", "run-confirm", "run-delegation", "run-cancel", "run-acknowledge", "logs", "artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register", "run-claim", "run-context", "run-report", "run-continue", "session-inspect", "session-create", "session-stop", "session-new"].includes(command)) {
|
|
19
20
|
return definitionCommand(command, rest, options);
|
|
20
21
|
}
|
|
21
22
|
if (command === "start")
|
|
@@ -39,7 +40,7 @@ async function definitionCommand(command, args, options) {
|
|
|
39
40
|
const flags = new Map();
|
|
40
41
|
for (let i = 0; i < args.length; i++) {
|
|
41
42
|
const flag = args[i];
|
|
42
|
-
if (!["--global", "--task", "--job", "--title", "--input", "--revision", "--request-id", "--retry-of", "--run"].includes(flag)
|
|
43
|
+
if (!["--global", "--task", "--job", "--title", "--input", "--revision", "--request-id", "--retry-of", "--run", "--artifact", "--reference", "--offset", "--result", "--expected-session", "--expected-attempt"].includes(flag)
|
|
43
44
|
|| flags.has(flag))
|
|
44
45
|
throw usageError(`Unknown or repeated Job flag: ${flag}.`);
|
|
45
46
|
if (flag === "--global") {
|
|
@@ -54,7 +55,10 @@ async function definitionCommand(command, args, options) {
|
|
|
54
55
|
if (flags.has("--global") === flags.has("--task"))
|
|
55
56
|
throw usageError("Choose exactly one of --task <id> or --global.");
|
|
56
57
|
const taskId = flags.get("--task");
|
|
57
|
-
const
|
|
58
|
+
const executionRead = ["show", "runs", "run-get", "run-result", "run-context", "run-claim", "run-report",
|
|
59
|
+
"run-evidence", "run-confirm", "artifact-save", "artifact-list", "artifact-read", "artifact-preview", "artifact-register"].includes(command);
|
|
60
|
+
const caller = resolveJobCaller(options.environment, taskId
|
|
61
|
+
?? (executionRead ? options.environment?.YUI_TASK_ID : undefined) ?? "");
|
|
58
62
|
const scope = taskId === undefined ? { kind: "global" } : { kind: "task", taskId };
|
|
59
63
|
let configuration;
|
|
60
64
|
if (flags.has("--input")) {
|
|
@@ -66,10 +70,31 @@ async function definitionCommand(command, args, options) {
|
|
|
66
70
|
}
|
|
67
71
|
}
|
|
68
72
|
const requestId = flags.get("--request-id");
|
|
73
|
+
const parseObject = (flag) => {
|
|
74
|
+
if (!flags.has(flag))
|
|
75
|
+
return undefined;
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(flags.get(flag));
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw usageError(`${flag} must be JSON.`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const artifact = parseObject("--artifact");
|
|
84
|
+
if (artifact !== undefined)
|
|
85
|
+
boundedJobArtifactPayload(artifact, "Artifact save");
|
|
86
|
+
const reference = parseObject("--reference");
|
|
87
|
+
const resultInput = parseObject("--result");
|
|
69
88
|
if (command === "run" && !requestId)
|
|
70
89
|
throw usageError("job run requires --request-id.");
|
|
71
90
|
const params = {
|
|
72
91
|
op: command, scope, caller,
|
|
92
|
+
...(artifact === undefined ? {} : { artifact }),
|
|
93
|
+
...(reference === undefined ? {} : { reference }),
|
|
94
|
+
...(resultInput === undefined ? {} : { result: resultInput }),
|
|
95
|
+
...(flags.has("--expected-session") ? { expectedSessionId: flags.get("--expected-session") } : {}),
|
|
96
|
+
...(flags.has("--expected-attempt") ? { expectedAttemptId: flags.get("--expected-attempt") } : {}),
|
|
97
|
+
...(flags.has("--offset") ? { offset: Number(flags.get("--offset")) } : {}),
|
|
73
98
|
...(flags.has("--job") ? { id: flags.get("--job") } : {}),
|
|
74
99
|
...(flags.has("--title") ? { title: flags.get("--title") } : {}),
|
|
75
100
|
...(configuration === undefined ? {} : { configuration }),
|
|
@@ -114,8 +139,8 @@ async function getJob(args, options) {
|
|
|
114
139
|
`Job ${job.id} (task ${job.taskId})`,
|
|
115
140
|
` status: ${job.status}`,
|
|
116
141
|
` owner: ${formatOwner(job.owner)}`,
|
|
117
|
-
`
|
|
118
|
-
|
|
142
|
+
...(job.delegation ? [` delegated task: ${job.delegation.taskId}`, ` goal: ${job.delegation.goal}`]
|
|
143
|
+
: [` head: ${job.head}`, ` workspace: ${job.workspace}`]),
|
|
119
144
|
` created: ${job.createdAt}`
|
|
120
145
|
];
|
|
121
146
|
if (job.startedAt !== undefined)
|
|
@@ -129,7 +154,7 @@ async function getJob(args, options) {
|
|
|
129
154
|
if (job.result.unknownReason !== undefined)
|
|
130
155
|
lines.push(` reason: ${job.result.unknownReason}`);
|
|
131
156
|
}
|
|
132
|
-
for (const step of job.steps) {
|
|
157
|
+
for (const step of job.steps ?? []) {
|
|
133
158
|
lines.push(` step ${step.name}: ${step.command}`);
|
|
134
159
|
}
|
|
135
160
|
return `${lines.join("\n")}\n`;
|
|
@@ -194,6 +194,11 @@ function roleContext(args, store, options) {
|
|
|
194
194
|
nativeTurnId: sessions.providerBinding.run.nativeTurnId,
|
|
195
195
|
status: sessions.providerBinding.run.status
|
|
196
196
|
},
|
|
197
|
+
...(role.jobOwner === undefined ? {} : { job: {
|
|
198
|
+
owner: role.jobOwner,
|
|
199
|
+
read: `yui job show ${role.jobOwner.scope.kind === "global" ? "--global" : `--task ${role.jobOwner.scope.taskId}`} --job ${role.jobOwner.jobId} --json`,
|
|
200
|
+
boundary: "Job execution only. This Role is not Operator and owns no Task delivery authority. Read the exact Run from its original message."
|
|
201
|
+
} }),
|
|
197
202
|
retry: providerRetrySummary(sessions?.providerBinding),
|
|
198
203
|
// Historical interrupt receipts are available through Session inspection;
|
|
199
204
|
// only this Turn's exact identity is required at entry.
|