@zq-silk/yui 0.15.9 → 0.15.12
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 +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agent/launchEnvironment.js +7 -0
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +64 -16
- package/dist/cli/interactionPolicy.js +7 -3
- package/dist/cli/managedDiagnostics.js +1 -1
- 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 +381 -107
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +339 -4
- package/dist/commands/projectCommands.js +50 -22
- package/dist/commands/releaseCommands.js +18 -0
- package/dist/commands/taskActor.js +25 -0
- package/dist/commands/taskCommands.js +586 -96
- package/dist/commands/taskIntegrationCommands.js +19 -39
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +22 -11
- package/dist/commands/taskRoleRuntimeStatus.js +35 -0
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +55 -6
- package/dist/controller/agentHostObservation.js +155 -0
- package/dist/controller/clientRuntime.js +17 -2
- package/dist/controller/controller.js +14 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +519 -25
- package/dist/controller/globalInputDelivery.js +132 -0
- package/dist/controller/jobControl.js +6 -2
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/resourceInventory.js +14 -4
- package/dist/controller/resourceInventoryLinux.js +2 -6
- package/dist/controller/runtime.js +117 -7
- 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 +329 -386
- package/dist/integration/integrationAttempt.js +30 -4
- package/dist/integration/integrationQueueService.js +7 -7
- package/dist/integration/integrationSourceApplication.js +323 -0
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalInterrupt.js +33 -0
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/message/inputControlResolution.js +106 -0
- package/dist/message/message.js +367 -0
- package/dist/message/messageContinuation.js +126 -3
- package/dist/message/taskInterrupt.js +34 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/observability/orchestrationMetrics.js +1 -1
- package/dist/release/releaseHandover.js +22 -0
- package/dist/release/releaseWorkflowPorts.js +15 -7
- package/dist/repository/gitWorkspace.js +430 -107
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +182 -101
- package/dist/repository/taskWorkspacePreparer.js +205 -72
- package/dist/repository/workItemCandidateSnapshot.js +34 -0
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/resources/resourceDiscovery.js +3 -2
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +179 -82
- package/dist/runtime/agentHostCompatibility.js +127 -0
- package/dist/runtime/agentHostProtocol.js +53 -0
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/executionEnvironment.js +0 -19
- package/dist/runtime/launchBroker.js +6 -0
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionReconciliation.js +4 -4
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskRuntimeIsolation.js +30 -6
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/runtime/tmuxAdapters.js +5 -3
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/operatorEvent.js +4 -0
- package/dist/scheduler/taskExecutionProjection.js +38 -6
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/scheduler/wakeReason.js +7 -1
- package/dist/scheduler/wakeupQueue.js +2 -0
- package/dist/setup/setupCommand.js +26 -8
- package/dist/storage/homeLayout.js +130 -0
- package/dist/storage/migrations/collapseWorktreeLayout.js +963 -0
- package/dist/storage/migrations/integrationContinuation.js +104 -0
- package/dist/storage/migrations/unifyHomeLayout.js +925 -0
- package/dist/storage/sqliteSchema.js +167 -4
- package/dist/storage/sqliteStore.js +57 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +2 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +95 -2
- package/dist/task/archiveDiagnostics.js +129 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/nextAction.js +44 -11
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +147 -17
- package/dist/web/assets/client/components.js +56 -13
- package/dist/web/assets/client/i18n.js +78 -4
- package/dist/web/assets/client/taskSurface.js +108 -1
- package/dist/web/assets/client/view.js +39 -8
- package/dist/web/assets/shell.js +29 -0
- package/dist/web/assets/styles/layout.js +8 -1
- package/dist/web/assets/styles/widgets.js +12 -0
- package/dist/web/webServer.js +131 -4
- package/dist/web/webSnapshot.js +16 -6
- package/dist/web/webTaskSurface.js +222 -5
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +111 -35
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/release-workflow.md +39 -0
- package/docs/release-workflow.zh-CN.md +29 -0
- package/docs/sqlite-control-plane-design.md +223 -1
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +40 -0
- package/docs/testing/verification-levels.zh-CN.md +23 -0
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +154 -51
- package/skills/yui-leader/references/integration.md +52 -2
- package/skills/yui-operator/SKILL.md +19 -3
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +42 -0
- package/skills/yui-runtime/references/publication.md +42 -0
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -65,7 +65,9 @@ export class FileTaskRuntimeIsolation {
|
|
|
65
65
|
this.#resourceRegistrar().registerTaskRuntimeIsolation(descriptor);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
ensureDirectoryChain(this.#
|
|
68
|
+
ensureDirectoryChain(this.#controlPlane.managedRuntimeRoot === undefined
|
|
69
|
+
? this.#runtimeRoot
|
|
70
|
+
: this.#controlPlane.yuiHome, dirname(root));
|
|
69
71
|
mkdirSync(root, { mode: 0o700 });
|
|
70
72
|
try {
|
|
71
73
|
const marker = {
|
|
@@ -255,10 +257,22 @@ export function assertTaskRuntimeIsolationPreflight(input) {
|
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
const control = normalizeControlBoundary(input.controlPlane);
|
|
258
|
-
const protectedPaths = [control.yuiHome, ...control.globalInstallPaths];
|
|
259
260
|
for (const root of Object.values(descriptor.roots)) {
|
|
260
|
-
if (
|
|
261
|
-
throw new Error("Task runtime roots overlap
|
|
261
|
+
if (control.globalInstallPaths.some((path) => pathsOverlap(root, path))) {
|
|
262
|
+
throw new Error("Task runtime roots overlap a global install path.");
|
|
263
|
+
}
|
|
264
|
+
// Precise partition isolation: a runtime root may occupy Home only when it
|
|
265
|
+
// is fully within the single designated managed runtime partition. Every
|
|
266
|
+
// other part of Home — the database, managed Git workspaces, projects —
|
|
267
|
+
// stays protected. Both Task and integration runtimes now supply such a
|
|
268
|
+
// partition; a caller that supplies none keeps the whole-Home overlap ban.
|
|
269
|
+
if (pathsOverlap(root, control.yuiHome)) {
|
|
270
|
+
const partition = control.managedRuntimeRoot;
|
|
271
|
+
const withinPartition = partition !== undefined
|
|
272
|
+
&& (root === partition || isWithin(partition, root));
|
|
273
|
+
if (!withinPartition) {
|
|
274
|
+
throw new Error("Task runtime roots overlap the control YUI_HOME outside the managed runtime partition.");
|
|
275
|
+
}
|
|
262
276
|
}
|
|
263
277
|
if (pathsOverlap(root, descriptor.workspace.root)) {
|
|
264
278
|
throw new Error("Task runtime roots must not dirty the managed workspace.");
|
|
@@ -441,11 +455,21 @@ function taskRuntimeServiceNamespace(taskId, owner, runtimeRoot) {
|
|
|
441
455
|
])).slice(0, 24)}`;
|
|
442
456
|
}
|
|
443
457
|
function normalizeControlBoundary(value) {
|
|
458
|
+
const yuiHome = canonicalPath(value.yuiHome, "Control YUI_HOME");
|
|
459
|
+
const managedRuntimeRoot = value.managedRuntimeRoot === undefined
|
|
460
|
+
? undefined
|
|
461
|
+
: canonicalPath(value.managedRuntimeRoot, "Managed runtime root");
|
|
462
|
+
if (managedRuntimeRoot !== undefined
|
|
463
|
+
&& managedRuntimeRoot !== yuiHome
|
|
464
|
+
&& !isWithin(yuiHome, managedRuntimeRoot)) {
|
|
465
|
+
throw new Error("Managed runtime root must be within the control YUI_HOME.");
|
|
466
|
+
}
|
|
444
467
|
return Object.freeze({
|
|
445
|
-
yuiHome
|
|
468
|
+
yuiHome,
|
|
446
469
|
controllerSocketPath: canonicalPath(value.controllerSocketPath, "Controller socket path"),
|
|
447
470
|
tmuxNamespace: requireIdentity(value.tmuxNamespace, "Controller tmux namespace"),
|
|
448
|
-
globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path")))
|
|
471
|
+
globalInstallPaths: Object.freeze((value.globalInstallPaths ?? []).map((path) => canonicalPath(path, "Global install path"))),
|
|
472
|
+
...(managedRuntimeRoot === undefined ? {} : { managedRuntimeRoot })
|
|
449
473
|
});
|
|
450
474
|
}
|
|
451
475
|
function validateObservation(resource) {
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { builtinDriverIdForAdapter } from "./builtinAgentDrivers.js";
|
|
2
|
+
import { isRuntimeTokenEvidence, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
|
|
3
|
+
import { projectSessionTokenObservations, unobservedSessionTokenMetrics } from "./sessionTokenMetrics.js";
|
|
4
|
+
/**
|
|
5
|
+
* Pure lifetime projection. No transcript reads, collection, persistence,
|
|
6
|
+
* lifecycle decisions, guessed Run identities, or cumulative counter allocation.
|
|
7
|
+
*/
|
|
8
|
+
export function projectTaskUsageMetrics(input) {
|
|
9
|
+
const now = input.now ?? new Date();
|
|
10
|
+
const events = input.events.filter((event) => event.taskId === input.task.id);
|
|
11
|
+
const observations = events.flatMap((event) => {
|
|
12
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
13
|
+
return observation !== null
|
|
14
|
+
&& observation.fence.taskId === input.task.id
|
|
15
|
+
&& Date.parse(observation.receivedAt) <= now.getTime()
|
|
16
|
+
? [observation] : [];
|
|
17
|
+
}).sort(compare);
|
|
18
|
+
const runs = new Map(input.runs.filter((run) => run.taskId === input.task.id).map((run) => [run.id, run]));
|
|
19
|
+
const inScope = (observation) => {
|
|
20
|
+
if (input.workItemId === undefined)
|
|
21
|
+
return true;
|
|
22
|
+
const run = exactRun(observation, runs);
|
|
23
|
+
return run?.workItemId === input.workItemId;
|
|
24
|
+
};
|
|
25
|
+
const scoped = observations.filter(inScope);
|
|
26
|
+
const buckets = new Map();
|
|
27
|
+
for (const observation of observations) {
|
|
28
|
+
const identity = sessionIdentity(observation);
|
|
29
|
+
if (identity === null)
|
|
30
|
+
continue;
|
|
31
|
+
const key = identityKey(identity);
|
|
32
|
+
const bucket = buckets.get(key) ?? { identity, observations: [] };
|
|
33
|
+
bucket.observations.push(observation);
|
|
34
|
+
buckets.set(key, bucket);
|
|
35
|
+
}
|
|
36
|
+
const resourceOwners = new Map();
|
|
37
|
+
for (const { identity, observations: all } of buckets.values()) {
|
|
38
|
+
if (!all.some((observation) => !child(observation) && usageAuthority(observation)))
|
|
39
|
+
continue;
|
|
40
|
+
const resource = resourceKey(identity);
|
|
41
|
+
const owners = resourceOwners.get(resource) ?? new Set();
|
|
42
|
+
owners.add(identity.roleName);
|
|
43
|
+
resourceOwners.set(resource, owners);
|
|
44
|
+
}
|
|
45
|
+
const sessions = [...buckets.values()]
|
|
46
|
+
.sort((a, b) => identityKey(a.identity).localeCompare(identityKey(b.identity)))
|
|
47
|
+
.flatMap(({ identity, observations: all }) => {
|
|
48
|
+
if (!all.some(inScope))
|
|
49
|
+
return [];
|
|
50
|
+
const main = all.filter((observation) => !child(observation) && usageAuthority(observation));
|
|
51
|
+
const evidence = main.filter(isRuntimeTokenEvidence);
|
|
52
|
+
const metrics = projectSessionTokenObservations(main, identity);
|
|
53
|
+
let displayedMetrics = input.workItemId === undefined ? metrics : unobservedSessionTokenMetrics(identity);
|
|
54
|
+
const reasons = [];
|
|
55
|
+
if (all.some(child))
|
|
56
|
+
reasons.push("child-usage-overlap-unproven");
|
|
57
|
+
if (evidence.some(({ payload }) => payload.observationQuality === "partial"
|
|
58
|
+
|| payload.observationQuality === "unavailable"))
|
|
59
|
+
reasons.push("source-coverage-partial");
|
|
60
|
+
let tokens = attributableTokens(evidence, metrics);
|
|
61
|
+
if (input.workItemId !== undefined) {
|
|
62
|
+
// Establish semantics and request ownership before filtering. Filtering
|
|
63
|
+
// first could hide a mixed stream or allocate a revised request twice.
|
|
64
|
+
if (metrics.cumulativeTotal.status !== "observed") {
|
|
65
|
+
tokens = metric(null, ["session-usage-unavailable"]);
|
|
66
|
+
}
|
|
67
|
+
else if (evidence.some(({ payload }) => payload.usage?.semantics === "cumulative-session")) {
|
|
68
|
+
tokens = metric(null, ["cumulative-not-allocatable-to-work-item"]);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
const requests = new Map();
|
|
72
|
+
for (const observation of evidence) {
|
|
73
|
+
if (observation.payload.usage?.semantics !== "request-context")
|
|
74
|
+
continue;
|
|
75
|
+
const key = observation.payload.activityId ?? observation.semanticKey;
|
|
76
|
+
requests.set(key, [...(requests.get(key) ?? []), observation]);
|
|
77
|
+
}
|
|
78
|
+
const selected = [];
|
|
79
|
+
for (const versions of requests.values()) {
|
|
80
|
+
if (!versions.some(inScope))
|
|
81
|
+
continue;
|
|
82
|
+
const owners = new Set(versions.map((observation) => exactRun(observation, runs)?.id));
|
|
83
|
+
if (owners.size !== 1 || owners.has(undefined) || !versions.every(inScope)) {
|
|
84
|
+
reasons.push("request-run-attribution-conflict");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
selected.push(...versions);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
displayedMetrics = projectSessionTokenObservations(selected, identity);
|
|
91
|
+
const subtotal = displayedMetrics.cumulativeTotal;
|
|
92
|
+
tokens = metric(subtotal.status === "observed" ? subtotal.totalTokens : null, subtotal.status === "observed" ? [] : ["no-attributable-request-usage"]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if ((resourceOwners.get(resourceKey(identity))?.size ?? 0) > 1) {
|
|
96
|
+
tokens = metric(null, ["native-session-role-overlap"]);
|
|
97
|
+
displayedMetrics = unobservedSessionTokenMetrics(identity);
|
|
98
|
+
}
|
|
99
|
+
tokens = metric(tokens.value, [...tokens.reasons, ...reasons]);
|
|
100
|
+
return [{
|
|
101
|
+
identity,
|
|
102
|
+
semantics: [...new Set(evidence.flatMap(({ payload }) => payload.usage === undefined ? [] : [payload.usage.semantics]))].sort(),
|
|
103
|
+
sources: [...new Set(evidence.map(({ authority, payload }) => payload.observerSource?.sourceId ?? authority))].sort(),
|
|
104
|
+
observedThrough: all.filter(inScope).at(-1).receivedAt,
|
|
105
|
+
tokens,
|
|
106
|
+
metrics: displayedMetrics
|
|
107
|
+
}];
|
|
108
|
+
});
|
|
109
|
+
const reasons = [];
|
|
110
|
+
if (scoped.some((observation) => sessionIdentity(observation) === null))
|
|
111
|
+
reasons.push("session-identity-missing");
|
|
112
|
+
for (const run of runs.values()) {
|
|
113
|
+
if (input.workItemId !== undefined && run.workItemId !== input.workItemId)
|
|
114
|
+
continue;
|
|
115
|
+
if (!observations.some((observation) => exactRun(observation, runs)?.id === run.id
|
|
116
|
+
&& usageAuthority(observation) && isRuntimeTokenEvidence(observation)))
|
|
117
|
+
reasons.push("run-usage-unobserved");
|
|
118
|
+
}
|
|
119
|
+
const tokenValues = sessions.flatMap(({ tokens }) => tokens.value === null ? [] : [tokens.value]);
|
|
120
|
+
const sum = safeSum(tokenValues);
|
|
121
|
+
const tokens = metric(tokenValues.length === 0 ? null : sum, [...reasons, ...sessions.flatMap(({ tokens }) => tokens.reasons),
|
|
122
|
+
...(sum === null ? ["counter-overflow"] : []),
|
|
123
|
+
...(sessions.length === 0 ? ["no-token-evidence"] : [])]);
|
|
124
|
+
const operations = new Map();
|
|
125
|
+
for (const observation of observations) {
|
|
126
|
+
const identity = sessionIdentity(observation);
|
|
127
|
+
const { nativeTurnId } = observation.fence;
|
|
128
|
+
if (!usageAuthority(observation) || child(observation) || identity === null || nativeTurnId === undefined
|
|
129
|
+
|| !observation.kind.startsWith("operation.")
|
|
130
|
+
|| observation.payload.operation !== "tool" || observation.payload.operationId === undefined)
|
|
131
|
+
continue;
|
|
132
|
+
const key = JSON.stringify([resourceKey(identity), nativeTurnId, observation.payload.operationId]);
|
|
133
|
+
const versions = operations.get(key) ?? [];
|
|
134
|
+
versions.push(observation);
|
|
135
|
+
operations.set(key, versions);
|
|
136
|
+
}
|
|
137
|
+
const toolCount = [...operations.values()].filter((versions) => {
|
|
138
|
+
if (input.workItemId === undefined)
|
|
139
|
+
return true;
|
|
140
|
+
const owners = new Set(versions.map((observation) => exactRun(observation, runs)?.id));
|
|
141
|
+
return owners.size === 1 && !owners.has(undefined) && versions.every(inScope);
|
|
142
|
+
}).length;
|
|
143
|
+
return {
|
|
144
|
+
scope: "task-lifetime", taskId: input.task.id,
|
|
145
|
+
...(input.workItemId === undefined ? {} : { workItemId: input.workItemId }),
|
|
146
|
+
observedThrough: scoped.at(-1)?.receivedAt ?? null,
|
|
147
|
+
tokens,
|
|
148
|
+
toolCalls: metric(toolCount === 0 ? null : toolCount, ["tool-history-compacted"]),
|
|
149
|
+
elapsedSeconds: input.workItemId === undefined
|
|
150
|
+
? taskElapsed(input.task, events, now) : metric(null, ["work-item-lifecycle-not-task-elapsed"]),
|
|
151
|
+
executionSeconds: executionDuration(scoped, now),
|
|
152
|
+
coverage: {
|
|
153
|
+
basis: "task-fenced-observations",
|
|
154
|
+
observedSessions: sessions.length,
|
|
155
|
+
tokenSessions: tokenValues.length,
|
|
156
|
+
completeness: "observed-sources-only"
|
|
157
|
+
},
|
|
158
|
+
sessions
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function attributableTokens(evidence, metrics) {
|
|
162
|
+
const total = metrics.cumulativeTotal;
|
|
163
|
+
if (total.status !== "observed")
|
|
164
|
+
return metric(null, ["session-usage-unavailable"]);
|
|
165
|
+
const cumulative = evidence.filter(({ payload }) => payload.usage?.semantics === "cumulative-session");
|
|
166
|
+
if (cumulative.length === 0)
|
|
167
|
+
return metric(total.totalTokens);
|
|
168
|
+
const first = cumulative[0].payload.usage;
|
|
169
|
+
const baseline = first.inputTokens + first.outputTokens;
|
|
170
|
+
if (baseline === 0)
|
|
171
|
+
return metric(total.totalTokens);
|
|
172
|
+
// session.started also occurs on resume: it cannot prove a fresh counter.
|
|
173
|
+
// A nonzero first counter may predate this Task, so exclude it. A single
|
|
174
|
+
// snapshot does not prove even a zero delta.
|
|
175
|
+
return cumulative.length < 2
|
|
176
|
+
? metric(null, ["nonzero-session-baseline"])
|
|
177
|
+
: metric(total.totalTokens - baseline, ["nonzero-session-baseline"]);
|
|
178
|
+
}
|
|
179
|
+
function exactRun(observation, runs) {
|
|
180
|
+
const { fence } = observation;
|
|
181
|
+
const run = fence.runId === undefined ? undefined : runs.get(fence.runId);
|
|
182
|
+
if (run === undefined || fence.nativeSessionId === undefined || run.roleName !== fence.roleName
|
|
183
|
+
|| run.effective.agentId !== fence.agentId)
|
|
184
|
+
return undefined;
|
|
185
|
+
try {
|
|
186
|
+
return builtinDriverIdForAdapter(run.effective.adapterId) === fence.driverId ? run : undefined;
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function sessionIdentity({ fence }) {
|
|
193
|
+
return fence.taskId === undefined || fence.nativeSessionId === undefined ? null : {
|
|
194
|
+
taskId: fence.taskId, roleName: fence.roleName, agentId: fence.agentId,
|
|
195
|
+
driverId: fence.driverId, nativeSessionId: fence.nativeSessionId
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function identityKey(identity) {
|
|
199
|
+
return JSON.stringify([identity.taskId, identity.roleName, identity.agentId, identity.driverId, identity.nativeSessionId]);
|
|
200
|
+
}
|
|
201
|
+
function resourceKey(identity) {
|
|
202
|
+
return JSON.stringify([identity.agentId, identity.driverId, identity.nativeSessionId]);
|
|
203
|
+
}
|
|
204
|
+
function child({ fence }) {
|
|
205
|
+
return fence.continuationId !== undefined || fence.parentContinuationId !== undefined;
|
|
206
|
+
}
|
|
207
|
+
function usageAuthority(observation) {
|
|
208
|
+
return observation.authority === "provider-structured" || observation.authority === "driver-inferred";
|
|
209
|
+
}
|
|
210
|
+
function compare(left, right) {
|
|
211
|
+
return left.receivedAt.localeCompare(right.receivedAt)
|
|
212
|
+
|| (left.sequence ?? -1) - (right.sequence ?? -1)
|
|
213
|
+
|| (left.ordinal ?? -1) - (right.ordinal ?? -1)
|
|
214
|
+
|| left.eventId.localeCompare(right.eventId);
|
|
215
|
+
}
|
|
216
|
+
function metric(value, reasons = []) {
|
|
217
|
+
return {
|
|
218
|
+
value, status: value === null ? "unknown" : reasons.length === 0 ? "known" : "partial",
|
|
219
|
+
reasons: [...new Set(reasons)].sort()
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function safeSum(values) {
|
|
223
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
224
|
+
return Number.isSafeInteger(sum) && sum >= 0 ? sum : null;
|
|
225
|
+
}
|
|
226
|
+
function taskElapsed(task, events, now) {
|
|
227
|
+
const terminal = task.status === "completed"
|
|
228
|
+
|| task.status === "cancelled" || task.status === "archived";
|
|
229
|
+
const cancelledAt = events.filter((event) => event.type === "task.cancelled")
|
|
230
|
+
.map(({ createdAt }) => createdAt).sort().at(-1);
|
|
231
|
+
const end = terminal ? task.completedAt ?? task.retiredAt ?? cancelledAt : now.toISOString();
|
|
232
|
+
const elapsed = end === undefined ? NaN : (Date.parse(end) - Date.parse(task.createdAt)) / 1000;
|
|
233
|
+
return Number.isFinite(elapsed) && elapsed >= 0
|
|
234
|
+
? metric(elapsed) : metric(null, ["task-lifecycle-time-missing"]);
|
|
235
|
+
}
|
|
236
|
+
function executionDuration(observations, now) {
|
|
237
|
+
const turns = new Map();
|
|
238
|
+
for (const observation of observations) {
|
|
239
|
+
const identity = sessionIdentity(observation);
|
|
240
|
+
const { nativeTurnId } = observation.fence;
|
|
241
|
+
if (identity === null || nativeTurnId === undefined || child(observation) || !usageAuthority(observation))
|
|
242
|
+
continue;
|
|
243
|
+
const resource = resourceKey(identity);
|
|
244
|
+
const key = JSON.stringify([resource, nativeTurnId]);
|
|
245
|
+
const turn = turns.get(key) ?? { resource };
|
|
246
|
+
const time = Date.parse(observation.observedAt ?? observation.receivedAt);
|
|
247
|
+
if (observation.kind === "turn.accepted")
|
|
248
|
+
turn.start = Math.min(turn.start ?? time, time);
|
|
249
|
+
if (["turn.completed", "turn.failed", "turn.cancelled"].includes(observation.kind)) {
|
|
250
|
+
turn.end = Math.max(turn.end ?? time, time);
|
|
251
|
+
}
|
|
252
|
+
turns.set(key, turn);
|
|
253
|
+
}
|
|
254
|
+
const intervals = new Map();
|
|
255
|
+
for (const turn of turns.values()) {
|
|
256
|
+
if (turn.start === undefined || turn.end === undefined || turn.end < turn.start || turn.end > now.getTime())
|
|
257
|
+
continue;
|
|
258
|
+
intervals.set(turn.resource, [...(intervals.get(turn.resource) ?? []), [turn.start, turn.end]]);
|
|
259
|
+
}
|
|
260
|
+
let milliseconds = 0;
|
|
261
|
+
for (const ranges of intervals.values()) {
|
|
262
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
263
|
+
let end = -Infinity;
|
|
264
|
+
for (const range of ranges) {
|
|
265
|
+
milliseconds += Math.max(0, range[1] - Math.max(end, range[0]));
|
|
266
|
+
end = Math.max(end, range[1]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return metric(intervals.size === 0 ? null : milliseconds / 1000, ["native-turn-history-compacted"]);
|
|
270
|
+
}
|
|
271
|
+
/** CLI/audit formatting shares the same metric contract; Web localizes labels. */
|
|
272
|
+
export function formatUsageMetric(metric, suffix = "") {
|
|
273
|
+
return metric.value === null ? `unknown (${metric.reasons.join(", ")})`
|
|
274
|
+
: `${metric.value}${suffix}${metric.status === "partial" ? ` (partial: ${metric.reasons.join(", ")})` : ""}`;
|
|
275
|
+
}
|
|
@@ -209,7 +209,8 @@ export class TmuxSessionHost {
|
|
|
209
209
|
throw toRuntimeLaunchFailure(error, "validation", launchContext);
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
const interactiveCodex = request.owner.scope === "global" && request.adapterId === "codex"
|
|
212
|
+
const interactiveCodex = request.owner.scope === "global" && request.adapterId === "codex"
|
|
213
|
+
&& planned.launch.providerControl === undefined;
|
|
213
214
|
let reuseInteractivePane = false;
|
|
214
215
|
if (interactiveCodex) {
|
|
215
216
|
let status;
|
|
@@ -310,6 +311,7 @@ export class TmuxSessionHost {
|
|
|
310
311
|
}
|
|
311
312
|
const reservation = broker.reserve(Object.freeze({
|
|
312
313
|
schemaVersion: 2,
|
|
314
|
+
...(request.runId === undefined ? {} : { startupRunId: request.runId }),
|
|
313
315
|
command: planned.launch.command,
|
|
314
316
|
args: [...planned.launch.args],
|
|
315
317
|
environment: { ...planned.launch.env },
|
|
@@ -577,8 +579,8 @@ export class AgentHostPromptPushAdapter {
|
|
|
577
579
|
try {
|
|
578
580
|
const result = await sendAgentHostSteerControl({
|
|
579
581
|
home: this.home,
|
|
580
|
-
scope:
|
|
581
|
-
taskId: request.owner.taskId,
|
|
582
|
+
scope: request.owner.scope,
|
|
583
|
+
...(request.owner.scope === "task" ? { taskId: request.owner.taskId } : {}),
|
|
582
584
|
roleName: request.owner.roleName,
|
|
583
585
|
control: {
|
|
584
586
|
protocol: AGENT_HOST_CONTROL_PROTOCOL,
|
|
@@ -51,6 +51,18 @@ async function deliverActiveRun(store, delivery, task, role, run, now) {
|
|
|
51
51
|
});
|
|
52
52
|
const sessionSet = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
|
|
53
53
|
const binding = sessionSet?.providerBinding ?? null;
|
|
54
|
+
if (binding?.retry !== undefined && (binding.retry.successorRunId === run.id
|
|
55
|
+
|| binding.retry.successorReviewRoundId !== undefined && binding.retry.successorReviewRoundId === run.reviewRoundId)
|
|
56
|
+
&& ["cancelled", "exhausted", "needs-attention"].includes(binding.retry.status)
|
|
57
|
+
&& binding.run?.runId !== run.id) {
|
|
58
|
+
return failRunDelivery(store, run, now, "runtime-failed", `Scheduled Provider retry ${binding.retry.status}: ${binding.retry.reason ?? "not admitted"}`);
|
|
59
|
+
}
|
|
60
|
+
if (binding?.retry?.status === "waiting"
|
|
61
|
+
&& (binding.retry.successorRunId === run.id || binding.retry.successorReviewRoundId !== undefined
|
|
62
|
+
&& binding.retry.successorReviewRoundId === run.reviewRoundId)
|
|
63
|
+
&& now.getTime() < Date.parse(binding.retry.nextEligibleAt)) {
|
|
64
|
+
return { ...base, status: "skipped", reason: "not-ready" };
|
|
65
|
+
}
|
|
54
66
|
const observedRun = binding?.run ?? null;
|
|
55
67
|
const initialAttemptId = formatRunReceiptId(task.id, run.id);
|
|
56
68
|
const currentProviderTurn = managedProviderTurnId(observedRun) === run.id ? observedRun : null;
|
|
@@ -3,6 +3,7 @@ import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
|
3
3
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
4
4
|
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
5
5
|
import { hasImmediateWakeReason } from "./wakeReason.js";
|
|
6
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
6
7
|
export const LEADER_WAKE_AGGREGATION_MS = 60_000;
|
|
7
8
|
/** Deliver a durable mailbox notification, not an implicit execution assignment.
|
|
8
9
|
* Acceptance settles only its claimed batch; no Agent report is required.
|
|
@@ -34,6 +35,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
34
35
|
}
|
|
35
36
|
const sessions = store.getTaskRoleSessionSet?.(task.id, role.name) ?? null;
|
|
36
37
|
const provider = sessions?.providerBinding;
|
|
38
|
+
if (providerRetryPending(provider)) {
|
|
39
|
+
results.push({ ...base, status: "skipped", reason: "not-ready" });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
37
42
|
if (provider?.authority.owner === "human" || provider?.authority.owner === "unknown") {
|
|
38
43
|
results.push({ ...base, status: "skipped", reason: "busy" });
|
|
39
44
|
continue;
|
|
@@ -4,6 +4,8 @@ import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
|
4
4
|
export const LEADER_ATTENTION_REQUIRED_EVENT = "leader.attention-required";
|
|
5
5
|
/** The sole Task-event boundary into the global Operator mailbox. */
|
|
6
6
|
export function enqueueOperatorEvent(store, event, reason, now) {
|
|
7
|
+
if (store.getTask?.(event.taskId)?.status === "archived")
|
|
8
|
+
return;
|
|
7
9
|
enqueueWork(store, { kind: "operator" }, reason, now, [
|
|
8
10
|
{ type: "event", taskId: event.taskId, id: event.id }
|
|
9
11
|
], {
|
|
@@ -13,6 +15,8 @@ export function enqueueOperatorEvent(store, event, reason, now) {
|
|
|
13
15
|
}
|
|
14
16
|
/** Routes one Role fact to its supervisor without exposing Provider details to Operator code. */
|
|
15
17
|
export function routeRoleEvent(store, event, roleName, reason, now) {
|
|
18
|
+
if (store.getTask?.(event.taskId)?.status === "archived")
|
|
19
|
+
return;
|
|
16
20
|
if (roleName === "leader") {
|
|
17
21
|
enqueueOperatorEvent(store, event, reason, now);
|
|
18
22
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
2
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
3
|
+
import { providerRetryProjection } from "../runtime/providerRetry.js";
|
|
3
4
|
import { taskOwnsManagedWorkspace } from "../task/task.js";
|
|
4
5
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
5
6
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
@@ -19,7 +20,8 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride, now =
|
|
|
19
20
|
return null;
|
|
20
21
|
const roles = store.listRoles?.(taskId) ?? [];
|
|
21
22
|
const events = store.listEvents?.(taskId) ?? [];
|
|
22
|
-
const
|
|
23
|
+
const usageRuns = store.listRuns?.(taskId) ?? [];
|
|
24
|
+
const runs = operationalTaskRecords(usageRuns, events, "run");
|
|
23
25
|
const leaderMailbox = store.getWorkMailbox?.({
|
|
24
26
|
kind: "role",
|
|
25
27
|
taskId,
|
|
@@ -35,6 +37,11 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride, now =
|
|
|
35
37
|
task,
|
|
36
38
|
roles,
|
|
37
39
|
runs,
|
|
40
|
+
usageRuns,
|
|
41
|
+
providerRetries: roles.flatMap(role => {
|
|
42
|
+
const retry = providerRetryProjection(store.getTaskRoleSessionSet?.(taskId, role.name)?.providerBinding);
|
|
43
|
+
return retry === null ? [] : [retry];
|
|
44
|
+
}),
|
|
38
45
|
runDelivery: Object.fromEntries(runs.map((run) => {
|
|
39
46
|
const observed = store.getTaskRoleSessionSet?.(taskId, run.roleName)?.providerBinding?.run;
|
|
40
47
|
return [run.id, observed?.runId === run.id ? observed.status : "unobserved"];
|
|
@@ -79,6 +86,7 @@ export function projectTaskExecutionFromFacts(facts) {
|
|
|
79
86
|
return projectTaskExecution({
|
|
80
87
|
...facts,
|
|
81
88
|
runs,
|
|
89
|
+
usageRuns: facts.usageRuns ?? facts.runs,
|
|
82
90
|
executionGroups
|
|
83
91
|
});
|
|
84
92
|
}
|
|
@@ -107,9 +115,10 @@ export function projectTaskExecution(facts) {
|
|
|
107
115
|
});
|
|
108
116
|
});
|
|
109
117
|
const observability = buildTaskObservabilityProjection({
|
|
118
|
+
task,
|
|
110
119
|
workItems,
|
|
111
120
|
executionGroups: observabilityGroups,
|
|
112
|
-
runs,
|
|
121
|
+
runs: facts.usageRuns ?? runs,
|
|
113
122
|
events,
|
|
114
123
|
contextSnapshots: facts.contextSnapshots,
|
|
115
124
|
sessionTokens,
|
|
@@ -206,13 +215,19 @@ export function projectTaskExecution(facts) {
|
|
|
206
215
|
const hasPendingLeaderWork = pendingWakeup !== null
|
|
207
216
|
|| leaderMailbox?.pending !== null;
|
|
208
217
|
const recoveryPending = isRecoveryPending(pendingWakeup, leaderMailbox, leaderFailure);
|
|
209
|
-
const
|
|
210
|
-
|
|
218
|
+
const providerRecoveries = (facts.providerRetries ?? []).filter(retry => retry.status === "waiting" || retry.status === "in-flight");
|
|
219
|
+
const recoveringRunIds = new Set(providerRecoveries.flatMap(retry => [retry.previousRunId, retry.successorRunId].filter((id) => id !== undefined)));
|
|
220
|
+
const failedWork = workItems.some((item) => {
|
|
221
|
+
const last = runs.filter((run) => run.workItemId === item.id).at(-1);
|
|
222
|
+
return item.status === "open" && last?.status === "failed" && !recoveringRunIds.has(last.id);
|
|
223
|
+
});
|
|
211
224
|
const candidateReady = workItems.some((item) => (item.status === "open" && item.currentCandidateId !== undefined));
|
|
212
225
|
const blockedIntegration = integrations.some((attempt) => attempt.status === "blocked");
|
|
226
|
+
const conflictedIntegration = integrations.find((attempt) => attempt.status === "conflicted");
|
|
213
227
|
const unresolvedIntegration = integrations.some((attempt) => (attempt.status === "running"
|
|
214
228
|
|| attempt.status === "validating"
|
|
215
|
-
|| attempt.status === "blocked"
|
|
229
|
+
|| attempt.status === "blocked"
|
|
230
|
+
|| attempt.status === "conflicted"));
|
|
216
231
|
const hasLeaderMismatch = attention.some((item) => item.kind === "identity-mismatch");
|
|
217
232
|
const renderAttention = () => {
|
|
218
233
|
const first = attention[0];
|
|
@@ -256,7 +271,7 @@ export function projectTaskExecution(facts) {
|
|
|
256
271
|
pendingWakeup
|
|
257
272
|
});
|
|
258
273
|
}
|
|
259
|
-
if (laneRecovery !== undefined) {
|
|
274
|
+
if (laneRecovery !== undefined && (laneRecovery.runId === undefined || !recoveringRunIds.has(laneRecovery.runId))) {
|
|
260
275
|
return render({
|
|
261
276
|
task,
|
|
262
277
|
status: laneRecovery.runtimeHealth === "confirmed-dead"
|
|
@@ -279,6 +294,15 @@ export function projectTaskExecution(facts) {
|
|
|
279
294
|
if (attention.length > 0) {
|
|
280
295
|
return renderAttention();
|
|
281
296
|
}
|
|
297
|
+
if (conflictedIntegration !== undefined) {
|
|
298
|
+
return render({
|
|
299
|
+
task, status: "needs-leader-action", owner: "leader",
|
|
300
|
+
action: "resolve-integration-conflicts",
|
|
301
|
+
summary: `Integration ${conflictedIntegration.id} has Git conflicts; resolve its workspace and continue.`,
|
|
302
|
+
reason: "integration-conflicted", monitoring, failClosed: false,
|
|
303
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
304
|
+
});
|
|
305
|
+
}
|
|
282
306
|
if (blockedIntegration || failedWork || hasLeaderMismatch) {
|
|
283
307
|
return render({
|
|
284
308
|
task,
|
|
@@ -295,6 +319,14 @@ export function projectTaskExecution(facts) {
|
|
|
295
319
|
pendingWakeup
|
|
296
320
|
});
|
|
297
321
|
}
|
|
322
|
+
if (providerRecoveries.length > 0 && healthyActiveRuns.length === 0) {
|
|
323
|
+
return render({
|
|
324
|
+
task, status: "recovering", owner: "none", action: "none",
|
|
325
|
+
summary: `${providerRecoveries.length} Provider recovery chain(s) are cooling down or awaiting their exact result; existing work is preserved. Do not dispatch a duplicate.`,
|
|
326
|
+
reason: "provider-recovery", monitoring, failClosed: false,
|
|
327
|
+
activeRuns: activeRunViews, attention, blockers, pendingWakeup
|
|
328
|
+
});
|
|
329
|
+
}
|
|
298
330
|
if (recoveryPending) {
|
|
299
331
|
return render({
|
|
300
332
|
task,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
|
|
1
2
|
/**
|
|
2
3
|
* Build the read-only DAG, execution, cost, and context view consumed by CLI
|
|
3
4
|
* and Web. It deliberately derives every value from existing Task records and
|
|
@@ -8,7 +9,7 @@ export function buildTaskObservabilityProjection(input) {
|
|
|
8
9
|
const dag = projectDag(input.workItems);
|
|
9
10
|
const workItems = input.workItems.map((item) => {
|
|
10
11
|
const groups = item.executionGroups;
|
|
11
|
-
const itemCost =
|
|
12
|
+
const itemCost = projectCost({ ...input, now }, groups, item.id);
|
|
12
13
|
const itemContext = projectContext(groups, input.runs, input.contextSnapshots);
|
|
13
14
|
const producerObservability = projectWorkItemProducerObservability(item, input.runs);
|
|
14
15
|
return Object.freeze({
|
|
@@ -21,7 +22,7 @@ export function buildTaskObservabilityProjection(input) {
|
|
|
21
22
|
resultCount: producerObservability?.resultCount ?? null
|
|
22
23
|
});
|
|
23
24
|
});
|
|
24
|
-
const cost = projectCost(input
|
|
25
|
+
const cost = projectCost({ ...input, now }, input.executionGroups);
|
|
25
26
|
const context = projectContext(input.executionGroups, input.runs, input.contextSnapshots);
|
|
26
27
|
return Object.freeze({
|
|
27
28
|
dag,
|
|
@@ -140,17 +141,13 @@ function dependencyEdgeStatus(id, byId) {
|
|
|
140
141
|
return "active";
|
|
141
142
|
return "active";
|
|
142
143
|
}
|
|
143
|
-
function projectCost(
|
|
144
|
+
function projectCost(input, groups, workItemId) {
|
|
144
145
|
const uniqueGroups = [...new Map(groups.map((group) => [group.id, group])).values()];
|
|
145
146
|
const groupIds = new Set(uniqueGroups.map(({ id }) => id));
|
|
146
|
-
const attempts = runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
|
|
147
|
+
const attempts = input.runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
|
|
147
148
|
const laneCount = uniqueGroups.reduce((total, group) => total + group.lanes.length, 0);
|
|
148
149
|
return Object.freeze({
|
|
149
|
-
|
|
150
|
-
toolCalls: 0,
|
|
151
|
-
wallClockSeconds: uniqueGroups.reduce((total, group) => total + groupDurationSeconds(group, now), 0),
|
|
152
|
-
tokensObservable: false,
|
|
153
|
-
toolCallsObservable: false,
|
|
150
|
+
...projectTaskUsageMetrics({ ...input, workItemId }),
|
|
154
151
|
laneCount,
|
|
155
152
|
groupCount: uniqueGroups.length,
|
|
156
153
|
retryCount: Math.max(0, attempts.length - laneCount),
|
|
@@ -158,32 +155,6 @@ function projectCost(groups, runs, _events, now) {
|
|
|
158
155
|
marginalValueStatus: "unavailable"
|
|
159
156
|
});
|
|
160
157
|
}
|
|
161
|
-
function projectWorkItemCost(groups, runs, now) {
|
|
162
|
-
const groupIds = new Set(groups.map(({ id }) => id));
|
|
163
|
-
const attempts = runs.filter(({ executionGroupId }) => (executionGroupId !== undefined && groupIds.has(executionGroupId)));
|
|
164
|
-
const wallClockSeconds = groups.reduce((total, group) => {
|
|
165
|
-
const started = Date.parse(group.createdAt);
|
|
166
|
-
const ended = group.lanes.some(({ disposition }) => disposition === "open")
|
|
167
|
-
? now.getTime()
|
|
168
|
-
: Date.parse(group.updatedAt);
|
|
169
|
-
return total + (Number.isFinite(started) && Number.isFinite(ended)
|
|
170
|
-
? Math.max(0, Math.floor((ended - started) / 1_000))
|
|
171
|
-
: 0);
|
|
172
|
-
}, 0);
|
|
173
|
-
const laneCount = groups.reduce((total, group) => total + group.lanes.length, 0);
|
|
174
|
-
return Object.freeze({
|
|
175
|
-
tokens: 0,
|
|
176
|
-
toolCalls: 0,
|
|
177
|
-
wallClockSeconds,
|
|
178
|
-
tokensObservable: false,
|
|
179
|
-
toolCallsObservable: false,
|
|
180
|
-
laneCount,
|
|
181
|
-
groupCount: groups.length,
|
|
182
|
-
retryCount: Math.max(0, attempts.length - laneCount),
|
|
183
|
-
marginalValuePercent: null,
|
|
184
|
-
marginalValueStatus: "unavailable"
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
158
|
function projectContext(groups, runs, snapshots) {
|
|
188
159
|
const refs = new Map();
|
|
189
160
|
for (const group of groups) {
|
|
@@ -223,12 +194,3 @@ function projectContext(groups, runs, snapshots) {
|
|
|
223
194
|
compressionStatus: "unavailable"
|
|
224
195
|
});
|
|
225
196
|
}
|
|
226
|
-
function groupDurationSeconds(group, now) {
|
|
227
|
-
const started = Date.parse(group.createdAt);
|
|
228
|
-
const ended = group.lanes.some(({ disposition }) => disposition === "open")
|
|
229
|
-
? now.getTime()
|
|
230
|
-
: Date.parse(group.updatedAt);
|
|
231
|
-
return Number.isFinite(started) && Number.isFinite(ended)
|
|
232
|
-
? Math.max(0, Math.floor((ended - started) / 1_000))
|
|
233
|
-
: 0;
|
|
234
|
-
}
|
|
@@ -37,7 +37,13 @@ const IMMEDIATE_KINDS = new Set([
|
|
|
37
37
|
"activation-failed",
|
|
38
38
|
"task-reopened",
|
|
39
39
|
"leader-turn-failed",
|
|
40
|
-
"role-turn-failed"
|
|
40
|
+
"role-turn-failed",
|
|
41
|
+
// A claimed interrupt-then handoff for a no-Run Leader turn: the operator/Leader
|
|
42
|
+
// explicitly redirected, so the release wake bypasses the aggregation debounce.
|
|
43
|
+
// It is still held by the leader-mailbox busy-gate until the interrupted native
|
|
44
|
+
// Turn reaches its proven terminal (decision-3 §4), so "immediate" only removes
|
|
45
|
+
// the extra 60s aggregation delay once that terminal actually opens the gate.
|
|
46
|
+
"interrupt-then"
|
|
41
47
|
]);
|
|
42
48
|
export function wakeReason(kind, ref) {
|
|
43
49
|
if (ref === undefined)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { mergePendingWakeup } from "./pendingWakeup.js";
|
|
2
2
|
export function queueLeaderWakeup(store, taskId, reason, now) {
|
|
3
|
+
if (store.getTask?.(taskId)?.status === "archived")
|
|
4
|
+
return null;
|
|
3
5
|
if (store.enqueueLeaderWakeup !== undefined) {
|
|
4
6
|
return store.enqueueLeaderWakeup(taskId, reason, now);
|
|
5
7
|
}
|