@zq-silk/yui 0.6.13 → 0.6.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
|
+
export const RUN_RECOVERY_ACTIONS = [
|
|
4
|
+
"diagnose",
|
|
5
|
+
"retry",
|
|
6
|
+
"replace-session",
|
|
7
|
+
"terminate"
|
|
8
|
+
];
|
|
9
|
+
/**
|
|
10
|
+
* Reads every durable record the recovery projection needs. Returns null
|
|
11
|
+
* only when the Run itself is absent.
|
|
12
|
+
*/
|
|
13
|
+
export function readRunRecoveryFacts(store, taskId, runId) {
|
|
14
|
+
const run = store.getAgentRun(taskId, runId);
|
|
15
|
+
if (run === null || run.taskId !== taskId)
|
|
16
|
+
return null;
|
|
17
|
+
const task = store.getTask(taskId);
|
|
18
|
+
const sessionSet = store.getTaskRoleSessionSet(taskId, run.roleName);
|
|
19
|
+
const progress = latestRunDurableProgressAt(store, taskId, run.roleName, runId);
|
|
20
|
+
return {
|
|
21
|
+
run,
|
|
22
|
+
task: task === null ? null : { id: task.id, status: task.status },
|
|
23
|
+
sessionSet,
|
|
24
|
+
progress,
|
|
25
|
+
latestProviderObservation: latestRunProviderObservation(store.listEvents(taskId), runId)
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Latest Provider observation for a Run. Provider timestamps are evidence:
|
|
30
|
+
* they explain why a stale fence was supplied but never authorize recovery.
|
|
31
|
+
*/
|
|
32
|
+
function latestRunProviderObservation(events, runId) {
|
|
33
|
+
let latest = null;
|
|
34
|
+
for (const event of events) {
|
|
35
|
+
if (event.type !== "runtime.observation")
|
|
36
|
+
continue;
|
|
37
|
+
if (event.payload.runId !== runId)
|
|
38
|
+
continue;
|
|
39
|
+
const kind = typeof event.payload.kind === "string" ? event.payload.kind : "unknown";
|
|
40
|
+
const receivedAt = typeof event.payload.receivedAt === "string"
|
|
41
|
+
&& Number.isFinite(Date.parse(event.payload.receivedAt))
|
|
42
|
+
? event.payload.receivedAt
|
|
43
|
+
: event.createdAt;
|
|
44
|
+
const at = Date.parse(receivedAt);
|
|
45
|
+
if (latest === null || at > latest.at) {
|
|
46
|
+
latest = { kind, receivedAt, at };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return latest === null ? null : { kind: latest.kind, receivedAt: latest.receivedAt };
|
|
50
|
+
}
|
|
51
|
+
export function projectRunRecovery(facts) {
|
|
52
|
+
const { run, task, sessionSet, progress } = facts;
|
|
53
|
+
const session = activeSession(facts);
|
|
54
|
+
const canonicalProgressAt = progress?.progressAt ?? null;
|
|
55
|
+
const accepted = run.deliveredAt !== undefined;
|
|
56
|
+
const acceptanceOptions = accepted
|
|
57
|
+
? ["accepted", "ambiguous"]
|
|
58
|
+
: ["rejected", "ambiguous"];
|
|
59
|
+
const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
|
|
60
|
+
const actions = blocked === null
|
|
61
|
+
? RUN_RECOVERY_ACTIONS.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
|
|
62
|
+
: [];
|
|
63
|
+
const judgmentRequired = blocked === null && actions.some((plan) => plan.argv.includes(PROVIDER_ACCEPTANCE_PLACEHOLDER))
|
|
64
|
+
? "Provider acceptance is not durably determined for every action; pass --provider-acceptance explicitly."
|
|
65
|
+
: undefined;
|
|
66
|
+
return {
|
|
67
|
+
taskId: run.taskId,
|
|
68
|
+
runId: run.id,
|
|
69
|
+
roleName: run.roleName,
|
|
70
|
+
runStatus: run.status,
|
|
71
|
+
recoverable: blocked === null,
|
|
72
|
+
canonicalProgressAt,
|
|
73
|
+
...(progress?.evidence === undefined ? {} : { canonicalProgressEvidence: progress.evidence }),
|
|
74
|
+
provider: {
|
|
75
|
+
acceptedAt: run.deliveredAt ?? null,
|
|
76
|
+
observedAt: facts.latestProviderObservation?.receivedAt ?? null,
|
|
77
|
+
observationKind: facts.latestProviderObservation?.kind ?? null
|
|
78
|
+
},
|
|
79
|
+
providerAcceptance: {
|
|
80
|
+
accepted,
|
|
81
|
+
options: acceptanceOptions
|
|
82
|
+
},
|
|
83
|
+
session: session === null ? null : {
|
|
84
|
+
status: session.status,
|
|
85
|
+
...(session.nativeSessionId === undefined ? {} : { nativeSessionId: session.nativeSessionId }),
|
|
86
|
+
...(session.launchId === undefined ? {} : { launchId: session.launchId })
|
|
87
|
+
},
|
|
88
|
+
actions,
|
|
89
|
+
...(judgmentRequired === undefined ? {} : { judgmentRequired }),
|
|
90
|
+
...(blocked === null ? {} : { reason: blocked })
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const PROVIDER_ACCEPTANCE_PLACEHOLDER = "<accepted|rejected|ambiguous>";
|
|
94
|
+
function activeSession(facts) {
|
|
95
|
+
const sessions = facts.sessionSet;
|
|
96
|
+
if (sessions === null)
|
|
97
|
+
return null;
|
|
98
|
+
const session = sessions.sessions[sessions.activeAgentId];
|
|
99
|
+
if (session === undefined)
|
|
100
|
+
return null;
|
|
101
|
+
if (session.agentId !== facts.run.effective.agentId)
|
|
102
|
+
return null;
|
|
103
|
+
if (session.adapterId !== facts.run.effective.adapterId)
|
|
104
|
+
return null;
|
|
105
|
+
return session;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Mirrors the fail-closed checks of `recoverExactAgentRun` that are visible
|
|
109
|
+
* from durable records. A non-null result means recovery cannot currently be
|
|
110
|
+
* applied; the canonical fence is still projected for diagnosis.
|
|
111
|
+
*/
|
|
112
|
+
function recoveryBlocker(facts, session, canonicalProgressAt) {
|
|
113
|
+
const { run, task } = facts;
|
|
114
|
+
if (task === null)
|
|
115
|
+
return "task-missing";
|
|
116
|
+
if (task.status !== "active")
|
|
117
|
+
return "task-terminal";
|
|
118
|
+
if (run.status !== "active")
|
|
119
|
+
return "run-terminal";
|
|
120
|
+
if (canonicalProgressAt === null)
|
|
121
|
+
return "progress-unavailable";
|
|
122
|
+
if (session === null)
|
|
123
|
+
return "session-missing";
|
|
124
|
+
if (session.status === "stopped")
|
|
125
|
+
return "session-stopped";
|
|
126
|
+
if (session.status === "broken")
|
|
127
|
+
return "session-broken";
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function buildActionPlan(facts, action, session, canonicalProgressAt) {
|
|
131
|
+
const { run } = facts;
|
|
132
|
+
const acceptance = actionAcceptance(facts, action);
|
|
133
|
+
const argv = [
|
|
134
|
+
"task",
|
|
135
|
+
"run",
|
|
136
|
+
"recover",
|
|
137
|
+
`${run.taskId}/${run.id}`,
|
|
138
|
+
"--action",
|
|
139
|
+
action,
|
|
140
|
+
"--expected-progress-at",
|
|
141
|
+
canonicalProgressAt,
|
|
142
|
+
"--provider-acceptance",
|
|
143
|
+
acceptance,
|
|
144
|
+
"--reason",
|
|
145
|
+
"<text>",
|
|
146
|
+
"--agent-id",
|
|
147
|
+
run.effective.agentId,
|
|
148
|
+
"--adapter-id",
|
|
149
|
+
run.effective.adapterId,
|
|
150
|
+
...(session.nativeSessionId === undefined
|
|
151
|
+
? []
|
|
152
|
+
: ["--native-session-id", session.nativeSessionId]),
|
|
153
|
+
...(session.launchId === undefined
|
|
154
|
+
? []
|
|
155
|
+
: ["--launch-id", session.launchId])
|
|
156
|
+
];
|
|
157
|
+
const command = `yui ${argv
|
|
158
|
+
.map((part) => (part === "<text>" ? '"<text>"' : part))
|
|
159
|
+
.join(" ")}`;
|
|
160
|
+
const fingerprintSource = [
|
|
161
|
+
run.id,
|
|
162
|
+
action,
|
|
163
|
+
canonicalProgressAt,
|
|
164
|
+
run.effective.agentId,
|
|
165
|
+
run.effective.adapterId,
|
|
166
|
+
session.nativeSessionId ?? "",
|
|
167
|
+
session.launchId ?? ""
|
|
168
|
+
].join("|");
|
|
169
|
+
return {
|
|
170
|
+
action,
|
|
171
|
+
reason: ACTION_REASONS[action],
|
|
172
|
+
expectedProgressAt: canonicalProgressAt,
|
|
173
|
+
agentId: run.effective.agentId,
|
|
174
|
+
adapterId: run.effective.adapterId,
|
|
175
|
+
...(session.nativeSessionId === undefined
|
|
176
|
+
? {}
|
|
177
|
+
: { nativeSessionId: session.nativeSessionId }),
|
|
178
|
+
...(session.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
179
|
+
command,
|
|
180
|
+
argv,
|
|
181
|
+
fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The acceptance value for the copy-paste command. When exactly one value is
|
|
186
|
+
* durably valid it is filled in (the durable record, not a guess); otherwise
|
|
187
|
+
* the Leader must choose and the command carries an explicit placeholder.
|
|
188
|
+
*/
|
|
189
|
+
function actionAcceptance(facts, action) {
|
|
190
|
+
if (action === "diagnose")
|
|
191
|
+
return PROVIDER_ACCEPTANCE_PLACEHOLDER;
|
|
192
|
+
return facts.run.deliveredAt === undefined ? "rejected" : "accepted";
|
|
193
|
+
}
|
|
194
|
+
const ACTION_REASONS = {
|
|
195
|
+
diagnose: "Collect bounded diagnostics before any state-changing recovery.",
|
|
196
|
+
retry: "Request another provider turn on the same native Session when the failure is transient.",
|
|
197
|
+
"replace-session": "Request a fresh native Session when the current one is unusable.",
|
|
198
|
+
terminate: "Fail the Run explicitly when recovery is not viable."
|
|
199
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AgentDriverRegistry } from "./agentDriver.js";
|
|
2
2
|
import { claudeTranscriptObserver, codexTranscriptObserver, transcriptObserverSource } from "./builtinTranscriptObserver.js";
|
|
3
|
+
import { parseClaudeError } from "./providerErrorCodes.js";
|
|
3
4
|
export const CODEX_DRIVER_ID = "openai/codex";
|
|
4
5
|
export const CLAUDE_CODE_DRIVER_ID = "anthropic/claude-code";
|
|
5
6
|
export function builtinDriverIdForAdapter(adapterId) {
|
|
@@ -352,8 +353,10 @@ function claudeFailure(payload) {
|
|
|
352
353
|
const code = firstIdentity(payload, ["error"], "Claude StopFailure error");
|
|
353
354
|
const details = optionalText(payload.error_details);
|
|
354
355
|
const lastOutput = optionalText(payload.last_assistant_message);
|
|
356
|
+
const parsed = parseClaudeError(code, details);
|
|
355
357
|
return {
|
|
356
358
|
failure: {
|
|
359
|
+
...(parsed.code !== "unknown" ? { errorCode: parsed.code } : {}),
|
|
357
360
|
code,
|
|
358
361
|
...(details === undefined ? {} : { details }),
|
|
359
362
|
...(lastOutput === undefined ? {} : { lastOutput }),
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
export function codexTranscriptUsage(transcript) {
|
|
2
|
-
|
|
2
|
+
const report = codexTranscriptUsageReport(transcript);
|
|
3
|
+
if (report === null)
|
|
4
|
+
return null;
|
|
5
|
+
return Object.freeze({
|
|
6
|
+
inputTokens: report.uncachedInputTokens + report.cacheReadTokens + report.cacheCreatedTokens,
|
|
7
|
+
outputTokens: report.outputTokens,
|
|
8
|
+
...(report.cacheReadTokens + report.cacheCreatedTokens === 0
|
|
9
|
+
? {}
|
|
10
|
+
: { cachedInputTokens: report.cacheReadTokens + report.cacheCreatedTokens })
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export function codexTranscriptUsageReport(transcript) {
|
|
14
|
+
// Codex token_count events are cumulative session snapshots. Per-request
|
|
15
|
+
// input is the delta between consecutive snapshots; the first snapshot is
|
|
16
|
+
// treated as one full request.
|
|
17
|
+
let latestInput = 0;
|
|
18
|
+
let latestCached = 0;
|
|
19
|
+
let latestOutput = 0;
|
|
20
|
+
let previous = 0;
|
|
21
|
+
let peak = 0;
|
|
22
|
+
let requests = 0;
|
|
23
|
+
let seen = false;
|
|
3
24
|
for (const line of transcript.split("\n")) {
|
|
4
25
|
const entry = parseLine(line);
|
|
5
26
|
if (entry?.type !== "event_msg")
|
|
@@ -13,18 +34,44 @@ export function codexTranscriptUsage(transcript) {
|
|
|
13
34
|
const outputTokens = integer(usage?.output_tokens);
|
|
14
35
|
if (inputTokens === null || outputTokens === null)
|
|
15
36
|
continue;
|
|
16
|
-
const cachedInputTokens = integer(usage?.cached_input_tokens);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
const cachedInputTokens = integer(usage?.cached_input_tokens) ?? 0;
|
|
38
|
+
requests += 1;
|
|
39
|
+
seen = true;
|
|
40
|
+
const total = inputTokens + cachedInputTokens;
|
|
41
|
+
const delta = Math.max(0, total - previous);
|
|
42
|
+
if (delta > peak)
|
|
43
|
+
peak = delta;
|
|
44
|
+
previous = total;
|
|
45
|
+
latestInput = inputTokens;
|
|
46
|
+
latestCached = cachedInputTokens;
|
|
47
|
+
latestOutput = outputTokens;
|
|
24
48
|
}
|
|
25
|
-
|
|
49
|
+
if (!seen)
|
|
50
|
+
return null;
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
requests,
|
|
53
|
+
uncachedInputTokens: Math.max(0, latestInput - latestCached),
|
|
54
|
+
cacheReadTokens: latestCached,
|
|
55
|
+
cacheCreatedTokens: 0,
|
|
56
|
+
outputTokens: latestOutput,
|
|
57
|
+
peakRequestTokens: peak
|
|
58
|
+
});
|
|
26
59
|
}
|
|
27
60
|
export function claudeTranscriptUsage(transcript) {
|
|
61
|
+
const report = claudeTranscriptUsageReport(transcript);
|
|
62
|
+
if (report === null)
|
|
63
|
+
return null;
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
inputTokens: report.uncachedInputTokens + report.cacheReadTokens + report.cacheCreatedTokens,
|
|
66
|
+
outputTokens: report.outputTokens,
|
|
67
|
+
...(report.cacheReadTokens + report.cacheCreatedTokens === 0
|
|
68
|
+
? {}
|
|
69
|
+
: { cachedInputTokens: report.cacheReadTokens + report.cacheCreatedTokens })
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export function claudeTranscriptUsageReport(transcript) {
|
|
73
|
+
// Each Claude assistant message carries its own per-request usage, so the
|
|
74
|
+
// report is a direct aggregation; the peak is the largest single message.
|
|
28
75
|
const messages = new Map();
|
|
29
76
|
for (const line of transcript.split("\n")) {
|
|
30
77
|
const entry = parseLine(line);
|
|
@@ -33,13 +80,11 @@ export function claudeTranscriptUsage(transcript) {
|
|
|
33
80
|
const message = object(entry.message);
|
|
34
81
|
const usage = object(message?.usage);
|
|
35
82
|
const directInput = integer(usage?.input_tokens);
|
|
36
|
-
const
|
|
37
|
-
if (directInput === null ||
|
|
83
|
+
const output = integer(usage?.output_tokens);
|
|
84
|
+
if (directInput === null || output === null)
|
|
38
85
|
continue;
|
|
39
86
|
const cacheRead = integer(usage?.cache_read_input_tokens) ?? 0;
|
|
40
87
|
const cacheCreated = integer(usage?.cache_creation_input_tokens) ?? 0;
|
|
41
|
-
const details = object(usage?.output_tokens_details);
|
|
42
|
-
const reasoningTokens = integer(details?.thinking_tokens);
|
|
43
88
|
const key = typeof message?.id === "string" && message.id.length > 0
|
|
44
89
|
? `message:${message.id}`
|
|
45
90
|
: typeof entry.uuid === "string" && entry.uuid.length > 0
|
|
@@ -49,32 +94,31 @@ export function claudeTranscriptUsage(transcript) {
|
|
|
49
94
|
// stable provider or entry identity, summing them would fabricate growth.
|
|
50
95
|
if (key === null)
|
|
51
96
|
continue;
|
|
52
|
-
messages.set(key,
|
|
53
|
-
// Normalize inputTokens as the complete input total. cachedInputTokens is
|
|
54
|
-
// a breakdown and must not be added to it again by the projection.
|
|
55
|
-
inputTokens: directInput + cacheRead + cacheCreated,
|
|
56
|
-
outputTokens,
|
|
57
|
-
cachedInputTokens: cacheRead + cacheCreated,
|
|
58
|
-
...(reasoningTokens === null ? {} : { reasoningTokens })
|
|
59
|
-
}));
|
|
97
|
+
messages.set(key, { directInput, cacheRead, cacheCreated, output });
|
|
60
98
|
}
|
|
61
99
|
if (messages.size === 0)
|
|
62
100
|
return null;
|
|
63
|
-
let
|
|
101
|
+
let uncachedInputTokens = 0;
|
|
102
|
+
let cacheReadTokens = 0;
|
|
103
|
+
let cacheCreatedTokens = 0;
|
|
64
104
|
let outputTokens = 0;
|
|
65
|
-
let
|
|
66
|
-
let reasoningTokens = 0;
|
|
105
|
+
let peak = 0;
|
|
67
106
|
for (const usage of messages.values()) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
107
|
+
uncachedInputTokens += usage.directInput;
|
|
108
|
+
cacheReadTokens += usage.cacheRead;
|
|
109
|
+
cacheCreatedTokens += usage.cacheCreated;
|
|
110
|
+
outputTokens += usage.output;
|
|
111
|
+
const total = usage.directInput + usage.cacheRead + usage.cacheCreated;
|
|
112
|
+
if (total > peak)
|
|
113
|
+
peak = total;
|
|
72
114
|
}
|
|
73
115
|
return Object.freeze({
|
|
74
|
-
|
|
116
|
+
requests: messages.size,
|
|
117
|
+
uncachedInputTokens,
|
|
118
|
+
cacheReadTokens,
|
|
119
|
+
cacheCreatedTokens,
|
|
75
120
|
outputTokens,
|
|
76
|
-
|
|
77
|
-
...(reasoningTokens === 0 ? {} : { reasoningTokens })
|
|
121
|
+
peakRequestTokens: peak
|
|
78
122
|
});
|
|
79
123
|
}
|
|
80
124
|
function parseLine(line) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { createProviderContinuation, observeProviderContinuation, providerContinuationKey, recordProviderReport } from "./providerContinuation.js";
|
|
2
3
|
import { createRuntimeObservation } from "./runtimeObservation.js";
|
|
3
4
|
export function foldContinuationObservation(existing, raw) {
|
|
@@ -30,12 +31,14 @@ export function foldContinuationObservation(existing, raw) {
|
|
|
30
31
|
throw new Error("Continuation observation identity does not match the existing projection.");
|
|
31
32
|
}
|
|
32
33
|
if (observation.kind === "continuation.reported") {
|
|
34
|
+
const resultReceipt = durableResultReceipt(payload.summary);
|
|
33
35
|
const next = recordProviderReport(base, {
|
|
34
36
|
reportId: payload.reportId,
|
|
35
37
|
...(payload.resultRef === undefined ? {} : { resultRef: payload.resultRef }),
|
|
36
38
|
...(payload.providerDeliveryRef === undefined
|
|
37
39
|
? {}
|
|
38
40
|
: { providerDeliveryRef: payload.providerDeliveryRef }),
|
|
41
|
+
...resultReceipt,
|
|
39
42
|
observedAt: observation.observedAt ?? observation.receivedAt
|
|
40
43
|
}, observation.sequence);
|
|
41
44
|
return {
|
|
@@ -62,6 +65,20 @@ export function foldContinuationObservation(existing, raw) {
|
|
|
62
65
|
continuation: next
|
|
63
66
|
};
|
|
64
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* The durable-result receipt for a native child report. Yui only claims
|
|
70
|
+
* "durable-result" durability when it has actually persisted result content;
|
|
71
|
+
* the sha256 digest makes replays idempotent and lets recovery verify the
|
|
72
|
+
* result by digest. A report without persisted content stays best-effort.
|
|
73
|
+
*/
|
|
74
|
+
function durableResultReceipt(summary) {
|
|
75
|
+
if (summary === undefined || summary.trim().length === 0)
|
|
76
|
+
return {};
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
resultDigest: createHash("sha256").update(summary).digest("hex"),
|
|
79
|
+
resultSize: summary.length
|
|
80
|
+
});
|
|
81
|
+
}
|
|
65
82
|
/**
|
|
66
83
|
* Commits the semantic continuation fact and its Leader notification before
|
|
67
84
|
* releasing child ownership. The surrounding TaskStore transaction is the
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { createPromptEnvelope } from "./promptEnvelope.js";
|
|
2
|
+
export { codexTranscriptUsage, codexTranscriptUsageReport, claudeTranscriptUsage, claudeTranscriptUsageReport } from "./builtinTranscriptUsage.js";
|
|
2
3
|
export { createRuntimeBinding } from "./runtimeBinding.js";
|
|
3
4
|
export { normalizeRuntimeOwner } from "./runtimeOwner.js";
|
|
4
5
|
export { createSessionLaunchRequest } from "./sessionLaunchRequest.js";
|
|
@@ -8,6 +9,7 @@ export { TmuxPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
|
|
|
8
9
|
export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TASK_RUNTIME_SERVICE_NAMESPACE, assertTaskRuntimeIsolationPreflight, createTaskRuntimeIsolationDescriptor, parseTaskRuntimeIsolationDescriptor, planTaskRuntimeCleanup, taskRuntimeIsolationEnvironment, taskRuntimeIsolationFingerprint } from "./taskRuntimeIsolation.js";
|
|
9
10
|
export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
10
11
|
export { FileSessionOwnerRegistry } from "./sessionOwnerRegistry.js";
|
|
12
|
+
export { formatRuntimeLaunchDiagnostic, redactLaunchArgument, redactLaunchText, RuntimeLaunchFailure, toRuntimeLaunchFailure } from "./launchDiagnostics.js";
|
|
11
13
|
export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
|
|
12
14
|
export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
|
|
13
15
|
export { codexNotificationBoundary, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { CommandExecutionError } from "../tmux/commandExecutor.js";
|
|
2
|
+
export const RUNTIME_LAUNCH_PHASES = [
|
|
3
|
+
"validation",
|
|
4
|
+
"host-start",
|
|
5
|
+
"host-started",
|
|
6
|
+
"native-session-discovery",
|
|
7
|
+
"host-stop",
|
|
8
|
+
"delivery"
|
|
9
|
+
];
|
|
10
|
+
export const RUNTIME_LAUNCH_KINDS = [
|
|
11
|
+
"config",
|
|
12
|
+
"auth",
|
|
13
|
+
"executable",
|
|
14
|
+
"tmux",
|
|
15
|
+
"timeout",
|
|
16
|
+
"provider",
|
|
17
|
+
"unknown"
|
|
18
|
+
];
|
|
19
|
+
const MAX_ARGUMENT_CHARS = 1_000;
|
|
20
|
+
const MAX_STDERR_CHARS = 4_000;
|
|
21
|
+
const MAX_DETAIL_CHARS = 1_000;
|
|
22
|
+
const SECRET_VALUE_PATTERN = /(api[_-]?key|access[_-]?token|refresh[_-]?token|secret|password|passwd|cookie|authorization)(\s*[=:]\s*)([^\s,;]+)/gi;
|
|
23
|
+
// Word boundary keeps "task-5-…" workspace paths from being mistaken for keys.
|
|
24
|
+
const OPENAI_KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{6,}/gu;
|
|
25
|
+
/** A bounded, single-line, secret-redacted launch failure for Run summaries. */
|
|
26
|
+
export class RuntimeLaunchFailure extends Error {
|
|
27
|
+
diagnostic;
|
|
28
|
+
name = "RuntimeLaunchFailure";
|
|
29
|
+
constructor(diagnostic) {
|
|
30
|
+
super(formatRuntimeLaunchDiagnostic(diagnostic));
|
|
31
|
+
this.diagnostic = diagnostic;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function toRuntimeLaunchFailure(error, phase, context = {}) {
|
|
35
|
+
if (error instanceof RuntimeLaunchFailure)
|
|
36
|
+
return error;
|
|
37
|
+
const commandError = error instanceof CommandExecutionError ? error : undefined;
|
|
38
|
+
const stderrTail = tail(redactLaunchText(context.stderrTail ?? commandError?.stderr ?? ""), MAX_STDERR_CHARS);
|
|
39
|
+
const kind = classifyLaunchFailure(phase, error, stderrTail);
|
|
40
|
+
const exitStatus = context.exitStatus ?? commandError?.exitStatus;
|
|
41
|
+
const detail = tail(redactLaunchText(error instanceof Error ? error.message : String(error)), MAX_DETAIL_CHARS);
|
|
42
|
+
return new RuntimeLaunchFailure({
|
|
43
|
+
phase,
|
|
44
|
+
kind,
|
|
45
|
+
...(context.command === undefined ? {} : { command: redactLaunchText(context.command) }),
|
|
46
|
+
...(context.argv === undefined
|
|
47
|
+
? {}
|
|
48
|
+
: { argv: context.argv.map((argument) => redactLaunchArgument(argument)) }),
|
|
49
|
+
...(context.cwd === undefined ? {} : { cwd: context.cwd }),
|
|
50
|
+
...(exitStatus === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { exitStatus }),
|
|
53
|
+
...(context.signal === undefined ? {} : { signal: context.signal }),
|
|
54
|
+
...(stderrTail.length === 0 ? {} : { stderrTail }),
|
|
55
|
+
...(context.pane === undefined ? {} : { pane: context.pane }),
|
|
56
|
+
...(detail.length === 0 ? {} : { detail }),
|
|
57
|
+
...(launchHint(kind, context.agentId) === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { hint: launchHint(kind, context.agentId) })
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function formatRuntimeLaunchDiagnostic(diagnostic) {
|
|
63
|
+
const fields = [
|
|
64
|
+
`failurePhase=${diagnostic.phase}`,
|
|
65
|
+
`failureKind=${diagnostic.kind}`
|
|
66
|
+
];
|
|
67
|
+
if (diagnostic.command !== undefined)
|
|
68
|
+
fields.push(`command=${JSON.stringify(diagnostic.command)}`);
|
|
69
|
+
if (diagnostic.argv !== undefined)
|
|
70
|
+
fields.push(`argv=${JSON.stringify(diagnostic.argv)}`);
|
|
71
|
+
if (diagnostic.cwd !== undefined)
|
|
72
|
+
fields.push(`cwd=${JSON.stringify(diagnostic.cwd)}`);
|
|
73
|
+
if (diagnostic.exitStatus !== undefined)
|
|
74
|
+
fields.push(`exitStatus=${diagnostic.exitStatus}`);
|
|
75
|
+
if (diagnostic.signal !== undefined)
|
|
76
|
+
fields.push(`signal=${JSON.stringify(diagnostic.signal)}`);
|
|
77
|
+
if (diagnostic.stderrTail !== undefined) {
|
|
78
|
+
fields.push(`stderrTail=${JSON.stringify(tail(diagnostic.stderrTail, MAX_STDERR_CHARS))}`);
|
|
79
|
+
}
|
|
80
|
+
if (diagnostic.pane !== undefined)
|
|
81
|
+
fields.push(`pane=${JSON.stringify(diagnostic.pane)}`);
|
|
82
|
+
if (diagnostic.detail !== undefined) {
|
|
83
|
+
fields.push(`detail=${JSON.stringify(tail(diagnostic.detail, MAX_DETAIL_CHARS))}`);
|
|
84
|
+
}
|
|
85
|
+
if (diagnostic.hint !== undefined)
|
|
86
|
+
fields.push(`hint=${JSON.stringify(diagnostic.hint)}`);
|
|
87
|
+
return `Role Run could not start: ${fields.join(" ")}`;
|
|
88
|
+
}
|
|
89
|
+
export function redactLaunchArgument(value) {
|
|
90
|
+
return tail(redactLaunchText(value), MAX_ARGUMENT_CHARS);
|
|
91
|
+
}
|
|
92
|
+
export function redactLaunchText(value) {
|
|
93
|
+
return value
|
|
94
|
+
.replace(OPENAI_KEY_PATTERN, "[REDACTED]")
|
|
95
|
+
.replace(SECRET_VALUE_PATTERN, "$1$2[REDACTED]");
|
|
96
|
+
}
|
|
97
|
+
function classifyLaunchFailure(phase, error, stderrTail) {
|
|
98
|
+
if ((error instanceof CommandExecutionError && error.code === "COMMAND_NOT_FOUND")
|
|
99
|
+
|| /command not found|no such file or directory/i.test(stderrTail)) {
|
|
100
|
+
return "executable";
|
|
101
|
+
}
|
|
102
|
+
if (isConfigFailure(stderrTail))
|
|
103
|
+
return "config";
|
|
104
|
+
if (isAuthFailure(stderrTail))
|
|
105
|
+
return "auth";
|
|
106
|
+
if (phase === "host-start")
|
|
107
|
+
return "tmux";
|
|
108
|
+
if (phase === "native-session-discovery")
|
|
109
|
+
return "timeout";
|
|
110
|
+
if (phase === "validation" || phase === "delivery")
|
|
111
|
+
return "config";
|
|
112
|
+
return "provider";
|
|
113
|
+
}
|
|
114
|
+
function isConfigFailure(stderrTail) {
|
|
115
|
+
return /unknown model|invalid effort|unknown option|invalid value|unexpected argument|invalid .*config|model .*not (?:found|supported)|effort .*not (?:valid|supported)/i
|
|
116
|
+
.test(stderrTail);
|
|
117
|
+
}
|
|
118
|
+
function isAuthFailure(stderrTail) {
|
|
119
|
+
return /\bauth(?:entication|orization)?\b|unauthorized|forbidden|credential|api[_-]?key|login|sign in|token/i
|
|
120
|
+
.test(stderrTail);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Conservative detection of fatal error signatures in agent output during
|
|
124
|
+
* launch. These are errors the agent cannot recover from without
|
|
125
|
+
* intervention (missing executable, invalid configuration, authentication
|
|
126
|
+
* failure). Transient errors (network retries, temporary blips) are not
|
|
127
|
+
* matched. Failure-kind classification uses the broader patterns in
|
|
128
|
+
* classifyLaunchFailure once a fatal output is confirmed.
|
|
129
|
+
*/
|
|
130
|
+
export function hasFatalLaunchOutput(output) {
|
|
131
|
+
return /command not found|no such file or directory/i.test(output)
|
|
132
|
+
|| /unknown model|invalid effort|unknown option|invalid value|unexpected argument/i.test(output)
|
|
133
|
+
|| /401\s+unauthorized|403\s+forbidden|authentication failed|not logged in|sign in with/i
|
|
134
|
+
.test(output);
|
|
135
|
+
}
|
|
136
|
+
function launchHint(kind, agentId) {
|
|
137
|
+
switch (kind) {
|
|
138
|
+
case "config":
|
|
139
|
+
return agentId === undefined
|
|
140
|
+
? "Verify the Provider model and effort configuration."
|
|
141
|
+
: `Verify custom model/effort with yui agent capabilities ${agentId}.`;
|
|
142
|
+
case "auth":
|
|
143
|
+
return "Verify Provider authentication and the Agent environment.";
|
|
144
|
+
case "executable":
|
|
145
|
+
return "Verify the Provider command is installed and on PATH.";
|
|
146
|
+
case "timeout":
|
|
147
|
+
return "Verify Provider lifecycle hooks and Controller connectivity.";
|
|
148
|
+
default:
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function tail(value, maxChars) {
|
|
153
|
+
return value.length <= maxChars ? value : value.slice(value.length - maxChars);
|
|
154
|
+
}
|
|
@@ -2,6 +2,19 @@ import { mailboxHasWork } from "../coordination/workMailbox.js";
|
|
|
2
2
|
export const RUNTIME_LIFECYCLE_OWNER = "runtime-lifecycle";
|
|
3
3
|
export const RUNTIME_LAUNCH_RESERVED_REASON = "runtime-launch-reserved";
|
|
4
4
|
export const RUNTIME_CLEANUP_REQUIRED_REASON = "runtime-cleanup-required";
|
|
5
|
+
/**
|
|
6
|
+
* A Role runtime lifecycle lane already holds an in-flight operation (a
|
|
7
|
+
* launch reservation or a cleanup obligation). This is scheduler
|
|
8
|
+
* backpressure: the equivalent wake/Run must be retried after the lane
|
|
9
|
+
* settles. It is never grounds to terminalize a Run, because the contention
|
|
10
|
+
* happens before (or outside) any semantic Run launch.
|
|
11
|
+
*/
|
|
12
|
+
export class RuntimeLifecycleBusyError extends Error {
|
|
13
|
+
name = "RuntimeLifecycleBusyError";
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
5
18
|
export function runtimeLifecycleTarget(owner) {
|
|
6
19
|
return owner.scope === "task"
|
|
7
20
|
? {
|
|
@@ -24,6 +24,7 @@ export function createProviderContinuation(input) {
|
|
|
24
24
|
attachment: input.attachment,
|
|
25
25
|
observation: input.observation,
|
|
26
26
|
mayWriteWorkspace: input.mayWriteWorkspace,
|
|
27
|
+
durability: "best-effort",
|
|
27
28
|
reports: [],
|
|
28
29
|
...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
|
|
29
30
|
identityConflict: false,
|
|
@@ -40,9 +41,17 @@ export function recordProviderReport(raw, report, providerSequence) {
|
|
|
40
41
|
const normalized = validateProviderReport(report);
|
|
41
42
|
if (current.reports.some((entry) => entry.reportId === normalized.reportId))
|
|
42
43
|
return current;
|
|
44
|
+
// Idempotent durable-result receipt: a replay carrying the same content
|
|
45
|
+
// digest must not create a second report even when the Provider assigns a
|
|
46
|
+
// fresh report id.
|
|
47
|
+
if (normalized.resultDigest !== undefined
|
|
48
|
+
&& current.reports.some((entry) => entry.resultDigest === normalized.resultDigest)) {
|
|
49
|
+
return current;
|
|
50
|
+
}
|
|
43
51
|
return validateProviderContinuation({
|
|
44
52
|
...current,
|
|
45
53
|
reports: [...current.reports, normalized],
|
|
54
|
+
...(normalized.resultDigest === undefined ? {} : { durability: "durable-result" }),
|
|
46
55
|
...(providerSequence === undefined ? {} : { lastProviderSequence: providerSequence }),
|
|
47
56
|
updatedAt: normalized.observedAt
|
|
48
57
|
});
|
|
@@ -160,10 +169,21 @@ export function validateProviderContinuation(value) {
|
|
|
160
169
|
if (typeof value.mayWriteWorkspace !== "boolean" || typeof value.identityConflict !== "boolean") {
|
|
161
170
|
throw new Error("Provider Continuation flags are invalid.");
|
|
162
171
|
}
|
|
172
|
+
if (value.durability !== "best-effort" && value.durability !== "durable-result") {
|
|
173
|
+
throw new Error("Provider Continuation durability is invalid.");
|
|
174
|
+
}
|
|
163
175
|
const reports = value.reports.map(validateProviderReport);
|
|
164
176
|
if (new Set(reports.map((entry) => entry.reportId)).size !== reports.length) {
|
|
165
177
|
throw new Error("Provider Continuation reports contain duplicate identity.");
|
|
166
178
|
}
|
|
179
|
+
if (value.durability === "durable-result"
|
|
180
|
+
&& !reports.some((entry) => entry.resultDigest !== undefined)) {
|
|
181
|
+
throw new Error("Durable-result Provider Continuation requires a result digest receipt.");
|
|
182
|
+
}
|
|
183
|
+
if (value.durability === "best-effort"
|
|
184
|
+
&& reports.some((entry) => entry.resultDigest !== undefined)) {
|
|
185
|
+
throw new Error("Best-effort Provider Continuation must not carry a result digest receipt.");
|
|
186
|
+
}
|
|
167
187
|
timestamp(value.createdAt, "Provider Continuation createdAt");
|
|
168
188
|
timestamp(value.updatedAt, "Provider Continuation updatedAt");
|
|
169
189
|
if (value.settledAt !== undefined) {
|
|
@@ -197,9 +217,27 @@ function validateProviderReport(value) {
|
|
|
197
217
|
...(value.providerDeliveryRef === undefined
|
|
198
218
|
? {}
|
|
199
219
|
: { providerDeliveryRef: text(value.providerDeliveryRef, "Provider delivery ref") }),
|
|
220
|
+
...(value.resultDigest === undefined
|
|
221
|
+
? {}
|
|
222
|
+
: { resultDigest: digest(value.resultDigest) }),
|
|
223
|
+
...(value.resultSize === undefined
|
|
224
|
+
? {}
|
|
225
|
+
: { resultSize: reportSize(value.resultSize) }),
|
|
200
226
|
observedAt: timestamp(value.observedAt, "Provider report observedAt")
|
|
201
227
|
};
|
|
202
228
|
}
|
|
229
|
+
function digest(value) {
|
|
230
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
231
|
+
throw new Error("Provider report result digest must be a sha256 hex string.");
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function reportSize(value) {
|
|
236
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
237
|
+
throw new Error("Provider report result size is invalid.");
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
203
241
|
function providerSequenceRegresses(current, incoming) {
|
|
204
242
|
if (incoming === undefined)
|
|
205
243
|
return false;
|