@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
package/dist/task/nextAction.js
CHANGED
|
@@ -121,6 +121,16 @@ export function projectNextAction(facts) {
|
|
|
121
121
|
recommendedCommand: `yui task activate ${task.id}`
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
|
+
const conflictedIntegration = facts.integrations.find(attempt => attempt.status === "conflicted");
|
|
125
|
+
if (conflictedIntegration !== undefined) {
|
|
126
|
+
return buildAction(facts, {
|
|
127
|
+
kind: "resolve-integration-conflicts",
|
|
128
|
+
reason: `Integration ${conflictedIntegration.id} has Git conflicts; the Leader resolves its workspace and continues without prior resolve or user approval.`,
|
|
129
|
+
refs: [ref("integration", conflictedIntegration.id)],
|
|
130
|
+
preconditions: [{ fact: "Git conflict resolution belongs to the Leader", satisfied: true }],
|
|
131
|
+
recommendedCommand: `yui task integration continue ${task.id}/${conflictedIntegration.id}`
|
|
132
|
+
});
|
|
133
|
+
}
|
|
124
134
|
const checkingIntegration = facts.integrations.find(attempt => attempt.status === "running" && attempt.jobId !== undefined);
|
|
125
135
|
if (checkingIntegration !== undefined) {
|
|
126
136
|
const job = facts.integrationJobs?.find(job => job.id === checkingIntegration.jobId);
|
|
@@ -146,6 +156,21 @@ export function projectNextAction(facts) {
|
|
|
146
156
|
recommendedCommand: `yui task integration continue ${task.id}/${checkingIntegration.id}`
|
|
147
157
|
});
|
|
148
158
|
}
|
|
159
|
+
const unfinishedIntegration = facts.integrations.find(attempt => attempt.status === "running" || attempt.status === "validating");
|
|
160
|
+
if (unfinishedIntegration !== undefined) {
|
|
161
|
+
return buildAction(facts, {
|
|
162
|
+
kind: "integrate-work-item",
|
|
163
|
+
reason: `Integration ${unfinishedIntegration.id} is unfinished. ${unfinishedIntegration.summary ?? "Continue from its persisted source and check evidence."}`,
|
|
164
|
+
refs: [ref("integration", unfinishedIntegration.id)],
|
|
165
|
+
preconditions: [{ fact: "Inspect exact Integration evidence before any retry", satisfied: true }],
|
|
166
|
+
recommendedCommand: `yui task integration continue ${task.id}/${unfinishedIntegration.id}`,
|
|
167
|
+
alternatives: [{
|
|
168
|
+
kind: "inspect-integration",
|
|
169
|
+
reason: "If completion cannot be proved, preserve evidence and choose formal abort and an authorized alternative.",
|
|
170
|
+
recommendedCommand: `yui task integration show ${task.id}/${unfinishedIntegration.id}`
|
|
171
|
+
}]
|
|
172
|
+
});
|
|
173
|
+
}
|
|
149
174
|
const candidateReady = facts.workItems
|
|
150
175
|
.find((item) => (item.status === "open" && item.currentCandidateId !== undefined));
|
|
151
176
|
if (candidateReady !== undefined) {
|
|
@@ -320,27 +345,35 @@ export function projectNextAction(facts) {
|
|
|
320
345
|
return synthesisSelectionAction(facts, "work", item.id);
|
|
321
346
|
}
|
|
322
347
|
const refs = [ref("work-item", item.id)];
|
|
348
|
+
const direct = item.assignee === undefined;
|
|
323
349
|
return buildAction(facts, {
|
|
324
350
|
kind: "implement-current-work-item",
|
|
325
|
-
reason:
|
|
351
|
+
reason: direct
|
|
352
|
+
? `Work Item ${item.id} has no managed assignee; the Leader can execute it directly.`
|
|
353
|
+
: `Work Item ${item.id} is assigned to ${item.assignee}; dispatch or continue that assignment.`,
|
|
326
354
|
refs,
|
|
327
355
|
preconditions: [
|
|
328
356
|
{ fact: `Work Item is ${item.status}`, satisfied: true, ref: refs[0] }
|
|
329
357
|
],
|
|
330
|
-
recommendedCommand:
|
|
331
|
-
|
|
332
|
-
{
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
recommendedCommand: direct
|
|
359
|
+
? `yui task work update ${task.id}/${item.id} running`
|
|
360
|
+
: `yui task work dispatch ${task.id}/${item.id}`,
|
|
361
|
+
alternatives: direct ? [
|
|
362
|
+
...(item.writeProjectIds.length === 0 ? [] : [{
|
|
363
|
+
kind: "isolate-work-item",
|
|
364
|
+
reason: "Prepare and inspect the WorkItem-owned code workspace before direct implementation.",
|
|
365
|
+
recommendedCommand: `yui task work isolate ${task.id}/${item.id}`,
|
|
366
|
+
refs
|
|
367
|
+
}]),
|
|
337
368
|
{
|
|
338
369
|
kind: "native-subagent",
|
|
339
|
-
reason: "Use
|
|
370
|
+
reason: "Use a bounded native child only when authorized and useful within the Leader's current scope.",
|
|
340
371
|
refs
|
|
341
372
|
}
|
|
342
|
-
],
|
|
343
|
-
judgmentRequired:
|
|
373
|
+
] : [],
|
|
374
|
+
judgmentRequired: direct
|
|
375
|
+
? "Honor explicit execution preferences. For code, use the WorkItem-owned workspace and integrate its Candidate before acceptance; no self-dispatch is needed."
|
|
376
|
+
: "Preserve the current managed Assignment and original results; do not switch ownership merely to follow a different default."
|
|
344
377
|
});
|
|
345
378
|
}
|
|
346
379
|
if (deliveryWorkItems.length === 0
|
|
@@ -22,6 +22,7 @@ export const TASK_ACTIVATION_EVENT = Object.freeze({
|
|
|
22
22
|
adopted: "task.activation-adopted",
|
|
23
23
|
failed: "task.activation-failed"
|
|
24
24
|
});
|
|
25
|
+
export const TASK_ACTIVATION_ORIGINS = Object.freeze(["explicit", "submit-develop"]);
|
|
25
26
|
/**
|
|
26
27
|
* The digest that decides whether a repeated requestId is the same request.
|
|
27
28
|
*
|
|
@@ -63,6 +64,7 @@ export function createTaskActivationRequest(taskId, input, now) {
|
|
|
63
64
|
partialResultRefs: []
|
|
64
65
|
},
|
|
65
66
|
startMode: input.startMode,
|
|
67
|
+
...(input.origin === undefined ? {} : { origin: input.origin }),
|
|
66
68
|
...(input.afterPlanningRun === undefined
|
|
67
69
|
? {}
|
|
68
70
|
: { afterPlanningRun: input.afterPlanningRun }),
|
|
@@ -194,6 +196,26 @@ export function admitTaskActivationRequest(task, request, planningRunIsActive) {
|
|
|
194
196
|
}
|
|
195
197
|
return { disposition: "ready", request };
|
|
196
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Whether the Controller may continue (auto-adopt) a released request without an
|
|
201
|
+
* explicit activation boundary call (task-32 §2.4).
|
|
202
|
+
*
|
|
203
|
+
* A deferred request already holds activation authority and is released by its
|
|
204
|
+
* planning Turn ending, so it is adoptable regardless of origin. An immediate
|
|
205
|
+
* request is auto-continued only when it carries a recognised provable origin:
|
|
206
|
+
* an explicit activation action, or a develop submission the shared transaction
|
|
207
|
+
* accepted while the Task was unplanned. A request with no origin — every one
|
|
208
|
+
* migrated from before the field existed — is deliberately held back so an
|
|
209
|
+
* upgrade cannot turn a historical pending request into a silent activation; it
|
|
210
|
+
* still adopts through the explicit `yui task activate` boundary, which does not
|
|
211
|
+
* consult origin. This replaces the old actor heuristic that both dropped legal
|
|
212
|
+
* Operator/user immediate requests and could not distinguish provenance.
|
|
213
|
+
*/
|
|
214
|
+
export function activationRequestIsControllerAdoptable(request) {
|
|
215
|
+
if (request.startMode === "after-planning-turn")
|
|
216
|
+
return true;
|
|
217
|
+
return request.origin !== undefined;
|
|
218
|
+
}
|
|
197
219
|
/**
|
|
198
220
|
* The durable terminal outcome for a requestId, read from the activation event
|
|
199
221
|
* ledger.
|
|
@@ -236,6 +258,10 @@ export function validateTaskActivationRequest(request) {
|
|
|
236
258
|
if (!["immediate", "after-planning-turn"].includes(request.startMode)) {
|
|
237
259
|
throw new Error(`Activation start mode is invalid: ${String(request.startMode)}.`);
|
|
238
260
|
}
|
|
261
|
+
if (request.origin !== undefined
|
|
262
|
+
&& !TASK_ACTIVATION_ORIGINS.includes(request.origin)) {
|
|
263
|
+
throw new Error(`Activation origin is invalid: ${String(request.origin)}.`);
|
|
264
|
+
}
|
|
239
265
|
if ((request.startMode === "after-planning-turn")
|
|
240
266
|
!== (request.afterPlanningRun !== undefined)) {
|
|
241
267
|
throw new Error("Activation deferral must name exactly the planning Turn it waits for.");
|
|
@@ -40,80 +40,96 @@ export function taskActivationOperationRef(taskId, requestId) {
|
|
|
40
40
|
* unchanged, so a retried tool call cannot produce a second activation.
|
|
41
41
|
*/
|
|
42
42
|
export function requestTaskActivation(store, input, now) {
|
|
43
|
-
return store.transaction((tx) =>
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
if (existing?.disposition === "pending") {
|
|
88
|
-
throw new Error(`Task already has a pending activation request: ${task.id}/${existing.operation.requestId}. `
|
|
89
|
-
+ "Cancel it before requesting different inputs.");
|
|
90
|
-
}
|
|
91
|
-
const request = createTaskActivationRequest(task.id, {
|
|
92
|
-
requestId: input.requestId,
|
|
93
|
-
actorId: input.actorId,
|
|
94
|
-
authorityRef: input.authorityRef,
|
|
43
|
+
return store.transaction((tx) => recordTaskActivationRequestInTransaction(tx, input, now));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The transactional core of {@link requestTaskActivation}, callable from inside
|
|
47
|
+
* a caller's own open transaction.
|
|
48
|
+
*
|
|
49
|
+
* The shared submission service (task-32 §2.3) records a develop route's
|
|
50
|
+
* activation request in the very transaction that saves the message, so the
|
|
51
|
+
* message and its activation intent commit together and no "submit then
|
|
52
|
+
* activate" two-command window exists. It reuses this exact logic rather than a
|
|
53
|
+
* second activation queue: the same idempotency, settled-id refusal and
|
|
54
|
+
* pending-collision rules apply whether the request arrives from the explicit
|
|
55
|
+
* boundary or from a develop submission. The caller owns transaction lifetime
|
|
56
|
+
* and any post-commit mailbox notification.
|
|
57
|
+
*/
|
|
58
|
+
export function recordTaskActivationRequestInTransaction(tx, input, now) {
|
|
59
|
+
const task = requireOpenDraft(tx, input.taskId);
|
|
60
|
+
const plan = canonicalEnvironmentPlan(input.environmentPlan);
|
|
61
|
+
const planningRun = resolveCallerPlanningRun(tx, task.id, input.callerRunId);
|
|
62
|
+
const startMode = planningRun === undefined
|
|
63
|
+
? "immediate"
|
|
64
|
+
: "after-planning-turn";
|
|
65
|
+
const existing = task.activationRequest;
|
|
66
|
+
// A requestId whose outcome was already decided is never re-opened, whether
|
|
67
|
+
// it still holds the slot or a later request displaced it. Cancelling A,
|
|
68
|
+
// requesting B and then retrying A must not resurrect A: the withdrawal is
|
|
69
|
+
// explicit authority, and adoption is exactly the effect it withdrew. An
|
|
70
|
+
// adopted id is equally final — its environment and status change happened.
|
|
71
|
+
// Only `failed` stays replayable, which is its documented contract.
|
|
72
|
+
//
|
|
73
|
+
// The authority is the durable activation event ledger, not the bounded
|
|
74
|
+
// display payload: the payload trims its oldest entries once it overflows,
|
|
75
|
+
// but the terminal events are never compacted, so an evicted cancellation is
|
|
76
|
+
// still refused with its original outcome instead of silently resurrecting.
|
|
77
|
+
const settled = settledActivationRequest(tx, task, input.requestId);
|
|
78
|
+
if (settled !== undefined) {
|
|
79
|
+
throw new Error(`Activation request ${input.requestId} was already ${settled.disposition} for ${task.id}`
|
|
80
|
+
+ `${settled.outcome === undefined ? "" : ` (${settled.outcome})`}. `
|
|
81
|
+
+ "Use a new requestId.");
|
|
82
|
+
}
|
|
83
|
+
if (existing?.operation.requestId === input.requestId) {
|
|
84
|
+
const digest = taskActivationInputDigest(task.id, {
|
|
95
85
|
startMode,
|
|
96
86
|
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
|
|
97
87
|
environmentPlan: plan
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
startMode: request.startMode,
|
|
103
|
-
environmentPlan: describeEnvironmentPlan(request.environmentPlan),
|
|
104
|
-
actor: request.operation.actorId,
|
|
105
|
-
...(request.afterPlanningRun === undefined
|
|
106
|
-
? {}
|
|
107
|
-
: { afterPlanningRun: request.afterPlanningRun })
|
|
108
|
-
}, now));
|
|
88
|
+
});
|
|
89
|
+
if (existing.operation.inputDigest !== digest) {
|
|
90
|
+
throw new Error(`Activation requestId ${input.requestId} was already used for different inputs.`);
|
|
91
|
+
}
|
|
109
92
|
return {
|
|
110
|
-
operationRef: taskActivationOperationRef(task.id,
|
|
111
|
-
request,
|
|
112
|
-
startMode,
|
|
113
|
-
created:
|
|
114
|
-
...(
|
|
93
|
+
operationRef: taskActivationOperationRef(task.id, existing.operation.requestId),
|
|
94
|
+
request: existing,
|
|
95
|
+
startMode: existing.startMode,
|
|
96
|
+
created: false,
|
|
97
|
+
...(existing.afterPlanningRun === undefined
|
|
98
|
+
? {}
|
|
99
|
+
: { afterPlanningRun: existing.afterPlanningRun })
|
|
115
100
|
};
|
|
116
|
-
}
|
|
101
|
+
}
|
|
102
|
+
if (existing?.disposition === "pending") {
|
|
103
|
+
throw new Error(`Task already has a pending activation request: ${task.id}/${existing.operation.requestId}. `
|
|
104
|
+
+ "Cancel it before requesting different inputs.");
|
|
105
|
+
}
|
|
106
|
+
const request = createTaskActivationRequest(task.id, {
|
|
107
|
+
requestId: input.requestId,
|
|
108
|
+
actorId: input.actorId,
|
|
109
|
+
authorityRef: input.authorityRef,
|
|
110
|
+
startMode,
|
|
111
|
+
...(input.origin === undefined ? {} : { origin: input.origin }),
|
|
112
|
+
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun }),
|
|
113
|
+
environmentPlan: plan
|
|
114
|
+
}, now);
|
|
115
|
+
tx.saveTask(setTaskActivationRequest(task, request, now));
|
|
116
|
+
tx.saveEvent(task.id, createTaskEvent(tx.nextEventId(task.id), task.id, TASK_ACTIVATION_EVENT.requested, {
|
|
117
|
+
requestId: request.operation.requestId,
|
|
118
|
+
startMode: request.startMode,
|
|
119
|
+
environmentPlan: describeEnvironmentPlan(request.environmentPlan),
|
|
120
|
+
actor: request.operation.actorId,
|
|
121
|
+
...(request.origin === undefined ? {} : { origin: request.origin }),
|
|
122
|
+
...(request.afterPlanningRun === undefined
|
|
123
|
+
? {}
|
|
124
|
+
: { afterPlanningRun: request.afterPlanningRun })
|
|
125
|
+
}, now));
|
|
126
|
+
return {
|
|
127
|
+
operationRef: taskActivationOperationRef(task.id, request.operation.requestId),
|
|
128
|
+
request,
|
|
129
|
+
startMode,
|
|
130
|
+
created: true,
|
|
131
|
+
...(planningRun === undefined ? {} : { afterPlanningRun: planningRun })
|
|
132
|
+
};
|
|
117
133
|
}
|
|
118
134
|
/** Explicit user or Operator cancellation. A cancelled request never adopts. */
|
|
119
135
|
export function cancelTaskActivation(store, taskId, requestId, reason, now) {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { SYSTEM_LEADER_ROLE } from "../role/systemRoles.js";
|
|
2
|
+
/**
|
|
3
|
+
* Requirement A phase and routing derivation (task-32 §2.1, §2.2).
|
|
4
|
+
*
|
|
5
|
+
* `Task.status` stays the only lifecycle statement. "Entered planning" is a
|
|
6
|
+
* derived fact, never a writable second state: this module reads it from the
|
|
7
|
+
* durable evidence a legitimate planning entry leaves behind, so idleness, a
|
|
8
|
+
* failed or replaced Session, a Controller restart, or a trimmed Context can
|
|
9
|
+
* never reset it to "not planned".
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The never-compacted event that records a Draft was routed into planning by an
|
|
13
|
+
* accepted user/operator submission, inside the same transaction that saved the
|
|
14
|
+
* message. It is one of the three independent derivation sources below, and the
|
|
15
|
+
* only one the shared submission service itself writes; the others are produced
|
|
16
|
+
* by the planning Run and its Session. Migration appends the same event for
|
|
17
|
+
* legitimate historical discussions so a post-upgrade develop cannot snatch a
|
|
18
|
+
* mid-discussion Draft into auto-activation.
|
|
19
|
+
*/
|
|
20
|
+
export const TASK_PLANNING_ENTERED_EVENT = "task.planning-entered";
|
|
21
|
+
/**
|
|
22
|
+
* Whether this Draft has already entered planning (task-32 §2.2).
|
|
23
|
+
*
|
|
24
|
+
* Derived — never a stored flag — from any of three independent, monotonic
|
|
25
|
+
* sources, each of which only ever comes into existence and never legitimately
|
|
26
|
+
* disappears:
|
|
27
|
+
*
|
|
28
|
+
* (a) a persisted planning-entered event, written in the message-save
|
|
29
|
+
* transaction the first time an accepted submission routed to planning;
|
|
30
|
+
* (b) a Leader planning AgentRun in history (the first-discuss Run, once the
|
|
31
|
+
* Controller has created it);
|
|
32
|
+
* (c) a Leader Session whose frozen launch carried planning authority.
|
|
33
|
+
*
|
|
34
|
+
* A role being configured, a non-empty body, or a non-empty Brief are
|
|
35
|
+
* deliberately NOT evidence: none of them proves the user's submission was ever
|
|
36
|
+
* accepted and routed. Only a Draft can be "in planning"; an active or terminal
|
|
37
|
+
* Task is past the question and answers false.
|
|
38
|
+
*/
|
|
39
|
+
export function draftHasEnteredPlanning(store, task) {
|
|
40
|
+
if (task.status !== "draft")
|
|
41
|
+
return false;
|
|
42
|
+
return hasPlanningEnteredEvent(store.listEvents(task.id))
|
|
43
|
+
|| hasLeaderPlanningRun(store, task.id)
|
|
44
|
+
|| hasLeaderPlanningSession(store, task.id);
|
|
45
|
+
}
|
|
46
|
+
function hasPlanningEnteredEvent(events) {
|
|
47
|
+
return events.some((event) => event.type === TASK_PLANNING_ENTERED_EVENT);
|
|
48
|
+
}
|
|
49
|
+
function hasLeaderPlanningRun(store, taskId) {
|
|
50
|
+
return store.listRuns(taskId).some((run) => run.roleName === SYSTEM_LEADER_ROLE && run.purpose === "planning");
|
|
51
|
+
}
|
|
52
|
+
function hasLeaderPlanningSession(store, taskId) {
|
|
53
|
+
const sessions = store.getTaskRoleSessionSet(taskId, SYSTEM_LEADER_ROLE);
|
|
54
|
+
if (sessions === null)
|
|
55
|
+
return false;
|
|
56
|
+
const candidates = [
|
|
57
|
+
...Object.values(sessions.sessions ?? {}),
|
|
58
|
+
...(sessions.history ?? [])
|
|
59
|
+
];
|
|
60
|
+
return candidates.some((session) => session.effective.executionAuthority === "planning");
|
|
61
|
+
}
|
|
62
|
+
export function draftActivationState(request) {
|
|
63
|
+
if (request === undefined)
|
|
64
|
+
return "none";
|
|
65
|
+
switch (request.disposition) {
|
|
66
|
+
case "pending": return "pending";
|
|
67
|
+
case "failed": return "failed";
|
|
68
|
+
case "adopted": return "adopted";
|
|
69
|
+
case "cancelled": return "none";
|
|
70
|
+
default: return "none";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Decide how a user/operator submission routes, given its normalized intent and
|
|
75
|
+
* the phase/activation facts read inside the save transaction.
|
|
76
|
+
*
|
|
77
|
+
* This is a pure decision over already-read facts, kept separate from the
|
|
78
|
+
* transaction that saves the message and enqueues work so it can be unit-tested
|
|
79
|
+
* exhaustively against the §2.1 table without a Store. The winner of a race is
|
|
80
|
+
* decided by which submission's transaction commits first: the second one reads
|
|
81
|
+
* the phase/activation the first one already wrote and routes accordingly.
|
|
82
|
+
*
|
|
83
|
+
* `executionEnabled` is the Task's own gate, read in the same transaction. It
|
|
84
|
+
* only ever gates a *fresh* develop activation: recording an activation request
|
|
85
|
+
* against a stopped Draft is refused at the activation boundary, so routing that
|
|
86
|
+
* case to `activate` would abort the whole transaction and lose the saved
|
|
87
|
+
* message. Planning and already-open activation obligations are unaffected — the
|
|
88
|
+
* gate is enforced later at dispatch, exactly as it is for a plain message.
|
|
89
|
+
*/
|
|
90
|
+
export function decideSubmissionRouting(input) {
|
|
91
|
+
// record never wakes anything, in any state (§2.1 first column).
|
|
92
|
+
if (input.intent === "record")
|
|
93
|
+
return { kind: "record" };
|
|
94
|
+
if (input.status === "active") {
|
|
95
|
+
// Neither discuss nor develop re-activates or downgrades an active Task.
|
|
96
|
+
return { kind: "active-context" };
|
|
97
|
+
}
|
|
98
|
+
// Only Draft remains: terminal statuses are refused before routing (§2.1 last
|
|
99
|
+
// row) by the caller's assertTaskOpen, so this function is never asked about
|
|
100
|
+
// them.
|
|
101
|
+
if (input.activation === "pending") {
|
|
102
|
+
return input.intent === "develop"
|
|
103
|
+
// A compatible pending request already exists; reference it, never a
|
|
104
|
+
// second request (§2.3 "重复 develop 不产生第二个兼容激活请求").
|
|
105
|
+
? { kind: "await-activation", state: "pending" }
|
|
106
|
+
// discuss becomes post-activation input; it does not start new planning.
|
|
107
|
+
: { kind: "await-activation", state: "pending" };
|
|
108
|
+
}
|
|
109
|
+
if (input.activation === "failed") {
|
|
110
|
+
// develop never auto-retries or downgrades to planning; discuss reports the
|
|
111
|
+
// original failure and the explicit retry/cancel next step. Both save only.
|
|
112
|
+
return { kind: "await-activation", state: "failed" };
|
|
113
|
+
}
|
|
114
|
+
if (input.intent === "develop") {
|
|
115
|
+
// develop on an already-planned Draft never auto-activates — it needs
|
|
116
|
+
// explicit manual activation. On an unplanned Draft with no open activation
|
|
117
|
+
// it records the request and routes to activation, unless execution is
|
|
118
|
+
// stopped: then the message is saved but no activation is recorded.
|
|
119
|
+
if (input.enteredPlanning)
|
|
120
|
+
return { kind: "planned-needs-manual-activation" };
|
|
121
|
+
return input.executionEnabled
|
|
122
|
+
? { kind: "activate", environmentPlan: input.developEnvironmentPlan }
|
|
123
|
+
: { kind: "activation-blocked-execution-stopped" };
|
|
124
|
+
}
|
|
125
|
+
// discuss (the default): first discussion records the planning entry; later
|
|
126
|
+
// discussion continues the existing planning conversation.
|
|
127
|
+
return input.enteredPlanning
|
|
128
|
+
? { kind: "continue-planning" }
|
|
129
|
+
: { kind: "enter-planning" };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Normalize a possibly-absent submission intent to its effective value.
|
|
133
|
+
*
|
|
134
|
+
* An absent intent is `discuss` (task-32 §2.5: "默认/旧客户端都按 discuss"), so
|
|
135
|
+
* every old client and every message saved before the field existed keeps its
|
|
136
|
+
* historical Leader-waking behaviour. Body text is never inspected.
|
|
137
|
+
*
|
|
138
|
+
* A legacy `--wake-policy none` carried the "save only, do not wake" meaning that
|
|
139
|
+
* `record` now names, so a caller that supplied only that older signal maps to
|
|
140
|
+
* `record` rather than silently becoming a Leader-waking `discuss`. An explicit
|
|
141
|
+
* intent always wins over the wake-policy shorthand.
|
|
142
|
+
*/
|
|
143
|
+
export function normalizeSubmissionIntent(intent, wakePolicy) {
|
|
144
|
+
if (intent !== undefined)
|
|
145
|
+
return intent;
|
|
146
|
+
if (wakePolicy === "none")
|
|
147
|
+
return "record";
|
|
148
|
+
return "discuss";
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Whether a retry aims at the same target the key was first bound to (§2.3). An
|
|
152
|
+
* absent prior target (a keyed Message from before receipts existed) never
|
|
153
|
+
* matches, so such a Message is treated as a conflict rather than replayed from
|
|
154
|
+
* a fabricated disposition.
|
|
155
|
+
*/
|
|
156
|
+
export function sameSubmissionTarget(prior, next) {
|
|
157
|
+
if (prior === undefined || prior.kind !== next.kind)
|
|
158
|
+
return false;
|
|
159
|
+
if (prior.kind === "task" && next.kind === "task")
|
|
160
|
+
return prior.taskId === next.taskId;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Derive the §2.5 feedback for one committed submission from its routing and the
|
|
165
|
+
* facts read in the save transaction.
|
|
166
|
+
*
|
|
167
|
+
* Pure over already-read facts, so it is unit-testable against the whole table
|
|
168
|
+
* without a Store. `enteredPlanning` and `activationState` are the values read
|
|
169
|
+
* before routing executed; the routing itself names what the transaction then
|
|
170
|
+
* did, and phase is derived to reflect the post-commit truth (a just-entered
|
|
171
|
+
* planning Draft reports `draft-planning`).
|
|
172
|
+
*/
|
|
173
|
+
export function describeSubmissionFeedback(input) {
|
|
174
|
+
const { routing } = input;
|
|
175
|
+
const phase = input.status === "active"
|
|
176
|
+
? "active"
|
|
177
|
+
: routing.kind === "enter-planning"
|
|
178
|
+
|| routing.kind === "continue-planning"
|
|
179
|
+
|| routing.kind === "planned-needs-manual-activation"
|
|
180
|
+
|| input.enteredPlanning
|
|
181
|
+
? "draft-planning"
|
|
182
|
+
: "draft-unplanned";
|
|
183
|
+
const planning = routing.kind === "enter-planning"
|
|
184
|
+
? "entered"
|
|
185
|
+
: routing.kind === "continue-planning"
|
|
186
|
+
? "continued"
|
|
187
|
+
: "none";
|
|
188
|
+
const activation = routing.kind === "activate"
|
|
189
|
+
? "requested"
|
|
190
|
+
: routing.kind === "planned-needs-manual-activation"
|
|
191
|
+
? "manual-required"
|
|
192
|
+
: routing.kind === "activation-blocked-execution-stopped"
|
|
193
|
+
? "execution-stopped"
|
|
194
|
+
: routing.kind === "await-activation"
|
|
195
|
+
? routing.state
|
|
196
|
+
// record and the planning routes report the existing obligation, if
|
|
197
|
+
// any, truthfully without acting on it.
|
|
198
|
+
: input.activationState === "pending"
|
|
199
|
+
? "pending"
|
|
200
|
+
: input.activationState === "failed"
|
|
201
|
+
? "failed"
|
|
202
|
+
: "none";
|
|
203
|
+
const delivery = routing.kind === "enter-planning"
|
|
204
|
+
|| routing.kind === "continue-planning"
|
|
205
|
+
|| routing.kind === "active-context"
|
|
206
|
+
? "queued"
|
|
207
|
+
: "none";
|
|
208
|
+
const nextStep = submissionNextStep(input);
|
|
209
|
+
return {
|
|
210
|
+
saved: { taskId: input.taskId, messageId: input.messageId },
|
|
211
|
+
phase,
|
|
212
|
+
planning,
|
|
213
|
+
activation,
|
|
214
|
+
delivery,
|
|
215
|
+
...(nextStep === undefined ? {} : { nextStep })
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function submissionNextStep(input) {
|
|
219
|
+
if (input.routing.kind === "planned-needs-manual-activation") {
|
|
220
|
+
return { kind: "activate-manually", taskId: input.taskId };
|
|
221
|
+
}
|
|
222
|
+
if (input.routing.kind === "activation-blocked-execution-stopped") {
|
|
223
|
+
return { kind: "start-execution", taskId: input.taskId };
|
|
224
|
+
}
|
|
225
|
+
if (input.routing.kind === "await-activation") {
|
|
226
|
+
if (input.routing.state === "failed") {
|
|
227
|
+
return {
|
|
228
|
+
kind: "resolve-failed-activation",
|
|
229
|
+
activationRef: input.activationRef ?? "",
|
|
230
|
+
failure: input.activationFailure ?? "activation failed"
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { kind: "await-pending-activation", activationRef: input.activationRef ?? "" };
|
|
234
|
+
}
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
@@ -128,8 +128,11 @@ function detailActions() {
|
|
|
128
128
|
answerInput: answerInput,
|
|
129
129
|
openTerminal: openTerminal,
|
|
130
130
|
inspect: inspectRecord,
|
|
131
|
-
sendMessage: (taskId, body, requestId) => submitMutation(taskId + "/messages",
|
|
132
|
-
"/api/tasks/" + encodeURIComponent(taskId) + "/messages",
|
|
131
|
+
sendMessage: (taskId, body, requestId, intent) => submitMutation(taskId + "/messages",
|
|
132
|
+
"/api/tasks/" + encodeURIComponent(taskId) + "/messages",
|
|
133
|
+
{ body, requestId, ...(intent === undefined ? {} : { intent }) }),
|
|
134
|
+
controlInput: (taskId, payload, requestId) => submitMutation(taskId + "/control/" + requestId,
|
|
135
|
+
"/api/tasks/" + encodeURIComponent(taskId) + "/control", payload),
|
|
133
136
|
updateTask: (taskId, patch, requestId) => submitMutation(taskId + "/metadata",
|
|
134
137
|
"/api/tasks/" + encodeURIComponent(taskId) + "/metadata", { patch, requestId }),
|
|
135
138
|
panels: (taskId) => requestJson("/api/tasks/" + encodeURIComponent(taskId) + "/panels",
|
|
@@ -658,9 +661,62 @@ elements.refresh.addEventListener("click", function () { refreshDashboard(); });
|
|
|
658
661
|
elements.operatorTerminal.addEventListener("click", function () {
|
|
659
662
|
openTerminal({ scope: "global", roleName: "operator" });
|
|
660
663
|
});
|
|
664
|
+
const globalDialog = document.querySelector("#global-input-dialog");
|
|
665
|
+
const globalForm = document.querySelector("#global-input-form");
|
|
666
|
+
const globalRole = document.querySelector("#global-input-role");
|
|
667
|
+
const globalAction = document.querySelector("#global-input-action");
|
|
668
|
+
const globalBody = document.querySelector("#global-input-body");
|
|
669
|
+
const globalTarget = document.querySelector("#global-input-target");
|
|
670
|
+
const globalThen = document.querySelector("#global-input-then");
|
|
671
|
+
const globalSubmit = document.querySelector("#global-input-submit");
|
|
672
|
+
const globalReceipt = document.querySelector("#global-input-receipt");
|
|
673
|
+
const globalState = document.querySelector("#global-input-state");
|
|
674
|
+
document.querySelector("#global-input-open").addEventListener("click", () => globalDialog.showModal());
|
|
675
|
+
document.querySelector("#global-input-close").addEventListener("click", () => globalDialog.close());
|
|
676
|
+
globalAction.addEventListener("change", () => {
|
|
677
|
+
document.querySelector("#global-input-body-label").hidden = globalAction.value === "interrupt";
|
|
678
|
+
document.querySelector("#global-input-target-label").hidden = globalAction.value === "queue";
|
|
679
|
+
document.querySelector("#global-input-then-label").hidden = globalAction.value !== "interrupt";
|
|
680
|
+
globalBody.required = globalAction.value !== "interrupt";
|
|
681
|
+
globalTarget.required = globalAction.value !== "queue";
|
|
682
|
+
});
|
|
683
|
+
document.querySelector("#global-input-inspect").addEventListener("click", async () => {
|
|
684
|
+
try {
|
|
685
|
+
const facts = await requestJson("/api/roles/" + encodeURIComponent(globalRole.value.trim()) + "/control");
|
|
686
|
+
globalState.textContent = JSON.stringify(facts, null, 2);
|
|
687
|
+
globalTarget.value = facts.turn?.nativeTurnId || facts.turn?.attemptId || "";
|
|
688
|
+
} catch (error) { globalState.textContent = error.message; }
|
|
689
|
+
});
|
|
690
|
+
globalForm.addEventListener("submit", async event => {
|
|
691
|
+
event.preventDefault();
|
|
692
|
+
if (globalSubmit.disabled) return;
|
|
693
|
+
globalSubmit.disabled = true;
|
|
694
|
+
const role = globalRole.value.trim();
|
|
695
|
+
const action = globalAction.value;
|
|
696
|
+
const requestId = crypto.randomUUID();
|
|
697
|
+
const input = { action, requestId };
|
|
698
|
+
if (action !== "interrupt") input.body = globalBody.value;
|
|
699
|
+
if (action !== "queue") input.expectedTarget = globalTarget.value.trim();
|
|
700
|
+
if (action === "interrupt" && globalThen.value.trim()) input.thenMessage = globalThen.value.trim();
|
|
701
|
+
globalReceipt.textContent = "Waiting for receipt · 等待回执 · " + requestId;
|
|
702
|
+
try {
|
|
703
|
+
const receipt = await submitMutation("global/" + role, "/api/roles/" + encodeURIComponent(role) + "/control", input);
|
|
704
|
+
const status = receipt.steer || receipt.interrupt || receipt.delivery || {};
|
|
705
|
+
globalReceipt.textContent = JSON.stringify(receipt);
|
|
706
|
+
const unknown = ["pending", "delivery-unknown", "steer-unknown", "interrupt-unknown"].includes(status.state)
|
|
707
|
+
|| status.code === "DELIVERY_UNKNOWN";
|
|
708
|
+
globalSubmit.disabled = unknown;
|
|
709
|
+
if (!unknown && !status.code) globalBody.value = "";
|
|
710
|
+
} catch (error) {
|
|
711
|
+
globalReceipt.textContent = error.disposition === "not-submitted" ? error.message
|
|
712
|
+
: "Outcome unknown; read state before acting. 结果未知,请先读取状态,不要重发。 · " + requestId;
|
|
713
|
+
globalSubmit.disabled = error.disposition !== "not-submitted";
|
|
714
|
+
}
|
|
715
|
+
});
|
|
661
716
|
elements.detailBack.addEventListener("click", clearSelection);
|
|
662
717
|
elements.terminalClose.addEventListener("click", closeTerminalPanel);
|
|
663
718
|
document.addEventListener("keydown", function (event) {
|
|
719
|
+
if (globalDialog.open) return;
|
|
664
720
|
const active = document.activeElement;
|
|
665
721
|
const typing = active && ["INPUT", "SELECT", "TEXTAREA"].includes(active.tagName);
|
|
666
722
|
if (event.key === "Escape" && terminalPanelOpen()) {
|