@zq-silk/yui 0.15.8 → 0.15.11
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 -0
- package/ARCHITECTURE.zh-CN.md +151 -0
- package/README.md +211 -14
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/artifacts/artifactCapability.js +74 -0
- package/dist/artifacts/artifactCommitLock.js +249 -0
- package/dist/artifacts/artifactPaths.js +151 -0
- package/dist/artifacts/gitArtifactRef.js +146 -0
- package/dist/artifacts/managedGit.js +332 -0
- package/dist/artifacts/taskArtifactRepository.js +277 -0
- package/dist/cli/commandCatalog.js +40 -16
- package/dist/cli/interactionPolicy.js +3 -3
- package/dist/cli/updateOrchestrator.js +24 -1
- package/dist/cli/updatePorts.js +7 -3
- package/dist/cli/upgradeCommand.js +42 -2
- package/dist/cli.js +403 -93
- package/dist/commands/globalRoleCommands.js +314 -4
- package/dist/commands/operatorCommands.js +33 -2
- package/dist/commands/projectCommands.js +6 -7
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActivationCommands.js +22 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +846 -155
- package/dist/commands/taskIntegrationCommands.js +16 -38
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskRemoteDeliveryCommand.js +6 -6
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +28 -16
- package/dist/context/taskContext.js +64 -5
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +446 -13
- package/dist/controller/globalInputDelivery.js +119 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +81 -6
- package/dist/controller/runtimeEventInbox.js +32 -3
- package/dist/controller/runtimeEventProcessor.js +26 -6
- package/dist/controller/runtimeHookRunFence.js +75 -19
- package/dist/controller/structuredProviderObservation.js +133 -70
- package/dist/coordination/workMailboxQueue.js +5 -0
- package/dist/execution/workItemExecutionProjection.js +1 -1
- package/dist/executor/agentExecutor.js +64 -4
- package/dist/executor/executorRegistry.js +3 -0
- package/dist/executor/fileRoleLaunchPlanner.js +78 -118
- package/dist/integration/deliveryObligation.js +2 -1
- package/dist/integration/gitIntegrationService.js +312 -382
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/kernel/builtinCapabilities.js +32 -24
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +423 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/plugins/pluginService.js +11 -3
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +72 -15
- package/dist/repository/taskWorkspaceCoordinator.js +134 -0
- package/dist/repository/taskWorkspacePreparer.js +120 -49
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/resources/projectResource.js +0 -48
- package/dist/resources/projectResourceService.js +3 -81
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentHost.js +152 -72
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +12 -1
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +29 -16
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/artifactsToGit.js +338 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/submitIntent.js +126 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +173 -7
- package/dist/storage/sqliteStore.js +41 -22
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -1
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +128 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/taskActivation.js +26 -0
- package/dist/task/taskActivationService.js +85 -69
- package/dist/task/taskSubmission.js +236 -0
- package/dist/web/assets/client/app.js +58 -2
- package/dist/web/assets/client/components.js +1 -0
- package/dist/web/assets/client/i18n.js +6 -0
- package/dist/web/assets/client/taskSurface.js +202 -7
- package/dist/web/assets/client/view.js +7 -4
- package/dist/web/assets/shell.js +23 -0
- package/dist/web/assets/styles/layout.js +1 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +135 -4
- package/dist/web/webSnapshot.js +4 -3
- package/dist/web/webTaskSurface.js +225 -8
- package/dist/workItem/workItem.js +14 -10
- package/dist/workspace/workItemChangeSetManager.js +18 -2
- package/docs/agent-result-consumption.md +2 -0
- package/docs/agent-result-consumption.zh-CN.md +81 -0
- package/docs/agent-runtime-drivers.md +2 -0
- package/docs/agent-runtime-drivers.zh-CN.md +77 -0
- package/docs/architecture/README.md +44 -32
- package/docs/architecture/README.zh-CN.md +43 -0
- package/docs/architecture/capabilities-and-resources.md +118 -79
- package/docs/architecture/capabilities-and-resources.zh-CN.md +83 -0
- package/docs/managed-turn-and-session-runtime.md +2 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +180 -0
- package/docs/observability/README.md +2 -0
- package/docs/observability/README.zh-CN.md +71 -0
- package/docs/plugin-sdk.md +320 -217
- package/docs/plugin-sdk.zh-CN.md +293 -0
- package/docs/provider-runtime.md +2 -0
- package/docs/provider-runtime.zh-CN.md +132 -0
- package/docs/release-workflow.md +41 -0
- package/docs/release-workflow.zh-CN.md +266 -0
- package/docs/roles-and-configuration.md +2 -0
- package/docs/roles-and-configuration.zh-CN.md +96 -0
- package/docs/sqlite-control-plane-design.md +225 -1
- package/docs/sqlite-control-plane-design.zh-CN.md +62 -0
- package/docs/task-dag-semantics.md +80 -57
- package/docs/task-dag-semantics.zh-CN.md +59 -0
- package/docs/task-delivery.md +2 -0
- package/docs/task-delivery.zh-CN.md +82 -0
- package/docs/task-local-identity.md +2 -0
- package/docs/task-local-identity.zh-CN.md +58 -0
- package/docs/testing/verification-levels.md +26 -0
- package/docs/testing/verification-levels.zh-CN.md +80 -0
- package/i18n/README.zh-CN.md +199 -10
- package/package.json +2 -1
- package/skills/yui-leader/SKILL.md +88 -331
- package/skills/yui-leader/references/execution.md +405 -0
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-leader/references/planning.md +109 -0
- package/skills/yui-leader/references/task-plugins.md +8 -4
- package/skills/yui-operator/SKILL.md +22 -4
- package/skills/yui-runtime/SKILL.md +27 -0
- package/skills/yui-runtime/references/publication.md +20 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Turn statuses that mean a Turn record is present and not yet terminal. This is
|
|
4
|
+
* only "there is something here", not "it is safe to target": `submitting` is a
|
|
5
|
+
* call still in flight and `delivery-unknown` is an outbound whose acceptance was
|
|
6
|
+
* never proven. Both are separated from `accepted` below (decision-3 §3/§8) so an
|
|
7
|
+
* unconfirmed delivery is reported as DELIVERY_UNKNOWN, never silently steered or
|
|
8
|
+
* cancelled as if it were a clean active Turn.
|
|
9
|
+
*/
|
|
10
|
+
const PRESENT_TURN_STATUSES = new Set(["submitting", "accepted", "delivery-unknown"]);
|
|
11
|
+
/**
|
|
12
|
+
* The scope-generic core. It takes an already-fetched Session set and the owner
|
|
13
|
+
* decoration for a resolved target, so Task and Global callers share the exact
|
|
14
|
+
* same capability, present-Turn, authority, and target-match logic.
|
|
15
|
+
*
|
|
16
|
+
* @param decorate Adds the owner-specific fields to a resolved target. For a
|
|
17
|
+
* Task Role it attaches the taskId; for a Global Role it attaches nothing.
|
|
18
|
+
*/
|
|
19
|
+
function resolveInputControl(sessions, kind, expectedTarget, roleName) {
|
|
20
|
+
const active = sessions?.sessions[sessions.activeAgentId];
|
|
21
|
+
// Capability is read from the declared Driver of the current active Session,
|
|
22
|
+
// never hardcoded by Agent name (decision-3 §7). When there is no active
|
|
23
|
+
// Session at all there is also no current Turn to target.
|
|
24
|
+
if (sessions == null || active === undefined || active.status !== "active") {
|
|
25
|
+
return { outcome: "no-active-turn", code: "NO_ACTIVE_TURN",
|
|
26
|
+
detail: "No active native Session holds a current Turn for this Role." };
|
|
27
|
+
}
|
|
28
|
+
const capabilities = builtinAgentDriverRegistry().requireByAdapterId(active.adapterId).capabilities;
|
|
29
|
+
if (kind === "steer" && capabilities.input.steer !== "fenced") {
|
|
30
|
+
return { outcome: "unsupported", code: "STEER_UNSUPPORTED",
|
|
31
|
+
detail: `Agent plan '${active.adapterId}' cannot steer its current Turn; `
|
|
32
|
+
+ "the input is saved for an explicit interrupt --then-message or a queue." };
|
|
33
|
+
}
|
|
34
|
+
if (kind === "interrupt" && capabilities.control.interruptDelivery !== "native") {
|
|
35
|
+
return { outcome: "unsupported", code: "INTERRUPT_UNSUPPORTED",
|
|
36
|
+
detail: `Agent plan '${active.adapterId}' has no native interrupt; Yui will not `
|
|
37
|
+
+ "stop the owned process, kill, restart, or detach it." };
|
|
38
|
+
}
|
|
39
|
+
// A Global Role now carries the same optional providerBinding as a Task Role;
|
|
40
|
+
// both read it the same way. Its absence is "no current Turn", never an error.
|
|
41
|
+
const binding = "providerBinding" in sessions ? sessions.providerBinding ?? null : null;
|
|
42
|
+
const turn = binding?.run ?? null;
|
|
43
|
+
if (binding === null || turn === null || !PRESENT_TURN_STATUSES.has(turn.status)) {
|
|
44
|
+
return { outcome: "no-active-turn", code: "NO_ACTIVE_TURN",
|
|
45
|
+
detail: "The Provider is not running a Turn now; there is nothing to steer or interrupt." };
|
|
46
|
+
}
|
|
47
|
+
// A human writer fence (an interactive takeover) owns the Turn; the Controller
|
|
48
|
+
// must not steer or cancel across it.
|
|
49
|
+
if (binding.authority.owner !== "controller") {
|
|
50
|
+
return { outcome: "target-changed", code: "TARGET_CHANGED",
|
|
51
|
+
detail: `Provider authority is ${binding.authority.owner}-held; release the takeover before a managed control.` };
|
|
52
|
+
}
|
|
53
|
+
const observedId = turn.nativeTurnId ?? turn.attemptId;
|
|
54
|
+
if (expectedTarget !== observedId
|
|
55
|
+
&& !(turn.nativeTurnId !== undefined && expectedTarget === turn.attemptId)) {
|
|
56
|
+
return { outcome: "target-changed", code: "TARGET_CHANGED",
|
|
57
|
+
detail: `The current Turn is ${observedId}, not ${expectedTarget}; re-read the Session before retrying.` };
|
|
58
|
+
}
|
|
59
|
+
// The right Turn is confirmed; now report its own delivery state. decision-3
|
|
60
|
+
// §3/§8, message-3 #7: an original Turn still in flight (`submitting`) or whose
|
|
61
|
+
// acceptance was never proven (`delivery-unknown`) is not a safe target.
|
|
62
|
+
// Steering it would push a second input onto an unconfirmed one; cancelling it
|
|
63
|
+
// would guess a stop against an attempt that may not have landed. Report
|
|
64
|
+
// DELIVERY_UNKNOWN so the caller confirms the original attempt first — never a
|
|
65
|
+
// silent downgrade to another action (§10).
|
|
66
|
+
if (turn.status !== "accepted") {
|
|
67
|
+
return { outcome: "delivery-unknown", code: "DELIVERY_UNKNOWN",
|
|
68
|
+
detail: `The current Turn's own delivery is ${turn.status}; confirm the original attempt `
|
|
69
|
+
+ "before steering or interrupting. Do not reissue under a new requestId or a different action." };
|
|
70
|
+
}
|
|
71
|
+
if (kind === "steer" && turn.nativeTurnId === undefined) {
|
|
72
|
+
return { outcome: "delivery-unknown", code: "DELIVERY_UNKNOWN",
|
|
73
|
+
detail: "The Provider has not supplied an exact native Turn id for steering." };
|
|
74
|
+
}
|
|
75
|
+
return { outcome: "ready", target: {
|
|
76
|
+
roleName, agentId: active.agentId, adapterId: active.adapterId,
|
|
77
|
+
nativeSessionId: active.nativeSessionId,
|
|
78
|
+
...(turn.nativeTurnId === undefined ? {} : { nativeTurnId: turn.nativeTurnId }),
|
|
79
|
+
attemptId: turn.attemptId,
|
|
80
|
+
authority: { epoch: binding.authority.epoch, owner: "controller",
|
|
81
|
+
holderId: binding.authority.holderId ?? "controller" }
|
|
82
|
+
} };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolve a live steer/interrupt against a Task Role's current native Turn.
|
|
86
|
+
*
|
|
87
|
+
* @param expectedTarget The Leader's exact expectation for the current Turn,
|
|
88
|
+
* as observed (its nativeTurnId, or its attemptId when no native id exists).
|
|
89
|
+
* A steer/interrupt that no longer matches is TARGET_CHANGED, never retargeted.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveTaskInputControl(store, taskId, roleName, kind, expectedTarget) {
|
|
92
|
+
const resolution = resolveInputControl(store.getTaskRoleSessionSet(taskId, roleName), kind, expectedTarget, roleName);
|
|
93
|
+
// A Task target carries its taskId so the live edge addresses the exact Host.
|
|
94
|
+
return resolution.outcome === "ready"
|
|
95
|
+
? { outcome: "ready", target: { taskId, ...resolution.target } }
|
|
96
|
+
: resolution;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a live steer/interrupt against a Global Role's current native Turn
|
|
100
|
+
* (decision-3 §6/§9). Identical contract to the Task resolver, reading the
|
|
101
|
+
* Global Role's own Session set; the resolved target carries no taskId because a
|
|
102
|
+
* Global Role has no owning Task and never fabricates one.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveGlobalInputControl(store, roleName, kind, expectedTarget) {
|
|
105
|
+
return resolveInputControl(store.getGlobalRoleSessionSet(roleName), kind, expectedTarget, roleName);
|
|
106
|
+
}
|
package/dist/message/message.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
2
2
|
export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
|
|
3
|
+
/**
|
|
4
|
+
* How a user/operator submission asks its Task to react (task-32 Requirement A).
|
|
5
|
+
*
|
|
6
|
+
* - `record`: save only. No Leader wake, no planning, no activation.
|
|
7
|
+
* - `discuss`: save and route to planning (the default when a submission omits
|
|
8
|
+
* an intent, so an old client keeps its existing Leader-waking behaviour).
|
|
9
|
+
* - `develop`: save the requirement and its activation intent; an unplanned
|
|
10
|
+
* Draft records an activation request and queues only activation processing.
|
|
11
|
+
*
|
|
12
|
+
* The intent is never inferred from body text and is only meaningful on a
|
|
13
|
+
* user/operator submission: a role-result or system Message can never carry one,
|
|
14
|
+
* so an internal Agent report cannot acquire develop authority.
|
|
15
|
+
*/
|
|
16
|
+
export const TASK_SUBMISSION_INTENTS = ["record", "discuss", "develop"];
|
|
17
|
+
/** The two durable input actions an authorized caller can choose (decision-3).
|
|
18
|
+
* `interrupt` is a control operation, not a persisted Message action, so it is
|
|
19
|
+
* absent here. */
|
|
20
|
+
export const TASK_MESSAGE_INPUT_ACTIONS = ["queue", "steer"];
|
|
21
|
+
/** The recordable outcomes of a steer's one live control attempt (message-5 gap
|
|
22
|
+
* D). `not-submitted` is not here: it is the absence of a record, not a value. */
|
|
23
|
+
export const TASK_MESSAGE_INPUT_CONTROL_OUTCOMES = ["pending", "accepted", "rejected", "delivery-unknown"];
|
|
3
24
|
export function createTaskMessage(id, taskId, body, kind, author, now, context = {}) {
|
|
4
25
|
validateKindAndAuthor(kind, author);
|
|
5
26
|
const message = {
|
|
@@ -12,6 +33,44 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
|
|
|
12
33
|
...(context.wakePolicy === undefined
|
|
13
34
|
? {}
|
|
14
35
|
: { wakePolicy: context.wakePolicy }),
|
|
36
|
+
...(context.intent === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { intent: context.intent }),
|
|
39
|
+
...(context.submissionKey === undefined
|
|
40
|
+
? {}
|
|
41
|
+
: { submissionKey: requireText(context.submissionKey, "Message submission key") }),
|
|
42
|
+
...(context.inputControl === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { inputControl: {
|
|
45
|
+
action: context.inputControl.action,
|
|
46
|
+
requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
|
|
47
|
+
...(context.inputControl.expectedTarget === undefined
|
|
48
|
+
? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
|
|
49
|
+
} }),
|
|
50
|
+
...(context.interruptThen === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { interruptThen: {
|
|
53
|
+
requestId: requireSafeIdentity(context.interruptThen.requestId, "Message interrupt requestId"),
|
|
54
|
+
targetAttemptId: requireText(context.interruptThen.targetAttemptId, "Message interrupt targetAttemptId"),
|
|
55
|
+
targetRoleName: context.interruptThen.targetRoleName,
|
|
56
|
+
targetAgentId: context.interruptThen.targetAgentId,
|
|
57
|
+
targetAdapterId: context.interruptThen.targetAdapterId,
|
|
58
|
+
targetNativeSessionId: context.interruptThen.targetNativeSessionId,
|
|
59
|
+
targetAuthorityEpoch: context.interruptThen.targetAuthorityEpoch,
|
|
60
|
+
targetAuthorityHolderId: context.interruptThen.targetAuthorityHolderId,
|
|
61
|
+
...(context.interruptThen.targetNativeTurnId === undefined ? {} : {
|
|
62
|
+
targetNativeTurnId: context.interruptThen.targetNativeTurnId
|
|
63
|
+
}),
|
|
64
|
+
...(context.interruptThen.notDeliveredReason === undefined ? {} : {
|
|
65
|
+
notDeliveredReason: context.interruptThen.notDeliveredReason
|
|
66
|
+
}),
|
|
67
|
+
...(context.interruptThen.targetRunId === undefined
|
|
68
|
+
? {} : { targetRunId: context.interruptThen.targetRunId }),
|
|
69
|
+
...(context.interruptThen.reusedInput === undefined ? {} : { reusedInput: {
|
|
70
|
+
action: context.interruptThen.reusedInput.action,
|
|
71
|
+
requestId: requireSafeIdentity(context.interruptThen.reusedInput.requestId, "Reused input requestId")
|
|
72
|
+
} })
|
|
73
|
+
} }),
|
|
15
74
|
...(context.runId === undefined
|
|
16
75
|
? {}
|
|
17
76
|
: { runId: requireSafeIdentity(context.runId, "Message AgentRun id") }),
|
|
@@ -29,6 +88,69 @@ export function createTaskMessage(id, taskId, body, kind, author, now, context =
|
|
|
29
88
|
export function taskMessageAuthorLabel(author) {
|
|
30
89
|
return author.type === "role" ? author.roleName : author.type;
|
|
31
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The four visible states a later reader distinguishes for a steer input
|
|
93
|
+
* (message-5 gap D): the absence of any live attempt is `not-submitted`, and a
|
|
94
|
+
* recorded attempt reports its own observed outcome. A non-steer Message has no
|
|
95
|
+
* control state and returns `undefined`. This is the single reader other CLI
|
|
96
|
+
* invocations, the Web surface, and the Leader's Context read all use, so no
|
|
97
|
+
* consumer reconstructs the state from raw fields.
|
|
98
|
+
*/
|
|
99
|
+
export function taskMessageInputControlState(message) {
|
|
100
|
+
const wasSteer = message.inputControl?.action === "steer"
|
|
101
|
+
|| message.interruptThen?.reusedInput?.action === "steer";
|
|
102
|
+
if (!wasSteer)
|
|
103
|
+
return undefined;
|
|
104
|
+
return message.control?.outcome ?? "not-submitted";
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Record the observed disposition of a steer's one live control attempt as an
|
|
108
|
+
* independent, idempotent, monotonic control op (decision-3 §3/§8, message-5 gap
|
|
109
|
+
* D). The op is keyed by the exact `receiptId` fence and the steer's own
|
|
110
|
+
* `requestId`; a record naming a different Message's receipt or a different
|
|
111
|
+
* input identity is refused, so an outcome can never be folded onto the wrong
|
|
112
|
+
* Message or fork a second op. Terminals never rewind to `pending` and never
|
|
113
|
+
* overwrite a different terminal — a repeated or out-of-order live/Host record
|
|
114
|
+
* is absorbed, never a conflicting second write. The frozen business
|
|
115
|
+
* {@link TaskMessageInputControl} intent is left untouched.
|
|
116
|
+
*/
|
|
117
|
+
export function recordTaskMessageControlOutcome(message, record) {
|
|
118
|
+
const inputRequestId = message.inputControl?.requestId ?? message.interruptThen?.reusedInput?.requestId;
|
|
119
|
+
const wasSteer = message.inputControl?.action === "steer"
|
|
120
|
+
|| message.interruptThen?.reusedInput?.action === "steer";
|
|
121
|
+
if (!wasSteer || inputRequestId === undefined) {
|
|
122
|
+
throw new Error("Only a steer input Message records a live control outcome.");
|
|
123
|
+
}
|
|
124
|
+
if (record.requestId !== inputRequestId) {
|
|
125
|
+
throw new Error("Control outcome requestId must match the steer input identity.");
|
|
126
|
+
}
|
|
127
|
+
const existing = message.control;
|
|
128
|
+
if (existing !== undefined && existing.receiptId !== record.receiptId) {
|
|
129
|
+
throw new Error("Control outcome receiptId cannot name another control op.");
|
|
130
|
+
}
|
|
131
|
+
// Monotonic freeze (decision-3 §8, message-5 gap D). Once any terminal is
|
|
132
|
+
// proven it is final: a later `pending` never rewinds it and a different
|
|
133
|
+
// terminal never overwrites it — the first proven terminal wins. While still
|
|
134
|
+
// `pending`, an identical repeat is idempotent. Each frozen/absorbed case
|
|
135
|
+
// returns the Message unchanged so a fold can persist without a second write.
|
|
136
|
+
if (existing !== undefined) {
|
|
137
|
+
if (existing.outcome === "accepted" || existing.outcome === "rejected")
|
|
138
|
+
return message;
|
|
139
|
+
if (record.outcome === "pending" || record.outcome === existing.outcome)
|
|
140
|
+
return message;
|
|
141
|
+
}
|
|
142
|
+
const updated = {
|
|
143
|
+
...message,
|
|
144
|
+
control: {
|
|
145
|
+
requestId: requireSafeIdentity(record.requestId, "Message control requestId"),
|
|
146
|
+
receiptId: requireText(record.receiptId, "Message control receiptId"),
|
|
147
|
+
outcome: record.outcome,
|
|
148
|
+
observedAt: record.observedAt.toISOString()
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
validateTaskMessage(updated);
|
|
152
|
+
return updated;
|
|
153
|
+
}
|
|
32
154
|
/** A role-result reference never duplicates the execution's report body. */
|
|
33
155
|
export function expandTaskMessageResult(message, getRun) {
|
|
34
156
|
if (message.resultRef === undefined)
|
|
@@ -57,6 +179,24 @@ export function updateDraftTaskMessage(message, update) {
|
|
|
57
179
|
validateTaskMessage(updated);
|
|
58
180
|
return updated;
|
|
59
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Attach the §2.3 receipt a keyed submission earned, in the same transaction that
|
|
184
|
+
* saved and routed the Message. The receipt is a write-once fact: it is only set
|
|
185
|
+
* on a keyed Message that has none yet, so a replay (which never re-routes) can
|
|
186
|
+
* never overwrite the disposition the original submission recorded.
|
|
187
|
+
*/
|
|
188
|
+
export function withSubmissionReceipt(message, receipt) {
|
|
189
|
+
validateTaskMessage(message);
|
|
190
|
+
if (message.submissionKey === undefined) {
|
|
191
|
+
throw new Error(`A submission receipt requires a submission key: ${message.id}.`);
|
|
192
|
+
}
|
|
193
|
+
if (message.submissionReceipt !== undefined) {
|
|
194
|
+
throw new Error(`Submission receipt is already recorded: ${message.id}.`);
|
|
195
|
+
}
|
|
196
|
+
const updated = { ...message, submissionReceipt: receipt };
|
|
197
|
+
validateTaskMessage(updated);
|
|
198
|
+
return updated;
|
|
199
|
+
}
|
|
60
200
|
export function validateTaskMessage(message) {
|
|
61
201
|
if (message.schemaVersion !== 3)
|
|
62
202
|
throw new Error("Task Message must use schemaVersion 3.");
|
|
@@ -74,6 +214,90 @@ export function validateTaskMessage(message) {
|
|
|
74
214
|
&& message.kind !== "operator") {
|
|
75
215
|
throw new Error("Message wakePolicy is only valid for user/operator messages.");
|
|
76
216
|
}
|
|
217
|
+
if (message.intent !== undefined
|
|
218
|
+
&& !TASK_SUBMISSION_INTENTS.includes(message.intent)) {
|
|
219
|
+
throw new Error(`Message intent is invalid: ${String(message.intent)}.`);
|
|
220
|
+
}
|
|
221
|
+
if (message.intent !== undefined
|
|
222
|
+
&& message.kind !== "user"
|
|
223
|
+
&& message.kind !== "operator") {
|
|
224
|
+
throw new Error("Message intent is only valid for user/operator messages.");
|
|
225
|
+
}
|
|
226
|
+
if (message.submissionKey !== undefined) {
|
|
227
|
+
requireText(message.submissionKey, "Message submission key");
|
|
228
|
+
if (message.kind !== "user" && message.kind !== "operator") {
|
|
229
|
+
throw new Error("Message submission key is only valid for user/operator messages.");
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (message.submissionReceipt !== undefined && message.submissionKey === undefined) {
|
|
233
|
+
throw new Error("Message submission receipt requires a submission key.");
|
|
234
|
+
}
|
|
235
|
+
if (message.inputControl !== undefined) {
|
|
236
|
+
if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.inputControl.action)) {
|
|
237
|
+
throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
|
|
238
|
+
}
|
|
239
|
+
requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
|
|
240
|
+
if (message.inputControl.expectedTarget !== undefined) {
|
|
241
|
+
requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
|
|
242
|
+
if (message.inputControl.action !== "steer") {
|
|
243
|
+
throw new Error("Only a steer input binds an expectedTarget.");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (message.interruptThen !== undefined) {
|
|
248
|
+
requireSafeIdentity(message.interruptThen.requestId, "Message interrupt requestId");
|
|
249
|
+
requireText(message.interruptThen.targetAttemptId, "Message interrupt targetAttemptId");
|
|
250
|
+
requireSafeIdentity(message.interruptThen.targetRoleName, "Message interrupt target Role");
|
|
251
|
+
requireSafeIdentity(message.interruptThen.targetAgentId, "Message interrupt target Agent");
|
|
252
|
+
requireSafeIdentity(message.interruptThen.targetAdapterId, "Message interrupt target Adapter");
|
|
253
|
+
requireText(message.interruptThen.targetNativeSessionId, "Message interrupt target Session");
|
|
254
|
+
requireText(message.interruptThen.targetAuthorityHolderId, "Message interrupt authority holder");
|
|
255
|
+
if (message.interruptThen.notDeliveredReason !== undefined) {
|
|
256
|
+
requireText(message.interruptThen.notDeliveredReason, "Message interrupt nondelivery reason");
|
|
257
|
+
}
|
|
258
|
+
if (!Number.isSafeInteger(message.interruptThen.targetAuthorityEpoch)
|
|
259
|
+
|| message.interruptThen.targetAuthorityEpoch < 1)
|
|
260
|
+
throw new Error("Invalid interrupt authority epoch.");
|
|
261
|
+
if (message.interruptThen.targetRunId !== undefined) {
|
|
262
|
+
validateTaskRecordReference({ taskId: message.taskId, localId: message.interruptThen.targetRunId }, "run");
|
|
263
|
+
}
|
|
264
|
+
if (message.interruptThen.reusedInput !== undefined) {
|
|
265
|
+
if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.interruptThen.reusedInput.action)) {
|
|
266
|
+
throw new Error(`Reused input action is invalid: ${String(message.interruptThen.reusedInput.action)}.`);
|
|
267
|
+
}
|
|
268
|
+
requireSafeIdentity(message.interruptThen.reusedInput.requestId, "Reused input requestId");
|
|
269
|
+
}
|
|
270
|
+
// A then-handoff is an explicit deliverable Message, never an auto-queued
|
|
271
|
+
// steer: the two intents are mutually exclusive by construction. The prior
|
|
272
|
+
// steer identity, when one existed, is preserved as reusedInput provenance
|
|
273
|
+
// rather than left live on inputControl.
|
|
274
|
+
if (message.inputControl?.action === "steer") {
|
|
275
|
+
throw new Error("An interrupt-then handoff cannot also be a steer input.");
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (message.control !== undefined) {
|
|
279
|
+
requireSafeIdentity(message.control.requestId, "Message control requestId");
|
|
280
|
+
requireText(message.control.receiptId, "Message control receiptId");
|
|
281
|
+
if (!TASK_MESSAGE_INPUT_CONTROL_OUTCOMES.includes(message.control.outcome)) {
|
|
282
|
+
throw new Error(`Message control outcome is invalid: ${String(message.control.outcome)}.`);
|
|
283
|
+
}
|
|
284
|
+
if (typeof message.control.observedAt !== "string" || Number.isNaN(Date.parse(message.control.observedAt))) {
|
|
285
|
+
throw new Error("Message control observedAt is invalid.");
|
|
286
|
+
}
|
|
287
|
+
// A recorded control outcome is the disposition of a live steer attempt; it
|
|
288
|
+
// only exists for a steer input, or for the reused-steer provenance a
|
|
289
|
+
// handoff preserved. It is never fabricated on a plain queue or a Message
|
|
290
|
+
// that never carried a steer (decision-3 §3, message-5 gap D).
|
|
291
|
+
const inputRequestId = message.inputControl?.requestId ?? message.interruptThen?.reusedInput?.requestId;
|
|
292
|
+
const wasSteer = message.inputControl?.action === "steer"
|
|
293
|
+
|| message.interruptThen?.reusedInput?.action === "steer";
|
|
294
|
+
if (!wasSteer || inputRequestId === undefined) {
|
|
295
|
+
throw new Error("Only a steer input Message records a live control outcome.");
|
|
296
|
+
}
|
|
297
|
+
if (message.control.requestId !== inputRequestId) {
|
|
298
|
+
throw new Error("Message control requestId must match the steer input identity.");
|
|
299
|
+
}
|
|
300
|
+
}
|
|
77
301
|
if (message.runId !== undefined)
|
|
78
302
|
requireSafeIdentity(message.runId, "Message AgentRun id");
|
|
79
303
|
if (message.recipient !== undefined) {
|
|
@@ -130,6 +354,205 @@ export function validateTaskMessage(message) {
|
|
|
130
354
|
throw new Error("Message createdAt is invalid.");
|
|
131
355
|
}
|
|
132
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* The author kinds a Global Role input can carry. A Global Role has no Task
|
|
359
|
+
* Assignment, so a `role-result` (which references a Task AgentRun) is not one
|
|
360
|
+
* of them; user/operator/system inputs are (decision-3 §9/§11).
|
|
361
|
+
*/
|
|
362
|
+
export const GLOBAL_ROLE_MESSAGE_KINDS = ["user", "operator", "system", "agent"];
|
|
363
|
+
export function createGlobalRoleMessage(id, roleName, body, kind, author, now, context = {}) {
|
|
364
|
+
validateGlobalKindAndAuthor(kind, author);
|
|
365
|
+
const message = {
|
|
366
|
+
schemaVersion: 1,
|
|
367
|
+
id: validateGlobalRoleMessageId(id),
|
|
368
|
+
roleName: requireSafeIdentity(roleName, "Global message Role name"),
|
|
369
|
+
kind,
|
|
370
|
+
author: { type: author.type },
|
|
371
|
+
body: requireBody(body),
|
|
372
|
+
...(context.inputControl === undefined
|
|
373
|
+
? {}
|
|
374
|
+
: { inputControl: {
|
|
375
|
+
action: context.inputControl.action,
|
|
376
|
+
requestId: requireSafeIdentity(context.inputControl.requestId, "Message input requestId"),
|
|
377
|
+
...(context.inputControl.expectedTarget === undefined
|
|
378
|
+
? {} : { expectedTarget: requireText(context.inputControl.expectedTarget, "Message input expectedTarget") })
|
|
379
|
+
} }),
|
|
380
|
+
createdAt: now.toISOString()
|
|
381
|
+
};
|
|
382
|
+
validateGlobalRoleMessage(message);
|
|
383
|
+
return message;
|
|
384
|
+
}
|
|
385
|
+
export function validateGlobalRoleMessage(message) {
|
|
386
|
+
if (message.schemaVersion !== 1)
|
|
387
|
+
throw new Error("Global Role Message must use schemaVersion 1.");
|
|
388
|
+
validateGlobalRoleMessageId(message.id);
|
|
389
|
+
requireSafeIdentity(message.roleName, "Global message Role name");
|
|
390
|
+
requireBody(message.body);
|
|
391
|
+
validateGlobalKindAndAuthor(message.kind, message.author);
|
|
392
|
+
if (message.deliveryTarget !== undefined) {
|
|
393
|
+
requireText(message.deliveryTarget.agentId, "Global input Agent");
|
|
394
|
+
requireText(message.deliveryTarget.nativeSessionId, "Global input Session");
|
|
395
|
+
}
|
|
396
|
+
if (message.control !== undefined) {
|
|
397
|
+
requireSafeIdentity(message.control.requestId, "Global input requestId");
|
|
398
|
+
requireText(message.control.receiptId, "Global input receiptId");
|
|
399
|
+
if (!TASK_MESSAGE_INPUT_CONTROL_OUTCOMES.includes(message.control.outcome)
|
|
400
|
+
|| !Number.isFinite(Date.parse(message.control.observedAt)))
|
|
401
|
+
throw new Error("Global input disposition is invalid.");
|
|
402
|
+
}
|
|
403
|
+
if (message.inputControl !== undefined) {
|
|
404
|
+
if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.inputControl.action)) {
|
|
405
|
+
throw new Error(`Message input action is invalid: ${String(message.inputControl.action)}.`);
|
|
406
|
+
}
|
|
407
|
+
requireSafeIdentity(message.inputControl.requestId, "Message input requestId");
|
|
408
|
+
if (message.inputControl.expectedTarget !== undefined) {
|
|
409
|
+
requireText(message.inputControl.expectedTarget, "Message input expectedTarget");
|
|
410
|
+
if (message.inputControl.action !== "steer") {
|
|
411
|
+
throw new Error("Only a steer input binds an expectedTarget.");
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (message.delivery !== undefined) {
|
|
416
|
+
if (message.delivery.via !== "provider" && message.delivery.via !== "transport") {
|
|
417
|
+
throw new Error(`Global message delivery via is invalid: ${String(message.delivery.via)}.`);
|
|
418
|
+
}
|
|
419
|
+
if (typeof message.delivery.deliveredAt !== "string"
|
|
420
|
+
|| Number.isNaN(Date.parse(message.delivery.deliveredAt))) {
|
|
421
|
+
throw new Error("Global message delivery deliveredAt is invalid.");
|
|
422
|
+
}
|
|
423
|
+
// Receipt evidence is shared by queue, explicit then and native steer.
|
|
424
|
+
if (message.inputControl === undefined && message.interruptThen === undefined) {
|
|
425
|
+
throw new Error("Only a durable Global input records Provider delivery.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (message.interruptThen !== undefined) {
|
|
429
|
+
requireSafeIdentity(message.interruptThen.requestId, "Global message interrupt requestId");
|
|
430
|
+
requireText(message.interruptThen.targetAttemptId, "Global message interrupt targetAttemptId");
|
|
431
|
+
requireText(message.interruptThen.targetNativeSessionId, "Global interrupt target Session");
|
|
432
|
+
requireText(message.interruptThen.targetAgentId, "Global interrupt target Agent");
|
|
433
|
+
requireText(message.interruptThen.targetAuthorityHolderId, "Global interrupt writer");
|
|
434
|
+
if (!Number.isSafeInteger(message.interruptThen.targetAuthorityEpoch)
|
|
435
|
+
|| message.interruptThen.targetAuthorityEpoch < 1)
|
|
436
|
+
throw new Error("Global interrupt epoch is invalid.");
|
|
437
|
+
if (message.interruptThen.targetNativeTurnId !== undefined) {
|
|
438
|
+
requireText(message.interruptThen.targetNativeTurnId, "Global message interrupt targetNativeTurnId");
|
|
439
|
+
}
|
|
440
|
+
if (message.interruptThen.reusedInput !== undefined) {
|
|
441
|
+
if (!TASK_MESSAGE_INPUT_ACTIONS.includes(message.interruptThen.reusedInput.action)) {
|
|
442
|
+
throw new Error(`Reused input action is invalid: ${String(message.interruptThen.reusedInput.action)}.`);
|
|
443
|
+
}
|
|
444
|
+
requireSafeIdentity(message.interruptThen.reusedInput.requestId, "Reused input requestId");
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (message.notDelivered !== undefined) {
|
|
448
|
+
requireText(message.notDelivered.reason, "Global message nondelivery reason");
|
|
449
|
+
if (typeof message.notDelivered.at !== "string"
|
|
450
|
+
|| Number.isNaN(Date.parse(message.notDelivered.at))) {
|
|
451
|
+
throw new Error("Global message nondelivery timestamp is invalid.");
|
|
452
|
+
}
|
|
453
|
+
// A delivered Message is a settled positive fact; a not-delivered fact is its
|
|
454
|
+
// visible negative twin. The two are mutually exclusive on one Message.
|
|
455
|
+
if (message.delivery !== undefined) {
|
|
456
|
+
throw new Error("A Global message cannot be both delivered and not-delivered.");
|
|
457
|
+
}
|
|
458
|
+
if (message.inputControl?.action !== "queue" && message.interruptThen === undefined) {
|
|
459
|
+
throw new Error("Only a queued or interrupt-then Global message records a nondelivery.");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (typeof message.createdAt !== "string" || Number.isNaN(Date.parse(message.createdAt))) {
|
|
463
|
+
throw new Error("Message createdAt is invalid.");
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/** A Global message local id is `global-message-<n>`; it carries no Task id. */
|
|
467
|
+
export function validateGlobalRoleMessageId(localId) {
|
|
468
|
+
const normalized = requireSafeIdentity(localId, "Global message id");
|
|
469
|
+
if (!/^global-message-[1-9]\d*$/.test(normalized)) {
|
|
470
|
+
throw new Error(`Global message id is invalid: ${localId}.`);
|
|
471
|
+
}
|
|
472
|
+
return normalized;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Return a copy of a queued Global Message marked delivered at its next legal
|
|
476
|
+
* opportunity — the Role's own authorized Context read (decision-3 §9). Only a
|
|
477
|
+
* `queue` input may be consumed this way and only once: a Message that already
|
|
478
|
+
* carries a delivery is returned unchanged, so a repeated self-read is
|
|
479
|
+
* idempotent and never re-delivers. A steer/interrupt Message, which targets the
|
|
480
|
+
* exact current Turn rather than a queued next opportunity, is never delivered
|
|
481
|
+
* here.
|
|
482
|
+
*/
|
|
483
|
+
export function markGlobalRoleMessageDelivered(message, now, via = "provider") {
|
|
484
|
+
if (message.inputControl === undefined && message.interruptThen === undefined) {
|
|
485
|
+
throw new Error("Only a durable Global input is delivered by a Provider.");
|
|
486
|
+
}
|
|
487
|
+
if (message.delivery?.via === "provider" || message.delivery?.via === via)
|
|
488
|
+
return message;
|
|
489
|
+
const delivered = {
|
|
490
|
+
...message,
|
|
491
|
+
...(message.control === undefined ? {} : { control: {
|
|
492
|
+
...message.control, outcome: via === "provider" ? "accepted" : "pending", observedAt: now.toISOString()
|
|
493
|
+
} }),
|
|
494
|
+
delivery: { deliveredAt: now.toISOString(), via }
|
|
495
|
+
};
|
|
496
|
+
validateGlobalRoleMessage(delivered);
|
|
497
|
+
return delivered;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Return a copy of an already-saved durable Global Message that claims the single
|
|
501
|
+
* interrupt-then continuation of an exact interrupted native Turn (decision-3
|
|
502
|
+
* §4). The original input identity, when the Message carried one, is preserved as
|
|
503
|
+
* `reusedInput` provenance rather than erased, so a replay of that requestId
|
|
504
|
+
* keeps resolving to this same Message and never creates a second input.
|
|
505
|
+
*/
|
|
506
|
+
export function claimGlobalRoleMessageInterruptThen(message, claim) {
|
|
507
|
+
const { inputControl, notDelivered: _priorNondelivery, ...rest } = message;
|
|
508
|
+
const claimed = {
|
|
509
|
+
...rest,
|
|
510
|
+
interruptThen: {
|
|
511
|
+
requestId: requireSafeIdentity(claim.requestId, "Global message interrupt requestId"),
|
|
512
|
+
targetAttemptId: requireText(claim.targetAttemptId, "Global message interrupt targetAttemptId"),
|
|
513
|
+
targetNativeSessionId: claim.targetNativeSessionId,
|
|
514
|
+
targetAgentId: claim.targetAgentId,
|
|
515
|
+
targetAuthorityEpoch: claim.targetAuthorityEpoch,
|
|
516
|
+
targetAuthorityHolderId: claim.targetAuthorityHolderId,
|
|
517
|
+
...(claim.targetNativeTurnId === undefined
|
|
518
|
+
? {}
|
|
519
|
+
: { targetNativeTurnId: requireText(claim.targetNativeTurnId, "Global message interrupt targetNativeTurnId") }),
|
|
520
|
+
...(inputControl === undefined ? {} : { reusedInput: inputControl })
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
validateGlobalRoleMessage(claimed);
|
|
524
|
+
return claimed;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Return a copy of a durable Global Message marked visibly not-delivered
|
|
528
|
+
* (decision-3 §5/§10), the Global twin of the Task {@link markNotDelivered}. An
|
|
529
|
+
* ordinary queue entry or a claimed interrupt-then handoff whose target can never
|
|
530
|
+
* prove a safe boundary fails here and stops holding the Role's pending set,
|
|
531
|
+
* rather than silently wedging it. It is idempotent for the same reason — a
|
|
532
|
+
* Message already carrying this exact reason is returned unchanged — and a
|
|
533
|
+
* delivered Message is never overwritten with a nondelivery.
|
|
534
|
+
*/
|
|
535
|
+
export function markGlobalRoleMessageNotDelivered(message, reason, now) {
|
|
536
|
+
if (message.delivery !== undefined) {
|
|
537
|
+
throw new Error("A delivered Global message cannot be marked not-delivered.");
|
|
538
|
+
}
|
|
539
|
+
if (message.notDelivered?.reason === reason)
|
|
540
|
+
return message;
|
|
541
|
+
const marked = {
|
|
542
|
+
...message,
|
|
543
|
+
notDelivered: { reason: requireText(reason, "Global message nondelivery reason"), at: now.toISOString() }
|
|
544
|
+
};
|
|
545
|
+
validateGlobalRoleMessage(marked);
|
|
546
|
+
return marked;
|
|
547
|
+
}
|
|
548
|
+
function validateGlobalKindAndAuthor(kind, author) {
|
|
549
|
+
if (!GLOBAL_ROLE_MESSAGE_KINDS.includes(kind)) {
|
|
550
|
+
throw new Error(`Global message kind is invalid: ${String(kind)}.`);
|
|
551
|
+
}
|
|
552
|
+
if (author?.type !== kind) {
|
|
553
|
+
throw new Error(`Global message kind ${kind} requires a ${kind} author.`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
133
556
|
function validateKindAndAuthor(kind, author) {
|
|
134
557
|
if (!TASK_MESSAGE_KINDS.includes(kind))
|
|
135
558
|
throw new Error(`Message kind is invalid: ${String(kind)}.`);
|