@zq-silk/yui 2.3.0 → 2.5.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/README.md +15 -1
- package/dist/artifacts/artifactCapability.js +39 -10
- package/dist/artifacts/imageMetadata.js +120 -0
- package/dist/artifacts/managedGit.js +7 -4
- package/dist/artifacts/taskArtifactRepository.js +6 -0
- package/dist/cli/commandCatalog.js +18 -9
- package/dist/cli/invocationAuthority.js +1 -1
- package/dist/commands/executionAuditCommands.js +5 -0
- package/dist/commands/globalRoleCommands.js +41 -3
- package/dist/commands/projectCommands.js +89 -94
- package/dist/commands/roleSkillValidation.js +4 -8
- package/dist/commands/taskCommands.js +65 -14
- package/dist/commands/taskUsageCommand.js +38 -0
- package/dist/context/roleSessionContext.js +18 -2
- package/dist/context/runContextPack.js +5 -1
- package/dist/context/sessionBootstrapManifest.js +18 -3
- package/dist/context/skillCatalog.js +108 -0
- package/dist/context/skillPackage.js +161 -0
- package/dist/context/taskContext.js +3 -2
- package/dist/context/taskSearch.js +127 -0
- package/dist/controlPlaneCli.js +25 -20
- package/dist/controller/fileSchedulerStoreAdapter.js +39 -8
- package/dist/controller/globalInputDelivery.js +2 -4
- package/dist/controller/jobControl.js +2 -0
- package/dist/controller/nativeHostInput.js +37 -0
- package/dist/controller/providerRetryDelivery.js +4 -2
- package/dist/controller/runtime.js +93 -2
- package/dist/controller/runtimeEventInbox.js +2 -2
- package/dist/controller/runtimeEventProcessor.js +3 -3
- package/dist/doctor/doctor.js +10 -0
- package/dist/execution/reviewMainRun.js +1 -0
- package/dist/execution/workItemMainRun.js +1 -0
- package/dist/executor/agentAdapter.js +3 -5
- package/dist/executor/codexConfigConflict.js +6 -4
- package/dist/executor/effectiveLaunch.js +24 -1
- package/dist/executor/fileRoleLaunchPlanner.js +15 -7
- package/dist/integration/gitIntegrationService.js +7 -12
- package/dist/integration/integrationSourceApplication.js +4 -2
- package/dist/interaction/notificationPresentation.js +85 -0
- package/dist/interaction/operatorPresentation.js +1 -2
- package/dist/job/httpPreview.js +61 -0
- package/dist/job/jobRunner.js +12 -2
- package/dist/kernel/builtinCapabilities.js +3 -1
- package/dist/message/message.js +15 -1
- package/dist/output/rolePresentation.js +4 -1
- package/dist/repository/executionLaneGitSnapshot.js +2 -1
- package/dist/repository/gitCompatibility.js +62 -0
- package/dist/repository/gitWorkspace.js +48 -71
- package/dist/repository/gitWorktreeInventory.js +121 -0
- package/dist/repository/project.js +74 -25
- package/dist/resources/resourceDiscovery.js +2 -2
- package/dist/resources/resourceGc.js +7 -4
- package/dist/resources/resourceRegistrar.js +2 -1
- package/dist/runtime/agentEndpoint.js +8 -0
- package/dist/runtime/agentHost.js +170 -3
- package/dist/runtime/codexAppServerRuntime.js +1 -1
- package/dist/runtime/codexNativeAccess.js +175 -0
- package/dist/runtime/controlledCodexTui.js +116 -0
- package/dist/runtime/publicReply.js +23 -0
- package/dist/runtime/runtimeObservation.js +48 -0
- package/dist/runtime/structuredProviderHost.js +16 -7
- package/dist/runtime/taskCostMetrics.js +133 -0
- package/dist/runtime/taskUsageMetrics.js +11 -2
- package/dist/runtime/taskUsageQuery.js +38 -0
- package/dist/scheduler/leaderWakeupProcessor.js +1 -9
- package/dist/scheduler/taskExecutionProjection.js +2 -0
- package/dist/scheduler/taskObservabilityProjection.js +9 -1
- package/dist/storage/homeLayout.js +13 -0
- package/dist/storage/sqliteSchema.js +44 -0
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskSearch.js +35 -0
- package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
- package/dist/task/ordinaryArchive.js +42 -0
- package/dist/tmux/tmuxManager.js +8 -8
- package/dist/web/assets/assetManifest.js +11 -51
- package/dist/web/assets/client/app/catalog.js +215 -0
- package/dist/web/assets/client/app/main.js +148 -0
- package/dist/web/assets/client/app/selection.js +139 -0
- package/dist/web/assets/client/app/settings.js +326 -0
- package/dist/web/assets/client/app/taskView.js +280 -0
- package/dist/web/assets/client/domain/context.js +96 -0
- package/dist/web/assets/client/domain/records.js +126 -0
- package/dist/web/assets/client/domain/runs.js +119 -0
- package/dist/web/assets/client/domain/taskForms.js +197 -0
- package/dist/web/assets/client/domain/vocab.js +69 -0
- package/dist/web/assets/client/domain/work.js +116 -0
- package/dist/web/assets/client/i18n/en.js +900 -0
- package/dist/web/assets/client/i18n/zh-CN.js +898 -0
- package/dist/web/assets/client/index.js +114 -0
- package/dist/web/assets/client/layout/resizer.js +37 -0
- package/dist/web/assets/client/layout/router.js +35 -0
- package/dist/web/assets/client/layout/shortcuts.js +45 -0
- package/dist/web/assets/client/layout/sizing.js +95 -0
- package/dist/web/assets/client/layout/workspace.js +247 -0
- package/dist/web/assets/client/{api.js → lib/api.js} +20 -4
- package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
- package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
- package/dist/web/assets/client/lib/i18n.js +54 -0
- package/dist/web/assets/client/lib/markdown.js +167 -0
- package/dist/web/assets/client/lib/prefs.js +28 -0
- package/dist/web/assets/client/lib/theme.js +68 -0
- package/dist/web/assets/client/ui/containers.js +101 -0
- package/dist/web/assets/client/ui/controls.js +51 -0
- package/dist/web/assets/client/ui/forms.js +48 -0
- package/dist/web/assets/client/ui/metrics.js +137 -0
- package/dist/web/assets/client/ui/primitives.js +92 -0
- package/dist/web/assets/client/ui/text.js +33 -0
- package/dist/web/assets/client/ui/toast.js +12 -0
- package/dist/web/assets/client/views/bodySearch.js +172 -0
- package/dist/web/assets/client/views/createTask.js +85 -0
- package/dist/web/assets/client/views/dock/conversation.js +612 -0
- package/dist/web/assets/client/views/dock/discussion.js +173 -0
- package/dist/web/assets/client/views/dock/preview.js +129 -0
- package/dist/web/assets/client/views/dock/terminal.js +199 -0
- package/dist/web/assets/client/views/globalInput.js +67 -0
- package/dist/web/assets/client/{overview.js → views/overview.js} +61 -51
- package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
- package/dist/web/assets/client/views/skills.js +152 -0
- package/dist/web/assets/client/views/task/actions.js +36 -0
- package/dist/web/assets/client/views/task/delivery.js +185 -0
- package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
- package/dist/web/assets/client/views/task/files.js +272 -0
- package/dist/web/assets/client/views/task/observation.js +153 -0
- package/dist/web/assets/client/views/task/overview.js +161 -0
- package/dist/web/assets/client/views/task/page.js +131 -0
- package/dist/web/assets/client/views/task/records.js +163 -0
- package/dist/web/assets/client/views/task/runtime.js +130 -0
- package/dist/web/assets/client/views/task/timeline.js +190 -0
- package/dist/web/assets/shared/geometry.js +19 -0
- package/dist/web/assets/shared/taskTabs.js +8 -0
- package/dist/web/assets/shared/themes.js +93 -0
- package/dist/web/assets/shell/dialogs.js +63 -0
- package/dist/web/assets/shell/dock.js +40 -0
- package/dist/web/assets/shell/index.js +40 -0
- package/dist/web/assets/shell/settings.js +34 -0
- package/dist/web/assets/shell/sidebar.js +45 -0
- package/dist/web/assets/styles/index.js +41 -0
- package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +10 -10
- package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
- package/dist/web/assets/styles/markdown.js +22 -3
- package/dist/web/assets/styles/tokens.js +10 -63
- package/dist/web/assets/styles/ui/controls.js +76 -0
- package/dist/web/assets/styles/ui/forms.js +46 -0
- package/dist/web/assets/styles/ui/surfaces.js +122 -0
- package/dist/web/assets/styles/views/delivery.js +29 -0
- package/dist/web/assets/styles/views/dock.js +71 -0
- package/dist/web/assets/styles/views/page.js +32 -0
- package/dist/web/assets/styles/views/records.js +70 -0
- package/dist/web/assets/styles/views/settings.js +26 -0
- package/dist/web/assets/styles/views/sidebar.js +74 -0
- package/dist/web/assets/styles/views/task.js +80 -0
- package/dist/web/assets/styles/views/terminal.js +38 -0
- package/dist/web/taskPreviews.js +205 -0
- package/dist/web/tmuxWebTerminal.js +16 -9
- package/dist/web/webConversation.js +373 -0
- package/dist/web/webMaterials.js +155 -0
- package/dist/web/webServer.js +295 -18
- package/dist/web/webSettings.js +331 -0
- package/dist/web/webSkills.js +86 -0
- package/dist/web/webSnapshot.js +40 -8
- package/dist/web/webTaskSurface.js +106 -8
- package/dist/workspace/gitChangeSetCapture.js +3 -1
- package/docs/cli-information-contract.md +1 -1
- package/docs/cli-information-contract.zh-CN.md +1 -1
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +22 -0
- package/docs/release-workflow.md +54 -0
- package/docs/release-workflow.zh-CN.md +39 -0
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +22 -0
- package/docs/task-discovery.md +28 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +1 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +5 -4
- package/skills/yui-leader/references/execution.md +2 -1
- package/skills/yui-operator/SKILL.md +3 -0
- package/skills/yui-reviewer/SKILL.md +4 -2
- package/skills/yui-runtime/SKILL.md +43 -2
- package/skills/yui-worker/SKILL.md +3 -2
- package/dist/web/assets/client/app.js +0 -808
- package/dist/web/assets/client/components.js +0 -477
- package/dist/web/assets/client/detail.js +0 -379
- package/dist/web/assets/client/dock.js +0 -309
- package/dist/web/assets/client/evidence.js +0 -364
- package/dist/web/assets/client/forms.js +0 -230
- package/dist/web/assets/client/i18n.js +0 -1277
- package/dist/web/assets/client/markdown.js +0 -114
- package/dist/web/assets/client/records.js +0 -198
- package/dist/web/assets/client/sections.js +0 -349
- package/dist/web/assets/client/theme.js +0 -71
- package/dist/web/assets/shell.js +0 -161
- package/dist/web/assets/styles/components.js +0 -216
- package/dist/web/assets/styles/views.js +0 -309
- /package/dist/web/assets/{icons.js → shared/icons.js} +0 -0
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ For the self-built coding Agent (without a Controller or Yui Home), see
|
|
|
46
46
|
[`yui agent`](docs/native-agent.md). Its explicit configuration and product state
|
|
47
47
|
are separate from the orchestration setup below.
|
|
48
48
|
|
|
49
|
-
You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git, tmux, and
|
|
49
|
+
You need Linux x64 (glibc) or macOS (x64 or Apple Silicon), Git 2.29.0 or newer, tmux, and
|
|
50
50
|
Node.js `^20.17.0`, `^22.9.0`, `^24.0.0` or `^26.0.0`. On macOS install tmux with
|
|
51
51
|
`brew install tmux`. For the simplest setup, have Codex CLI or Claude Code CLI
|
|
52
52
|
installed and ready to use with your own account. Yui coordinates those
|
|
@@ -162,6 +162,20 @@ The task list remains at the left, task details occupy the primary reading area,
|
|
|
162
162
|
and recorded Task messages live in a secondary discussion pane. You can resize,
|
|
163
163
|
swap or hide that pane; on narrow screens it opens over the detail. Native Sessions
|
|
164
164
|
remain separate from recorded Task messages.
|
|
165
|
+
For a current controlled Codex Operator or Task Leader, **Conversation** and
|
|
166
|
+
**Terminal** are two views of the same Thread. Terminal opens the native Codex
|
|
167
|
+
TUI through the existing Host; it does not start another Agent. Only one browser
|
|
168
|
+
terminal holds the writer at a time. Returning to Conversation or closing the
|
|
169
|
+
page detaches that browser, without stopping the Agent. Conversation drafts and
|
|
170
|
+
the browser's default-view preference survive switching. Native questions and
|
|
171
|
+
approvals answer the original Codex request; Leader Task questions remain on
|
|
172
|
+
the separate InputRequest channel. The default-view preference is shared with
|
|
173
|
+
Settings; saving it does not switch the current view or Session.
|
|
174
|
+
MCP forms and URL confirmations can be completed in Terminal or declined in
|
|
175
|
+
Conversation. Native additional input is saved as a user Message with its exact
|
|
176
|
+
steer receipt before submission.
|
|
177
|
+
Attachments and native configuration changes
|
|
178
|
+
are not supported by this managed text-input surface.
|
|
165
179
|
The detail has four tabs: Overview (user input, progress and key Decisions),
|
|
166
180
|
Delivery (WorkItems, Reviews, Integration and remote delivery, read on demand),
|
|
167
181
|
Runtime (Roles, native Session activity, open execution and usage) and Records
|
|
@@ -2,6 +2,7 @@ import { openTaskArtifactRepository } from "./taskArtifactRepository.js";
|
|
|
2
2
|
import { saveArtifactFile } from "./gitArtifactRef.js";
|
|
3
3
|
import { requireCommitId } from "./managedGit.js";
|
|
4
4
|
import { safeRelativeArtifactPath } from "./artifactPaths.js";
|
|
5
|
+
import { isRasterArtifact, MAX_IMAGE_BYTES, rasterMetadata } from "./imageMetadata.js";
|
|
5
6
|
/**
|
|
6
7
|
* Capability-shaped adapter over the per-Task local Git artifact repository.
|
|
7
8
|
*
|
|
@@ -13,11 +14,8 @@ import { safeRelativeArtifactPath } from "./artifactPaths.js";
|
|
|
13
14
|
* self-certifying commit-pinned reference (`taskId + commit + relativePath`),
|
|
14
15
|
* which is the identity used for frozen Candidate/Review/completion evidence.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* very large files are out of scope for this first version by contract (§3.5)
|
|
19
|
-
* and are rejected or referenced through their existing resource boundary, not
|
|
20
|
-
* carried here.
|
|
17
|
+
* Text is UTF-8 (8 MiB). Static raster images use explicit base64 (4 MiB
|
|
18
|
+
* decoded), with the same immutable repository and image limits as Web.
|
|
21
19
|
*/
|
|
22
20
|
/** Text cap for a single artifact file, mirroring the former immutable-artifact limit. */
|
|
23
21
|
const MAX_ARTIFACT_TEXT_BYTES = 8 * 1024 * 1024;
|
|
@@ -35,7 +33,23 @@ function requireTextContent(content) {
|
|
|
35
33
|
* commit exactly that path, and return the resulting commit-pinned reference.
|
|
36
34
|
*/
|
|
37
35
|
export async function saveArtifactCapability(home, taskId, input) {
|
|
38
|
-
|
|
36
|
+
if (input.encoding !== undefined && input.encoding !== "utf8" && input.encoding !== "base64") {
|
|
37
|
+
throw new Error("Artifact encoding must be utf8 or base64.");
|
|
38
|
+
}
|
|
39
|
+
let bytes;
|
|
40
|
+
if (input.encoding === "base64") {
|
|
41
|
+
if (!isRasterArtifact(input.relativePath) || typeof input.content !== "string"
|
|
42
|
+
|| input.content.length > Math.ceil(MAX_IMAGE_BYTES / 3) * 4) {
|
|
43
|
+
throw new Error("Base64 artifacts require a static PNG, JPEG or WebP path and at most 4 MiB.");
|
|
44
|
+
}
|
|
45
|
+
bytes = Buffer.from(input.content, "base64");
|
|
46
|
+
if (bytes.toString("base64") !== input.content)
|
|
47
|
+
throw new Error("Artifact image must use canonical base64.");
|
|
48
|
+
rasterMetadata(bytes);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
bytes = requireTextContent(input.content);
|
|
52
|
+
}
|
|
39
53
|
return saveArtifactFile(home, taskId, {
|
|
40
54
|
relativePath: input.relativePath,
|
|
41
55
|
bytes,
|
|
@@ -45,8 +59,8 @@ export async function saveArtifactCapability(home, taskId, input) {
|
|
|
45
59
|
}
|
|
46
60
|
/**
|
|
47
61
|
* `artifact.read`: read `relativePath` at HEAD, or at a pinned `commit` for
|
|
48
|
-
* frozen evidence.
|
|
49
|
-
*
|
|
62
|
+
* frozen evidence. Images return explicit base64 and bounded raster metadata;
|
|
63
|
+
* text returns UTF-8. Both carry the resolved commit and exact digest.
|
|
50
64
|
*/
|
|
51
65
|
export async function readArtifactCapability(home, taskId, input) {
|
|
52
66
|
const repo = openTaskArtifactRepository(home, taskId);
|
|
@@ -54,12 +68,27 @@ export async function readArtifactCapability(home, taskId, input) {
|
|
|
54
68
|
throw new Error(`Artifact repository is unavailable for ${taskId}.`);
|
|
55
69
|
const safeRelative = safeRelativeArtifactPath(input.relativePath);
|
|
56
70
|
const content = await repo.read(safeRelative, input.commit === undefined ? undefined : requireCommitId(input.commit));
|
|
71
|
+
const image = isRasterArtifact(safeRelative) ? rasterMetadata(content.bytes) : null;
|
|
72
|
+
let text;
|
|
73
|
+
if (image)
|
|
74
|
+
text = content.bytes.toString("base64");
|
|
75
|
+
else {
|
|
76
|
+
try {
|
|
77
|
+
text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(content.bytes);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new Error("Artifact is not readable UTF-8 text or a supported raster image.");
|
|
81
|
+
}
|
|
82
|
+
if (text.includes("\0"))
|
|
83
|
+
throw new Error("Binary artifact is not supported as text.");
|
|
84
|
+
}
|
|
57
85
|
return {
|
|
58
86
|
taskId,
|
|
59
87
|
relativePath: content.relativePath,
|
|
60
88
|
commit: content.commit,
|
|
61
|
-
content:
|
|
62
|
-
digest: content.digest
|
|
89
|
+
content: text,
|
|
90
|
+
digest: content.digest,
|
|
91
|
+
...(image ? { encoding: "base64", ...image } : {})
|
|
63
92
|
};
|
|
64
93
|
}
|
|
65
94
|
/**
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
export const MAX_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
3
|
+
export function isRasterArtifact(path) {
|
|
4
|
+
return [".png", ".jpg", ".jpeg", ".webp"].includes(extname(path).toLowerCase());
|
|
5
|
+
}
|
|
6
|
+
/** Bounded raster metadata inspection, not a decoder. The viewer also reports
|
|
7
|
+
* browser decode errors. Never admit SVG/HTML or trust a caller's MIME type. */
|
|
8
|
+
export function rasterMetadata(bytes) {
|
|
9
|
+
if (bytes.length > MAX_IMAGE_BYTES)
|
|
10
|
+
throw new Error("Image exceeds 4 MiB.");
|
|
11
|
+
let mime, width = 0, height = 0;
|
|
12
|
+
if (bytes.length >= 33 && bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))
|
|
13
|
+
&& bytes.toString("ascii", 12, 16) === "IHDR" && bytes.readUInt32BE(8) === 13) {
|
|
14
|
+
mime = "image/png";
|
|
15
|
+
width = bytes.readUInt32BE(16);
|
|
16
|
+
height = bytes.readUInt32BE(20);
|
|
17
|
+
let end = false;
|
|
18
|
+
for (let offset = 8; offset + 12 <= bytes.length;) {
|
|
19
|
+
const length = bytes.readUInt32BE(offset);
|
|
20
|
+
const type = bytes.toString("ascii", offset + 4, offset + 8);
|
|
21
|
+
if (offset + length + 12 > bytes.length)
|
|
22
|
+
throw new Error("Truncated PNG image.");
|
|
23
|
+
if (type === "acTL")
|
|
24
|
+
throw new Error("Animated images are unsupported; save a static frame.");
|
|
25
|
+
offset += length + 12;
|
|
26
|
+
if (type === "IEND") {
|
|
27
|
+
end = length === 0 && offset === bytes.length;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (!end)
|
|
32
|
+
throw new Error("Incomplete PNG image.");
|
|
33
|
+
}
|
|
34
|
+
else if (bytes.length >= 4 && bytes[0] === 255 && bytes[1] === 216) {
|
|
35
|
+
mime = "image/jpeg";
|
|
36
|
+
let offset = 2;
|
|
37
|
+
while (offset + 4 <= bytes.length) {
|
|
38
|
+
if (bytes[offset++] !== 255)
|
|
39
|
+
throw new Error("Invalid JPEG image marker.");
|
|
40
|
+
while (bytes[offset] === 255)
|
|
41
|
+
offset++;
|
|
42
|
+
const marker = bytes[offset++];
|
|
43
|
+
if (marker === 0xda || marker === 0xd9)
|
|
44
|
+
break;
|
|
45
|
+
if (offset + 2 > bytes.length)
|
|
46
|
+
throw new Error("Truncated JPEG image.");
|
|
47
|
+
const length = bytes.readUInt16BE(offset);
|
|
48
|
+
if (length < 2 || offset + length > bytes.length)
|
|
49
|
+
throw new Error("Truncated JPEG image.");
|
|
50
|
+
if ([0xc0, 0xc1, 0xc2].includes(marker)) {
|
|
51
|
+
if (length < 8)
|
|
52
|
+
throw new Error("Invalid JPEG image frame.");
|
|
53
|
+
if (width || height)
|
|
54
|
+
throw new Error("Multiple JPEG image frames are unsupported.");
|
|
55
|
+
height = bytes.readUInt16BE(offset + 3);
|
|
56
|
+
width = bytes.readUInt16BE(offset + 5);
|
|
57
|
+
}
|
|
58
|
+
offset += length;
|
|
59
|
+
}
|
|
60
|
+
if (bytes[bytes.length - 2] !== 255 || bytes[bytes.length - 1] !== 217)
|
|
61
|
+
throw new Error("Incomplete JPEG image.");
|
|
62
|
+
}
|
|
63
|
+
else if (bytes.length >= 30 && bytes.toString("ascii", 0, 4) === "RIFF"
|
|
64
|
+
&& bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length) {
|
|
65
|
+
mime = "image/webp";
|
|
66
|
+
const kind = bytes.toString("ascii", 12, 16);
|
|
67
|
+
if (kind === "VP8X") {
|
|
68
|
+
if (bytes[20] & 2)
|
|
69
|
+
throw new Error("Animated images are unsupported; save a static frame.");
|
|
70
|
+
width = 1 + bytes.readUIntLE(24, 3);
|
|
71
|
+
height = 1 + bytes.readUIntLE(27, 3);
|
|
72
|
+
}
|
|
73
|
+
else if (kind === "VP8 " && bytes.subarray(23, 26).equals(Buffer.from([157, 1, 42]))) {
|
|
74
|
+
width = bytes.readUInt16LE(26) & 0x3fff;
|
|
75
|
+
height = bytes.readUInt16LE(28) & 0x3fff;
|
|
76
|
+
}
|
|
77
|
+
else if (kind === "VP8L" && bytes[20] === 47) {
|
|
78
|
+
width = 1 + (bytes.readUInt32LE(21) & 0x3fff);
|
|
79
|
+
height = 1 + ((bytes.readUInt32LE(21) >>> 14) & 0x3fff);
|
|
80
|
+
}
|
|
81
|
+
let frames = 0;
|
|
82
|
+
for (let offset = 12; offset < bytes.length;) {
|
|
83
|
+
if (offset + 8 > bytes.length)
|
|
84
|
+
throw new Error("Truncated WebP image.");
|
|
85
|
+
const length = bytes.readUInt32LE(offset + 4);
|
|
86
|
+
const type = bytes.toString("ascii", offset, offset + 4);
|
|
87
|
+
if (type === "ANIM" || type === "ANMF")
|
|
88
|
+
throw new Error("Animated images are unsupported; save a static frame.");
|
|
89
|
+
if (offset + 8 + length > bytes.length)
|
|
90
|
+
throw new Error("Truncated WebP image.");
|
|
91
|
+
if (type === "VP8 " || type === "VP8L") {
|
|
92
|
+
let frameWidth = 0, frameHeight = 0;
|
|
93
|
+
if (type === "VP8 " && length >= 10
|
|
94
|
+
&& bytes.subarray(offset + 11, offset + 14).equals(Buffer.from([157, 1, 42]))) {
|
|
95
|
+
frameWidth = bytes.readUInt16LE(offset + 14) & 0x3fff;
|
|
96
|
+
frameHeight = bytes.readUInt16LE(offset + 16) & 0x3fff;
|
|
97
|
+
}
|
|
98
|
+
else if (type === "VP8L" && length >= 5 && bytes[offset + 8] === 47) {
|
|
99
|
+
frameWidth = 1 + (bytes.readUInt32LE(offset + 9) & 0x3fff);
|
|
100
|
+
frameHeight = 1 + ((bytes.readUInt32LE(offset + 9) >>> 14) & 0x3fff);
|
|
101
|
+
}
|
|
102
|
+
if (frameWidth !== width || frameHeight !== height || ++frames > 1) {
|
|
103
|
+
throw new Error("Invalid WebP image frame dimensions.");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
offset += 8 + length + (length % 2);
|
|
107
|
+
if (offset > bytes.length)
|
|
108
|
+
throw new Error("Truncated WebP image.");
|
|
109
|
+
}
|
|
110
|
+
if (frames !== 1)
|
|
111
|
+
throw new Error("WebP image has no readable frame.");
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
throw new Error("Unsupported or damaged image. Use static PNG, JPEG or WebP (not SVG/GIF).");
|
|
115
|
+
}
|
|
116
|
+
if (!width || !height || width > 8192 || height > 8192 || width * height > 16_000_000) {
|
|
117
|
+
throw new Error("Unsupported image dimensions: maximum 8192 per side and 16 million pixels.");
|
|
118
|
+
}
|
|
119
|
+
return { mime, width, height };
|
|
120
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import { supportedGit } from "../repository/gitCompatibility.js";
|
|
3
4
|
const executeFile = promisify(execFile);
|
|
4
5
|
/**
|
|
5
6
|
* Hardened, isolated Git runner for per-Task artifact repositories.
|
|
@@ -44,7 +45,7 @@ const HARDENING_FLAGS = Object.freeze([
|
|
|
44
45
|
"-c", "protocol.file.allow=never",
|
|
45
46
|
// Hooks / filters / external programs — no committed script may execute.
|
|
46
47
|
"-c", `core.hooksPath=${NULL_DEVICE}`,
|
|
47
|
-
"-c", "core.fsmonitor=
|
|
48
|
+
"-c", "core.fsmonitor=",
|
|
48
49
|
"-c", "core.pager=cat",
|
|
49
50
|
"-c", "core.editor=false",
|
|
50
51
|
// Signing — never shell out to a signing program.
|
|
@@ -77,8 +78,9 @@ function managedGitEnvironment() {
|
|
|
77
78
|
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
78
79
|
LANG: "C.UTF-8",
|
|
79
80
|
LC_ALL: "C.UTF-8",
|
|
80
|
-
//
|
|
81
|
-
//
|
|
81
|
+
// Older supported Git does not recognize GIT_CONFIG_GLOBAL/SYSTEM.
|
|
82
|
+
// A neutral HOME and absent XDG_CONFIG_HOME also block its global lookup;
|
|
83
|
+
// GIT_CONFIG_NOSYSTEM is understood by all supported versions.
|
|
82
84
|
HOME: process.platform === "win32" ? (process.env.USERPROFILE ?? "") : "/nonexistent",
|
|
83
85
|
GIT_CONFIG_NOSYSTEM: "1",
|
|
84
86
|
GIT_CONFIG_GLOBAL: NULL_DEVICE,
|
|
@@ -122,8 +124,9 @@ function assertSafeArguments(args) {
|
|
|
122
124
|
*/
|
|
123
125
|
async function spawnManagedGit(repoPath, args, options) {
|
|
124
126
|
assertSafeArguments(args);
|
|
127
|
+
const program = supportedGit();
|
|
125
128
|
try {
|
|
126
|
-
const result = await executeFile(
|
|
129
|
+
const result = await executeFile(program.path, [...HARDENING_FLAGS, "-C", repoPath, ...args], {
|
|
127
130
|
encoding: "buffer",
|
|
128
131
|
env: managedGitEnvironment(),
|
|
129
132
|
maxBuffer: options?.maxBuffer ?? 16 * 1024 * 1024,
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
|
+
import { supportedGit } from "../repository/gitCompatibility.js";
|
|
5
6
|
import { acquireArtifactCommitLock } from "./artifactCommitLock.js";
|
|
6
7
|
import { resolveContainedArtifactPath, safeRelativeArtifactPath, taskArtifactRepoPath } from "./artifactPaths.js";
|
|
7
8
|
import { ManagedGitError, externalProgramConfigViolations, managedGit, managedGitBuffer, managedGitSucceeds, requireCommitId } from "./managedGit.js";
|
|
@@ -96,6 +97,7 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
96
97
|
return currentHead(repoPath);
|
|
97
98
|
};
|
|
98
99
|
const ensure = async () => {
|
|
100
|
+
supportedGit();
|
|
99
101
|
if (exists()) {
|
|
100
102
|
await assertNoRemote(taskId, repoPath);
|
|
101
103
|
return;
|
|
@@ -189,6 +191,10 @@ export function openTaskArtifactRepository(home, taskId) {
|
|
|
189
191
|
if (objectType !== "blob") {
|
|
190
192
|
throw new Error(`Artifact ${safeRelative} at ${pinned} is not a file (${objectType}).`);
|
|
191
193
|
}
|
|
194
|
+
const size = Number((await managedGit(repoPath, ["cat-file", "-s", objectSpec])).trim());
|
|
195
|
+
if (!Number.isSafeInteger(size) || size > MAX_ARTIFACT_BYTES) {
|
|
196
|
+
throw new Error(`Artifact ${safeRelative} exceeds the ${MAX_ARTIFACT_BYTES}-byte limit.`);
|
|
197
|
+
}
|
|
192
198
|
const bytes = await managedGitBuffer(repoPath, ["cat-file", "blob", objectSpec], {
|
|
193
199
|
maxBuffer: MAX_ARTIFACT_BYTES + 4096
|
|
194
200
|
}).catch((error) => {
|
|
@@ -387,7 +387,14 @@ const taskChildren = [
|
|
|
387
387
|
usage: "yui task list [--all] [--status <status>] [--project <id>] [--search <text>] [--attention <category>] [--limit <1..100>] [--cursor <cursor>]",
|
|
388
388
|
options: ["--all", "--status", "--project", "--search", "--attention", "--limit", "--cursor"]
|
|
389
389
|
},
|
|
390
|
+
{
|
|
391
|
+
name: "search", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
392
|
+
summary: "Search authorized Task bodies and return exact, reference-only source citations.",
|
|
393
|
+
usage: "yui task search <query> [--task <id>] [--kind <brief|message|decision|result|completion>] [--limit <1..100>] [--cursor <cursor>]",
|
|
394
|
+
options: ["--task", "--kind", "--limit", "--cursor"]
|
|
395
|
+
},
|
|
390
396
|
{ name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] }, summary: "Show Task metadata and counts; long details use contentPage.", usage: "yui task show <id> [--cursor <cursor>]", options: ["--cursor"] },
|
|
397
|
+
{ name: "usage", discovery: { surface: "public", audiences: ["public", "operator", "leader"] }, summary: "Read bounded Task usage and sourced costs; request detail pages explicitly.", usage: "yui task usage <task> [--limit <0..50>] [--offset <n>]", options: ["--limit", "--offset"] },
|
|
391
398
|
{
|
|
392
399
|
name: "artifact", discovery: { surface: "managed", audiences: ["operator", "leader"] },
|
|
393
400
|
summary: "Save Task files in local Git and read current or commit-pinned content.",
|
|
@@ -395,7 +402,7 @@ const taskChildren = [
|
|
|
395
402
|
children: [
|
|
396
403
|
{ name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] }, summary: "List saved Task artifacts.", usage: "yui task artifact list <task>" },
|
|
397
404
|
{ name: "read", discovery: { surface: "public", audiences: ["public", "operator", "leader", "assignment"] }, summary: "Read a file at HEAD or an exact commit.", usage: "yui task artifact read <task> <relative-path> [<commit>]" },
|
|
398
|
-
{ name: "save", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Save and locally commit one file.", usage: "yui task artifact save <task> <relative-path> <content> [--message <text>] [--expected-head <commit>]", options: ["--message", "--expected-head"] }
|
|
405
|
+
{ name: "save", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Save UTF-8 text or a base64 static PNG/JPEG/WebP image and locally commit one file.", usage: "yui task artifact save <task> <relative-path> <content> [--encoding utf8|base64] [--message <text>] [--expected-head <commit>]", options: ["--encoding", "--message", "--expected-head"], optionValues: { "--encoding": ["utf8", "base64"] } }
|
|
399
406
|
]
|
|
400
407
|
},
|
|
401
408
|
{
|
|
@@ -1089,7 +1096,7 @@ const taskChildren = [
|
|
|
1089
1096
|
sections: [{ id: "manage", title: "Commands", entries: ["list", "show", "retry", "resolve"] }],
|
|
1090
1097
|
children: [
|
|
1091
1098
|
{ name: "list", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "List recorded Leader wakes.", usage: "yui task wake list <task> [--limit <1..100>] [--cursor <cursor>]", options: ["--limit", "--cursor"] },
|
|
1092
|
-
{ name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show
|
|
1099
|
+
{ name: "show", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Show fixed wake inputs/results; expand diagnostics with --history.", usage: "yui task wake show <task> <wake> [--history] [--cursor <cursor>]", options: ["--history", "--cursor"] },
|
|
1093
1100
|
{ name: "retry", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Retry a rejected notification after correcting its cause; preserve original input.",
|
|
1094
1101
|
usage: "yui task wake retry <task> <wake> --reason <correction>", options: ["--reason"] },
|
|
1095
1102
|
{ name: "resolve", discovery: { surface: "managed", audiences: ["operator", "leader"] }, summary: "Release an unknown notification claim after explicit quiescence evidence, without replay.",
|
|
@@ -1514,13 +1521,14 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1514
1521
|
{
|
|
1515
1522
|
name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
1516
1523
|
summary: "List Project knowledge.",
|
|
1517
|
-
usage: "yui project knowledge list <project> [--all]",
|
|
1518
|
-
options: ["--all"]
|
|
1524
|
+
usage: "yui project knowledge list <project> [--all] [--limit <1..100>] [--cursor <cursor>]",
|
|
1525
|
+
options: ["--all", "--limit", "--cursor"]
|
|
1519
1526
|
},
|
|
1520
1527
|
{
|
|
1521
1528
|
name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
1522
1529
|
summary: "Read one Project knowledge entry.",
|
|
1523
|
-
usage: "yui project knowledge show <project> <knowledge>"
|
|
1530
|
+
usage: "yui project knowledge show <project> <knowledge> [--cursor <cursor>]",
|
|
1531
|
+
options: ["--cursor"]
|
|
1524
1532
|
},
|
|
1525
1533
|
{
|
|
1526
1534
|
name: "propose", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
@@ -1538,13 +1546,14 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1538
1546
|
{
|
|
1539
1547
|
name: "list", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
1540
1548
|
summary: "List Knowledge promotion proposals (pending by default).",
|
|
1541
|
-
usage: "yui project knowledge proposals list <project> [--status pending|accepted|rejected] [--all]",
|
|
1542
|
-
options: ["--status", "--all"]
|
|
1549
|
+
usage: "yui project knowledge proposals list <project> [--status pending|accepted|rejected] [--all] [--limit <1..100>] [--cursor <cursor>]",
|
|
1550
|
+
options: ["--status", "--all", "--limit", "--cursor"]
|
|
1543
1551
|
},
|
|
1544
1552
|
{
|
|
1545
1553
|
name: "show", discovery: { surface: "public", audiences: ["public", "operator", "leader"] },
|
|
1546
1554
|
summary: "Show one Knowledge promotion proposal.",
|
|
1547
|
-
usage: "yui project knowledge proposals show <project> <proposal>"
|
|
1555
|
+
usage: "yui project knowledge proposals show <project> <proposal> [--cursor <cursor>]",
|
|
1556
|
+
options: ["--cursor"]
|
|
1548
1557
|
}
|
|
1549
1558
|
]
|
|
1550
1559
|
},
|
|
@@ -1586,7 +1595,7 @@ export const ROOT_COMMAND = buildNode({
|
|
|
1586
1595
|
sections: [
|
|
1587
1596
|
{ id: "lifecycle", title: "Lifecycle", entries: ["create", "project", "base", "update", "activate", "activation", "execution", "complete", "cancel", "reopen", "retire", "list", "show", "context", "next-action", "remote-delivery", "archive-preflight", "archive", "archive-cleanup", "replace", "reconcile", "upstream", "artifact"] },
|
|
1588
1597
|
{ id: "collaboration", title: "Collaboration", entries: ["message", "input", "grant", "workflow", "publication", "work", "run", "review", "integration", "role", "overlap", "change-set"] },
|
|
1589
|
-
{ id: "knowledge", title: "Task Knowledge", entries: ["brief", "decision", "milestone", "event", "continuation", "wake"] }
|
|
1598
|
+
{ id: "knowledge", title: "Task Knowledge", entries: ["search", "brief", "decision", "milestone", "event", "continuation", "wake", "usage"] }
|
|
1590
1599
|
],
|
|
1591
1600
|
children: taskChildren
|
|
1592
1601
|
},
|
|
@@ -35,7 +35,7 @@ export function assertTaskInvocationScope(args, env) {
|
|
|
35
35
|
if (args[0] === "jobs" || (args[0] === "task" && ["create", "overlap"].includes(args[1] ?? ""))) {
|
|
36
36
|
throw usageError("This global operation is outside the caller's Task; use its scoped Context.");
|
|
37
37
|
}
|
|
38
|
-
if (args[0] !== "task" || args[1]
|
|
38
|
+
if (args[0] !== "task" || ["list", "search"].includes(args[1] ?? ""))
|
|
39
39
|
return;
|
|
40
40
|
const diagnostic = taskDiagnosticTarget(args);
|
|
41
41
|
if (diagnostic !== undefined) {
|
|
@@ -251,6 +251,11 @@ export function renderExecutionAudit(report, width = defaultTableWidth()) {
|
|
|
251
251
|
lines.push("", "Observed usage — Task lifetime, observed sources only (audit time window not applied):");
|
|
252
252
|
for (const usage of report.usage.data) {
|
|
253
253
|
lines.push(` ${usage.taskId}: tokens=${formatUsageMetric(usage.tokens)}; tools=${formatUsageMetric(usage.toolCalls)}; elapsed=${formatUsageMetric(usage.elapsedSeconds, "s")}; native execution sum=${formatUsageMetric(usage.executionSeconds, "s")}`);
|
|
254
|
+
for (const kind of ["actual", "estimated"]) {
|
|
255
|
+
const costs = usage.costs[kind];
|
|
256
|
+
lines.push(` ${kind} cost: ${costs.amounts.length === 0 ? `unknown (${costs.reasons.join(", ")})`
|
|
257
|
+
: costs.amounts.map(amount => `${formatUsageMetric(amount)} ${amount.currency}`).join("; ")} (observed sources only, not a bill)`);
|
|
258
|
+
}
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
else {
|
|
@@ -16,8 +16,38 @@ import { isSystemRoleName, SYSTEM_ROLE_NAMES, systemRoleDescription } from "../r
|
|
|
16
16
|
import { hasAgentConfigOptions, hasNoRoleMutation, parseRoleOptions, patchRoleAgentBinding, roleOptionSpecs, roleProfileFrom, roleProfilePatch } from "./roleConfiguration.js";
|
|
17
17
|
import { hasRoleLaunchContextOptions, validateConfiguredRoleSkills } from "./roleSkillValidation.js";
|
|
18
18
|
import { assertLiveRoleSessionAcknowledged, assertRoleRuntimeMutationAllowed, LIVE_SESSION_ACKNOWLEDGEMENT_OPTION } from "./roleRuntimeGuard.js";
|
|
19
|
+
/** Preview the complete binding, not just the fields supplied by the caller. */
|
|
20
|
+
export function previewGlobalRoleAgentConfigurationMutation(args, store, options = {}) {
|
|
21
|
+
const [command, rawName, ...tail] = args;
|
|
22
|
+
if (command !== "add" && command !== "update")
|
|
23
|
+
return undefined;
|
|
24
|
+
const name = roleName(rawName);
|
|
25
|
+
const parsed = parseRoleOptions(tail, roleOptionSpecs({
|
|
26
|
+
update: command === "update", includeAgent: true, includeWorkspace: true
|
|
27
|
+
}));
|
|
28
|
+
if (command === "update" && !hasAgentConfigOptions(parsed))
|
|
29
|
+
return undefined;
|
|
30
|
+
const role = command === "update" ? requireRole(name, store) : undefined;
|
|
31
|
+
const agentId = command === "add"
|
|
32
|
+
? required(parsed.one("--agent"), "--agent")
|
|
33
|
+
: parsed.one("--agent")?.trim() || role.activeAgentId;
|
|
34
|
+
const agent = requireAgent(agentId, store);
|
|
35
|
+
const binding = patchRoleAgentBinding(role?.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent)), parsed);
|
|
36
|
+
return {
|
|
37
|
+
agentId, config: binding.config,
|
|
38
|
+
cwd: trimmed(parsed.one("--workspace")) ?? role?.workspace
|
|
39
|
+
?? (options.yuiHome !== undefined ? managedGlobalRoleWorkspace(options.yuiHome)
|
|
40
|
+
: store.getConfig().defaultWorkspace ?? process.cwd())
|
|
41
|
+
};
|
|
42
|
+
}
|
|
19
43
|
export function applyGlobalInputControl(name, input, store, options = {}) {
|
|
20
44
|
assertGlobalInputAuthority(store, options, name);
|
|
45
|
+
if (input.expectedSessionId !== undefined) {
|
|
46
|
+
const set = store.getGlobalRoleSessionSet(name);
|
|
47
|
+
if (set?.sessions[set.activeAgentId]?.nativeSessionId !== input.expectedSessionId) {
|
|
48
|
+
throw usageError("Selected native Session changed; refresh before sending.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
21
51
|
return input.action === "interrupt"
|
|
22
52
|
? interruptGlobalInput(name, input, store)
|
|
23
53
|
: messageGlobalInput(name, input, store, options);
|
|
@@ -153,6 +183,7 @@ function roleContext(args, store, options) {
|
|
|
153
183
|
// Historical interrupt receipts are available through Session inspection;
|
|
154
184
|
// only this Turn's exact identity is required at entry.
|
|
155
185
|
sessionManifestPath: environment.YUI_SESSION_MANIFEST,
|
|
186
|
+
skillPackages: session?.effective.skillPackages ?? null,
|
|
156
187
|
cliCommand: "yui"
|
|
157
188
|
});
|
|
158
189
|
const data = boundedDocument(context, `global:${name}/context`, parsed.cursor);
|
|
@@ -206,6 +237,7 @@ function addRole(args, store, options) {
|
|
|
206
237
|
? managedGlobalRoleWorkspace(options.yuiHome)
|
|
207
238
|
: tx.getConfig().defaultWorkspace ?? process.cwd());
|
|
208
239
|
const binding = patchRoleAgentBinding(createRoleAgentBinding(definition(agent)), parsed);
|
|
240
|
+
options.validateAgentConfiguration?.({ agentId, config: binding.config, cwd: workspace });
|
|
209
241
|
const profile = roleProfileFrom(parsed);
|
|
210
242
|
validateConfiguredRoleSkills(options.yuiHome, profile.skills ?? []);
|
|
211
243
|
const role = createGlobalRole(name, [binding], agent.id, workspace, now, profile);
|
|
@@ -295,7 +327,11 @@ function updateRole(args, store, options) {
|
|
|
295
327
|
const agentId = parsed.one("--agent")?.trim() || role.activeAgentId;
|
|
296
328
|
const agent = requireAgent(agentId, tx);
|
|
297
329
|
const binding = role.agentBindings[agentId] ?? createRoleAgentBinding(definition(agent));
|
|
298
|
-
|
|
330
|
+
const patched = patchRoleAgentBinding(binding, parsed);
|
|
331
|
+
options.validateAgentConfiguration?.({
|
|
332
|
+
agentId, config: patched.config, cwd: workspace ?? role.workspace
|
|
333
|
+
});
|
|
334
|
+
bindings = { ...role.agentBindings, [agentId]: patched };
|
|
299
335
|
}
|
|
300
336
|
const updated = updateGlobalRole(role, {
|
|
301
337
|
...(workspace === undefined ? {} : { workspace }),
|
|
@@ -537,7 +573,8 @@ export function sendGlobalRoleMessageCommand(store, roleName, body, author, inpu
|
|
|
537
573
|
const original = existing.inputControl ?? existing.interruptThen?.reusedInput;
|
|
538
574
|
if (original?.action !== inputControl.action
|
|
539
575
|
|| existing.body !== body
|
|
540
|
-
|| original.expectedTarget !== inputControl.expectedTarget
|
|
576
|
+
|| original.expectedTarget !== inputControl.expectedTarget
|
|
577
|
+
|| original.expectedSessionId !== inputControl.expectedSessionId) {
|
|
541
578
|
throw usageError(`Input requestId ${inputControl.requestId} was already used with different content or target; use a new requestId for a new input.`);
|
|
542
579
|
}
|
|
543
580
|
return { message: existing, idempotentReplay: true };
|
|
@@ -630,7 +667,8 @@ function messageGlobalInput(name, input, store, options) {
|
|
|
630
667
|
? environment.YUI_ROLE === "operator" ? "operator" : "agent" : "user" };
|
|
631
668
|
const persisted = store.transaction((tx) => {
|
|
632
669
|
requireRole(name, tx);
|
|
633
|
-
return sendGlobalRoleMessageCommand(tx, name, body, author, { action, requestId, ...(expectedTarget === undefined ? {} : { expectedTarget })
|
|
670
|
+
return sendGlobalRoleMessageCommand(tx, name, body, author, { action, requestId, ...(expectedTarget === undefined ? {} : { expectedTarget }),
|
|
671
|
+
...(input.expectedSessionId === undefined ? {} : { expectedSessionId: input.expectedSessionId }) }, now);
|
|
634
672
|
});
|
|
635
673
|
if (action === "queue") {
|
|
636
674
|
const state = persisted.idempotentReplay ? "idempotent-replay" : "queued";
|