@wardby/cli 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/deploy/local/docker-compose.quickstart-coding.yml +5 -0
- package/dist/claude-plugin/command.d.ts +4 -1
- package/dist/claude-plugin/command.js +7 -4
- package/dist/cli.js +24 -0
- package/dist/coding/transcript/recorder.js +2 -8
- package/dist/core/dispatch.js +3 -0
- package/dist/core/engine-native.d.ts +2 -0
- package/dist/core/engine-native.js +13 -0
- package/dist/core/error-code.js +1 -0
- package/dist/core/listen-address.d.ts +10 -0
- package/dist/core/listen-address.js +14 -0
- package/dist/core/power-idle.d.ts +8 -2
- package/dist/core/power-idle.js +17 -3
- package/dist/core/power.d.ts +5 -0
- package/dist/core/power.js +5 -0
- package/dist/core/retention-days.d.ts +5 -0
- package/dist/core/retention-days.js +14 -0
- package/dist/core/run-steps/config.d.ts +25 -0
- package/dist/core/run-steps/config.js +40 -0
- package/dist/core/run-steps/control-chars.d.ts +16 -0
- package/dist/core/run-steps/control-chars.js +26 -0
- package/dist/core/run-steps/detail.d.ts +32 -0
- package/dist/core/run-steps/detail.js +240 -0
- package/dist/core/run-steps/extract.d.ts +52 -0
- package/dist/core/run-steps/extract.js +467 -0
- package/dist/core/run-steps/ingress.d.ts +51 -0
- package/dist/core/run-steps/ingress.js +160 -0
- package/dist/core/run-steps/last-step.d.ts +15 -0
- package/dist/core/run-steps/last-step.js +35 -0
- package/dist/core/run-steps/native.d.ts +48 -0
- package/dist/core/run-steps/native.js +164 -0
- package/dist/core/run-steps/seen.d.ts +16 -0
- package/dist/core/run-steps/seen.js +35 -0
- package/dist/core/run-steps/summarize.d.ts +57 -0
- package/dist/core/run-steps/summarize.js +304 -0
- package/dist/core/run-steps/sweep.d.ts +21 -0
- package/dist/core/run-steps/sweep.js +41 -0
- package/dist/core/run-steps/types.d.ts +89 -0
- package/dist/core/run-steps/types.js +39 -0
- package/dist/core/run-steps/wire.d.ts +35 -0
- package/dist/core/run-steps/wire.js +47 -0
- package/dist/core/run-steps/writer.d.ts +49 -0
- package/dist/core/run-steps/writer.js +279 -0
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.js +15 -1
- package/dist/generated/prisma/browser.d.ts +8 -0
- package/dist/generated/prisma/client.d.ts +8 -0
- package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
- package/dist/generated/prisma/enums.d.ts +6 -0
- package/dist/generated/prisma/enums.js +5 -0
- package/dist/generated/prisma/internal/class.d.ts +11 -0
- package/dist/generated/prisma/internal/class.js +4 -4
- package/dist/generated/prisma/internal/prismaNamespace.d.ts +103 -1
- package/dist/generated/prisma/internal/prismaNamespace.js +19 -1
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +19 -0
- package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +19 -1
- package/dist/generated/prisma/models/Agent.d.ts +101 -1
- package/dist/generated/prisma/models/CodingProxySession.d.ts +41 -1
- package/dist/generated/prisma/models/Run.d.ts +503 -1
- package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
- package/dist/generated/prisma/models/RunStep.js +1 -0
- package/dist/generated/prisma/models.d.ts +1 -0
- package/dist/help-index.json +102 -14
- package/dist/mcp/auth/access.d.ts +1 -0
- package/dist/mcp/context.d.ts +3 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/tools/agents.js +37 -7
- package/dist/mcp/tools/power.js +2 -2
- package/dist/mcp/tools/run-access.d.ts +7 -0
- package/dist/mcp/tools/run-access.js +14 -0
- package/dist/mcp/tools/run-steps.d.ts +16 -0
- package/dist/mcp/tools/run-steps.js +85 -0
- package/dist/mcp/tools/runs.js +10 -2
- package/dist/native-worker/gateway-server.d.ts +4 -1
- package/dist/native-worker/gateway-server.js +10 -2
- package/dist/native-worker/gateway.d.ts +3 -0
- package/dist/native-worker/gateway.js +8 -0
- package/dist/native-worker/http-server.d.ts +2 -4
- package/dist/native-worker/http-server.js +2 -8
- package/dist/providers/coding-proxy/memory-ledger.js +1 -0
- package/dist/providers/coding-proxy/prisma-ledger.js +9 -3
- package/dist/providers/coding-proxy/proxy.d.ts +26 -0
- package/dist/providers/coding-proxy/proxy.js +133 -42
- package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
- package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
- package/dist/providers/coding-proxy/runtime.d.ts +7 -0
- package/dist/providers/coding-proxy/runtime.js +55 -3
- package/dist/providers/coding-proxy/types.d.ts +5 -0
- package/dist/providers/engine/types.d.ts +26 -0
- package/dist/providers/executor/container.d.ts +14 -0
- package/dist/providers/executor/container.js +8 -1
- package/dist/providers/jobs/kubernetes.d.ts +8 -0
- package/dist/providers/jobs/kubernetes.js +22 -4
- package/dist/quickstart-images.json +1 -1
- package/dist/serve.js +26 -18
- package/dist/viewer/api-schema.d.ts +66 -0
- package/dist/viewer/api-schema.js +31 -0
- package/dist/viewer/openapi.js +2 -1
- package/dist/viewer/run-detail.d.ts +2 -0
- package/dist/viewer/run-detail.js +26 -1
- package/docs/claude-code-plugin.md +57 -13
- package/docs/coding-agent-setup.md +17 -0
- package/docs/coding-worker-isolation.md +16 -0
- package/docs/debugging-agent-runs.md +112 -0
- package/docs/getting-started-gke.md +28 -7
- package/docs/mcp-servers.md +9 -0
- package/docs/native-sandbox.md +6 -0
- package/docs/security-deployment.md +51 -0
- package/docs/viewer-api.md +21 -12
- package/docs/viewer-api.openapi.yml +78 -2
- package/help/admin-viewer.md +1 -0
- package/help/claude-code-run-pane.md +27 -4
- package/help/deploy-gke.md +5 -0
- package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
- package/help/low-power-mode.md +19 -8
- package/help/mcp.md +2 -0
- package/help/resume-runs-locally.md +4 -0
- package/help/run-steps.md +140 -0
- package/package.json +1 -1
- package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
- package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
- package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
- package/prisma/schema.prisma +52 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { loadDependencyCacheConfig } from "../../config/dependency-cache.js";
|
|
2
2
|
import { createSimulatorModelFetch, createSimulatorRegistryFetch, startupTestSimulators, } from "../../config/test-simulators.js";
|
|
3
3
|
import { logger } from "../../core/logger.js";
|
|
4
|
+
import { loadRunStepsLevel } from "../../core/run-steps/config.js";
|
|
4
5
|
import { REGISTRY_ADAPTERS } from "../../coding/registry/adapters.js";
|
|
5
6
|
import { CODING_PROXY_ALIAS, CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "../jobs/docker-isolation.js";
|
|
6
7
|
import { createBlobStore } from "../storage/index.js";
|
|
@@ -14,6 +15,7 @@ import { createMcpRoute } from "./mcp-route.js";
|
|
|
14
15
|
import { createMockUpstream, mockUpstreamFromEnv } from "./mock-upstream.js";
|
|
15
16
|
import { CodingProxy } from "./proxy.js";
|
|
16
17
|
import { PrismaProxyLedger } from "./prisma-ledger.js";
|
|
18
|
+
import { RunStepForwarder } from "./run-step-forwarder.js";
|
|
17
19
|
import { OsvAudit } from "./registry/audit.js";
|
|
18
20
|
import { PrismaRegistryStore } from "./registry/prisma-store.js";
|
|
19
21
|
import { DEFAULT_PLAN_MAX_ENTRIES, DEFAULT_PLAN_TIMEOUT_MS } from "./registry/plan.js";
|
|
@@ -71,6 +73,34 @@ export function loadMcpGatewayUrl(env) {
|
|
|
71
73
|
}
|
|
72
74
|
return url.href.replace(/\/+$/, "");
|
|
73
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* WARDBY_RUN_STEPS_URL: the control plane's run-steps listener (WARDBY_RUN_STEPS_LISTEN on the
|
|
78
|
+
* server), which the proxy posts coding runs' steps to. Unset or empty: coding runs record no steps.
|
|
79
|
+
* Anything but an http(s) origin without credentials, path, query or fragment stops start-up (the
|
|
80
|
+
* value is not echoed, so a credential in it never reaches a log).
|
|
81
|
+
*/
|
|
82
|
+
export function loadRunStepsUrl(env) {
|
|
83
|
+
const value = env.WARDBY_RUN_STEPS_URL?.trim();
|
|
84
|
+
if (!value)
|
|
85
|
+
return undefined;
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
url = new URL(value);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
url = undefined;
|
|
92
|
+
}
|
|
93
|
+
if (!url ||
|
|
94
|
+
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
95
|
+
url.username ||
|
|
96
|
+
url.password ||
|
|
97
|
+
url.pathname !== "/" ||
|
|
98
|
+
url.search ||
|
|
99
|
+
url.hash) {
|
|
100
|
+
throw new Error("WARDBY_RUN_STEPS_URL must be an http(s) URL without credentials, path, query or fragment");
|
|
101
|
+
}
|
|
102
|
+
return url.origin;
|
|
103
|
+
}
|
|
74
104
|
/** Starts the trusted proxy with the fixed worker-only Docker endpoint, plus the deny port the enforcement witness probes. */
|
|
75
105
|
export async function startConfiguredCodingProxy(options) {
|
|
76
106
|
const env = options.env ?? process.env;
|
|
@@ -86,6 +116,10 @@ export async function startConfiguredCodingProxy(options) {
|
|
|
86
116
|
: options.audit;
|
|
87
117
|
// Throws on an invalid setting, before anything listens.
|
|
88
118
|
const mcpGatewayUrl = loadMcpGatewayUrl(env);
|
|
119
|
+
// Throws on an invalid setting, before anything listens.
|
|
120
|
+
const runStepsUrl = loadRunStepsUrl(env);
|
|
121
|
+
// Throws on an invalid WARDBY_RUN_STEPS. At off no step text leaves the proxy.
|
|
122
|
+
const runStepsCeiling = loadRunStepsLevel(env);
|
|
89
123
|
// Throws (test_simulators_refused) on any refusal, before anything listens.
|
|
90
124
|
const simulators = startupTestSimulators("coding-proxy", env);
|
|
91
125
|
const buildPinnedFetch = options.createPinnedFetch ?? createPinnedProxyFetch;
|
|
@@ -152,6 +186,10 @@ export async function startConfiguredCodingProxy(options) {
|
|
|
152
186
|
logger.warn({ module: "coding-proxy-runtime", event: "proxy.dependency_cache_no_blob_store" }, "coding proxy: WARDBY_DEPENDENCY_CACHE=true but no BLOB_STORE is configured; the dependency cache is off");
|
|
153
187
|
}
|
|
154
188
|
const seal = modelRequestSeal;
|
|
189
|
+
// Coding runs' steps go to the control plane only when WARDBY_RUN_STEPS_URL is set and
|
|
190
|
+
// WARDBY_RUN_STEPS is not off; otherwise no
|
|
191
|
+
// submitter is passed and the proxy does no step extraction.
|
|
192
|
+
const steps = runStepsUrl && runStepsCeiling !== "off" ? new RunStepForwarder({ url: runStepsUrl }) : undefined;
|
|
155
193
|
const ledger = new PrismaProxyLedger(options.db);
|
|
156
194
|
const proxy = new CodingProxy({
|
|
157
195
|
ledger,
|
|
@@ -166,6 +204,8 @@ export async function startConfiguredCodingProxy(options) {
|
|
|
166
204
|
: {}),
|
|
167
205
|
// The run's first model request closes its dependency cache (no-op for a run with it off).
|
|
168
206
|
...(seal ? { onModelRequest: (runId) => seal.onModelRequest(runId) } : {}),
|
|
207
|
+
...(steps ? { steps } : {}),
|
|
208
|
+
runStepsCeiling,
|
|
169
209
|
});
|
|
170
210
|
const startServer = options.startServer ?? startCodingProxyServer;
|
|
171
211
|
let server;
|
|
@@ -182,6 +222,7 @@ export async function startConfiguredCodingProxy(options) {
|
|
|
182
222
|
}
|
|
183
223
|
catch (error) {
|
|
184
224
|
eviction?.stop();
|
|
225
|
+
await steps?.close();
|
|
185
226
|
throw error;
|
|
186
227
|
}
|
|
187
228
|
let deny;
|
|
@@ -192,15 +233,26 @@ export async function startConfiguredCodingProxy(options) {
|
|
|
192
233
|
// A proxy without its witness must not stay up: the launcher's gate would never be able to
|
|
193
234
|
// prove a policy is enforced, and every launch would fail at the gate instead of at startup.
|
|
194
235
|
eviction?.stop();
|
|
195
|
-
|
|
236
|
+
try {
|
|
237
|
+
await server.close();
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await steps?.close();
|
|
241
|
+
}
|
|
196
242
|
throw error;
|
|
197
243
|
}
|
|
198
244
|
return {
|
|
199
245
|
port: server.port,
|
|
200
246
|
close: async () => {
|
|
201
247
|
eviction?.stop();
|
|
202
|
-
|
|
203
|
-
|
|
248
|
+
try {
|
|
249
|
+
await deny.close();
|
|
250
|
+
await server.close();
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
// Last: requests are done, so this is the final chance to hand over queued steps (bounded).
|
|
254
|
+
await steps?.close();
|
|
255
|
+
}
|
|
204
256
|
},
|
|
205
257
|
};
|
|
206
258
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type CatalogEntry } from "../llm/catalog-types.js";
|
|
2
|
+
import type { RunStepsLevel } from "../../core/run-steps/config.js";
|
|
2
3
|
import type { ModelPricing } from "../llm/pricing-core.js";
|
|
3
4
|
import type { ProxyRefusalDetails } from "../../core/budget-events.js";
|
|
4
5
|
import type { McpToolPin } from "../../core/mcp-tools.js";
|
|
@@ -53,6 +54,8 @@ export interface ProxySession {
|
|
|
53
54
|
mcpTokenHash: string | null;
|
|
54
55
|
/** The run's MCP tools/call count so far. */
|
|
55
56
|
mcpCallCount: number;
|
|
57
|
+
/** The run's Run.runStepsLevel, copied at session creation; null on sessions from before run steps detail. */
|
|
58
|
+
runStepsLevel: RunStepsLevel | null;
|
|
56
59
|
}
|
|
57
60
|
export interface ProxyRequest {
|
|
58
61
|
id: string;
|
|
@@ -100,6 +103,8 @@ export interface CreateProxySessionInput {
|
|
|
100
103
|
mcpTools?: McpToolPin[];
|
|
101
104
|
/** SHA-256 (base64url) of the run's MCP token (mcpTokenHash in src/coding/mcp-token.ts). */
|
|
102
105
|
mcpTokenHash?: string;
|
|
106
|
+
/** The run's level, fixed at dispatch; absent counts as summary. */
|
|
107
|
+
runStepsLevel?: RunStepsLevel;
|
|
103
108
|
}
|
|
104
109
|
export interface ReserveProxyRequestInput {
|
|
105
110
|
id: string;
|
|
@@ -117,9 +117,35 @@ export interface EngineRunContext {
|
|
|
117
117
|
onText?: (delta: string) => void;
|
|
118
118
|
/** Called after each model turn that completed without error; the engine awaits it. */
|
|
119
119
|
onProgress?: (progress: EngineProgress) => Promise<void>;
|
|
120
|
+
/**
|
|
121
|
+
* Receives each model turn's text and tool calls, then the turn's tool results, as the engine saw
|
|
122
|
+
* them. The runner turns them into the run's steps (src/core/run-steps); the engine never imports
|
|
123
|
+
* that code, so the sandbox worker image does not ship it. Synchronous and not awaited. A replay
|
|
124
|
+
* delivers the same call ids again; the writer drops the repeats.
|
|
125
|
+
*/
|
|
126
|
+
onTurn?: (event: EngineTurnEvent) => void;
|
|
120
127
|
/** See StepRunner. Optional; the in-process executor leaves it unset. */
|
|
121
128
|
step?: StepRunner;
|
|
122
129
|
}
|
|
130
|
+
/** What EngineRunContext.onTurn receives: a turn's output, then (if it called tools) their results in call order. */
|
|
131
|
+
export type EngineTurnEvent = {
|
|
132
|
+
kind: "turn";
|
|
133
|
+
/** 1-based turn number. */
|
|
134
|
+
turn: number;
|
|
135
|
+
text: string;
|
|
136
|
+
toolCalls: readonly {
|
|
137
|
+
id: string;
|
|
138
|
+
name: string;
|
|
139
|
+
argsJson: string;
|
|
140
|
+
}[];
|
|
141
|
+
} | {
|
|
142
|
+
kind: "results";
|
|
143
|
+
turn: number;
|
|
144
|
+
toolCalls: readonly {
|
|
145
|
+
id: string;
|
|
146
|
+
}[];
|
|
147
|
+
results: readonly string[];
|
|
148
|
+
};
|
|
123
149
|
export type EngineStatus = "succeeded" | "budget_exhausted" | "refused" | "failed";
|
|
124
150
|
export interface EngineResult {
|
|
125
151
|
status: EngineStatus;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RunStepsLevel } from "../../core/run-steps/config.js";
|
|
1
2
|
import { type PackageAuditEntry, type PackageAuditMode } from "../../coding/registry/audit-mode.js";
|
|
2
3
|
import type { PrismaClient } from "#prisma";
|
|
3
4
|
import type { RunTranscriptRecorder } from "../../coding/transcript/recorder.js";
|
|
@@ -101,6 +102,8 @@ export type StoredDescriptionBaseline = DescriptionBaseline & {
|
|
|
101
102
|
legacySource?: LegacyDescriptionSource;
|
|
102
103
|
};
|
|
103
104
|
export interface ContainerRunSnapshot {
|
|
105
|
+
/** Run.runStepsLevel, fixed at dispatch; absent on older snapshots = summary. */
|
|
106
|
+
runStepsLevel?: RunStepsLevel | null;
|
|
104
107
|
runId: string;
|
|
105
108
|
agentId: string;
|
|
106
109
|
status: string;
|
|
@@ -338,6 +341,8 @@ export interface CodingSessionController {
|
|
|
338
341
|
mcpTools?: McpToolPin[];
|
|
339
342
|
/** The hash of the run's MCP token (src/coding/mcp-token.ts); the token itself stays in the vault. */
|
|
340
343
|
mcpTokenHash?: string;
|
|
344
|
+
/** The run's run-steps level (Run.runStepsLevel); the proxy reads tool output only at detail. */
|
|
345
|
+
runStepsLevel?: RunStepsLevel;
|
|
341
346
|
}): Promise<{
|
|
342
347
|
id: string;
|
|
343
348
|
capability: string;
|
|
@@ -757,3 +762,12 @@ export declare function describeFailure(error: unknown, depth?: number): string;
|
|
|
757
762
|
export declare const MCP_PINS_UNAVAILABLE_ERROR = "coding_mcp_pins_unavailable";
|
|
758
763
|
/** Its category: the run's error is coding_failure_mcp_pins_unavailable:<diagnostic id>. */
|
|
759
764
|
export declare const MCP_PINS_UNAVAILABLE_CATEGORY = "mcp_pins_unavailable";
|
|
765
|
+
/** Low-power mode: the native gateway serving a run's MCP tools did not wake; nothing launched. */
|
|
766
|
+
export declare const MCP_GATEWAY_UNAVAILABLE_CATEGORY = "mcp_gateway_unavailable";
|
|
767
|
+
/**
|
|
768
|
+
* Error-code prefixes that name their category outright, checked before the
|
|
769
|
+
* substring heuristics below. Those heuristics filed every `github_*` error
|
|
770
|
+
* (a failed PR lookup or create after the push, a token mint) under
|
|
771
|
+
* "workspace" because "github" contains "git".
|
|
772
|
+
*/
|
|
773
|
+
export declare const CATEGORY_BY_PREFIX: ReadonlyArray<readonly [prefix: string, category: string]>;
|
|
@@ -5,6 +5,7 @@ import { join, resolve, sep } from "node:path";
|
|
|
5
5
|
import { withBaseCommit } from "../../coding/base-commit.js";
|
|
6
6
|
import { mcpTokenHash, mintMcpToken } from "../../coding/mcp-token.js";
|
|
7
7
|
import { isCodexCoding } from "../../core/mcp-tools.js";
|
|
8
|
+
import { CODING_MCP_GATEWAY_UNAVAILABLE } from "../../core/power.js";
|
|
8
9
|
import { codeProviderOf } from "../../coding/code-host.js";
|
|
9
10
|
import { withPullRequestDescription } from "../../coding/pr-description-note.js";
|
|
10
11
|
import { withLinkedIssue } from "../../coding/linked-issue-note.js";
|
|
@@ -185,6 +186,7 @@ export class PrismaContainerExecutionStore {
|
|
|
185
186
|
: null;
|
|
186
187
|
return {
|
|
187
188
|
runId: row.id,
|
|
189
|
+
runStepsLevel: row.runStepsLevel,
|
|
188
190
|
agentId: row.agentId,
|
|
189
191
|
status: row.status,
|
|
190
192
|
agentKind: row.agent.kind,
|
|
@@ -882,6 +884,7 @@ export class ContainerExecutor {
|
|
|
882
884
|
budgetUsd: run.budgetUsd,
|
|
883
885
|
terms: this.modelTerms(run),
|
|
884
886
|
...(mcpToken ? { mcpTools, mcpTokenHash: mcpTokenHash(mcpToken) } : {}),
|
|
887
|
+
...(run.runStepsLevel ? { runStepsLevel: run.runStepsLevel } : {}),
|
|
885
888
|
});
|
|
886
889
|
spendEnabled = true;
|
|
887
890
|
sessionId = session.id;
|
|
@@ -2485,13 +2488,15 @@ function safeError(error) {
|
|
|
2485
2488
|
export const MCP_PINS_UNAVAILABLE_ERROR = "coding_mcp_pins_unavailable";
|
|
2486
2489
|
/** Its category: the run's error is coding_failure_mcp_pins_unavailable:<diagnostic id>. */
|
|
2487
2490
|
export const MCP_PINS_UNAVAILABLE_CATEGORY = "mcp_pins_unavailable";
|
|
2491
|
+
/** Low-power mode: the native gateway serving a run's MCP tools did not wake; nothing launched. */
|
|
2492
|
+
export const MCP_GATEWAY_UNAVAILABLE_CATEGORY = "mcp_gateway_unavailable";
|
|
2488
2493
|
/**
|
|
2489
2494
|
* Error-code prefixes that name their category outright, checked before the
|
|
2490
2495
|
* substring heuristics below. Those heuristics filed every `github_*` error
|
|
2491
2496
|
* (a failed PR lookup or create after the push, a token mint) under
|
|
2492
2497
|
* "workspace" because "github" contains "git".
|
|
2493
2498
|
*/
|
|
2494
|
-
const CATEGORY_BY_PREFIX = [
|
|
2499
|
+
export const CATEGORY_BY_PREFIX = [
|
|
2495
2500
|
// Claude Code reached the agent's turn limit (codingProfile.maxTurns) before finishing.
|
|
2496
2501
|
["job_coding_turn_limit", "turn_limit"],
|
|
2497
2502
|
// A service sidecar never became ready (src/providers/jobs/kubernetes.ts).
|
|
@@ -2523,6 +2528,8 @@ const CATEGORY_BY_PREFIX = [
|
|
|
2523
2528
|
["vcs_", "workspace"],
|
|
2524
2529
|
// The agent's MCP tool pins could not be read at launch: nothing was launched.
|
|
2525
2530
|
[MCP_PINS_UNAVAILABLE_ERROR, MCP_PINS_UNAVAILABLE_CATEGORY],
|
|
2531
|
+
// Low-power mode: the native gateway serving the run's MCP tools did not wake (jobs/kubernetes.ts).
|
|
2532
|
+
[CODING_MCP_GATEWAY_UNAVAILABLE, MCP_GATEWAY_UNAVAILABLE_CATEGORY],
|
|
2526
2533
|
// Local repositories (LocalRepoError codes): the same family as the vcs_ errors.
|
|
2527
2534
|
["local_", "workspace"],
|
|
2528
2535
|
["git_", "workspace"],
|
|
@@ -100,6 +100,14 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
|
|
|
100
100
|
hasCapacityFor(spec: JobSpec): Promise<boolean>;
|
|
101
101
|
private readonly warnedOnce;
|
|
102
102
|
private warnOnce;
|
|
103
|
+
/**
|
|
104
|
+
* Wakes what the run needs before anything is created: the coding proxy always, and the native
|
|
105
|
+
* gateway too for a run with MCP tools (its MCP calls go proxy -> gateway; without a gateway the
|
|
106
|
+
* worker would start with its MCP tools missing). Both wake at once, so a cold start waits for
|
|
107
|
+
* the slower of the two, not their sum. A gateway that does not wake fails the launch with
|
|
108
|
+
* coding_mcp_gateway_unavailable; the proxy's own wake error is reported first.
|
|
109
|
+
*/
|
|
110
|
+
private wakeForRun;
|
|
103
111
|
launch(spec: JobSpec): Promise<JobHandle>;
|
|
104
112
|
status(handle: JobHandle): Promise<JobStatus>;
|
|
105
113
|
collect(handle: JobHandle): Promise<JobResult>;
|
|
@@ -29,7 +29,7 @@ import { finished } from "node:stream/promises";
|
|
|
29
29
|
import { createHash } from "node:crypto";
|
|
30
30
|
import { isIP } from "node:net";
|
|
31
31
|
import { logger } from "../../core/logger.js";
|
|
32
|
-
import { DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
|
|
32
|
+
import { CODING_MCP_GATEWAY_UNAVAILABLE, DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
|
|
33
33
|
import { MAX_CODING_ARTIFACT_BYTES, MAX_CODING_INPUT_BYTES, MAX_DEBUG_TRACE_LINE_BYTES, parseCodingWorkerOutputJson, } from "../../coding/protocol.js";
|
|
34
34
|
import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
|
|
35
35
|
import { parseWorkerDiagnosticLine } from "./docker.js";
|
|
@@ -500,6 +500,25 @@ export class KubernetesJobLauncher {
|
|
|
500
500
|
this.warnedOnce.add(message);
|
|
501
501
|
this.warn(message);
|
|
502
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* Wakes what the run needs before anything is created: the coding proxy always, and the native
|
|
505
|
+
* gateway too for a run with MCP tools (its MCP calls go proxy -> gateway; without a gateway the
|
|
506
|
+
* worker would start with its MCP tools missing). Both wake at once, so a cold start waits for
|
|
507
|
+
* the slower of the two, not their sum. A gateway that does not wake fails the launch with
|
|
508
|
+
* coding_mcp_gateway_unavailable; the proxy's own wake error is reported first.
|
|
509
|
+
*/
|
|
510
|
+
async wakeForRun(spec) {
|
|
511
|
+
const opts = { timeoutMs: wakeTimeoutMs(this.readyTimeoutMs), runId: spec.runId };
|
|
512
|
+
const [proxy, gateway] = await Promise.allSettled([
|
|
513
|
+
this.power.ensureAwake("proxy", opts),
|
|
514
|
+
spec.mcpTools ? this.power.ensureAwake("gateway", opts) : Promise.resolve(),
|
|
515
|
+
]);
|
|
516
|
+
if (proxy.status === "rejected")
|
|
517
|
+
throw proxy.reason;
|
|
518
|
+
if (gateway.status === "rejected") {
|
|
519
|
+
throw new Error(`${CODING_MCP_GATEWAY_UNAVAILABLE}: the native gateway, which serves this run's MCP tools, did not wake.`, { cause: gateway.reason });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
503
522
|
async launch(spec) {
|
|
504
523
|
try {
|
|
505
524
|
validateKubernetesSpec(spec);
|
|
@@ -516,9 +535,8 @@ export class KubernetesJobLauncher {
|
|
|
516
535
|
}
|
|
517
536
|
// Low-power mode: the proxy may be scaled to zero; wake it before the preflight reads it. Guarded
|
|
518
537
|
// (not just a no-op await) so mode off keeps launch() joining an in-flight warm-up synchronously.
|
|
519
|
-
if (this.power.enabled)
|
|
520
|
-
await this.
|
|
521
|
-
}
|
|
538
|
+
if (this.power.enabled)
|
|
539
|
+
await this.wakeForRun(spec);
|
|
522
540
|
await this.runPreflight();
|
|
523
541
|
const names = kubernetesRunNames(spec.runId);
|
|
524
542
|
const handle = { backend: BACKEND, id: `${this.namespace}/${names.token}` };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"runtime":"ghcr.io/wardby/wardby/wardby-runtime@sha256:
|
|
1
|
+
{"runtime":"ghcr.io/wardby/wardby/wardby-runtime@sha256:d5cb8fcfbfd43c230418374e9a5e203528b7d7fd372c01c8545284bc261edd58","worker":"ghcr.io/wardby/wardby/wardby-coding-worker@sha256:943a4023bcdc86d84fb923ebd97e3b0b0b5c0055ea81542cb9972ddc6ee36e69","claudeWorker":"ghcr.io/wardby/wardby/wardby-claude-coding-worker@sha256:861ef8f23bd70c329cde7013191371737d266b854678942d328906b29242d08f","claudeToolRunner":"ghcr.io/wardby/wardby/wardby-claude-tool-runner@sha256:247a870862a3b20d02b6d0a2a87ef7b3d2ae57a30478b6d517ae58535bf54260","workerNodePython":"ghcr.io/wardby/wardby/wardby-coding-worker-node-python@sha256:9684306a84da26ea057b6ccdaaa73f7377820a74bb30fe1740afb8c734da4300","claudeToolRunnerNodePython":"ghcr.io/wardby/wardby/wardby-claude-tool-runner-node-python@sha256:71bd16fd9635eee62b561771da9568120db25ec259807aa070c2eb7865dbbf0f","nativeWorker":"ghcr.io/wardby/wardby/wardby-native-worker@sha256:33606de7284c11dee354a44b6563cc7999fd135d71545c1e63244d867f1a7b2c","driver":"ghcr.io/wardby/wardby/wardby-coding-worker-driver@sha256:281a7cabc53d6ff1977d1b68c1420b2ef598f90da3995cb13df27395a58a60ef"}
|
package/dist/serve.js
CHANGED
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `wardby serve`: the complete wardby process. Runs the MCP server, the
|
|
3
|
-
* scheduler, and the reconciler together on ONE provider set and ONE
|
|
4
|
-
* executor, which is what a single-container deployment (Cloud Run, a lone
|
|
5
|
-
* VM) needs - `wardby mcp` alone never fires a schedule.
|
|
6
|
-
*
|
|
7
|
-
* Why one executor: DbosExecutor is a per-process singleton and refuses to
|
|
8
|
-
* launch twice under different ids (providers/executor/dbos.ts), and its
|
|
9
|
-
* recovery decisions key on that id. Building providers once and sharing
|
|
10
|
-
* them is the whole trick; it also gives scheduled native runs the same
|
|
11
|
-
* `providers.executor` wiring MCP-triggered runs get, so sub-agent dispatch
|
|
12
|
-
* from a scheduled run works (it does not from `wardby scheduler`).
|
|
13
|
-
*
|
|
14
|
-
* Why the scheduler and reconciler can run in every replica: the scheduler
|
|
15
|
-
* elects one ticker via a Postgres lease and enforces at-most-once with row
|
|
16
|
-
* locks regardless (core/scheduler.ts); the reconciler is deliberately not
|
|
17
|
-
* lease-gated and is safe under concurrency (core/reconciler.ts).
|
|
18
|
-
*/
|
|
19
1
|
import { loadCodingConcurrencyConfig, loadMcpConfig } from "./config/providers.js";
|
|
20
2
|
import { drainCodingQueue } from "./core/coding-queue.js";
|
|
21
3
|
import { prisma } from "./core/db.js";
|
|
@@ -28,6 +10,8 @@ import { markRunFailedFromExecutorError } from "./core/dispatch.js";
|
|
|
28
10
|
import { startScheduler } from "./core/scheduler.js";
|
|
29
11
|
import { drainDeferredRuns } from "./providers/executor/deferred.js";
|
|
30
12
|
import { resolveDeferredCodingRun } from "./core/deferred-coding.js";
|
|
13
|
+
import { RunStepSweeper } from "./core/run-steps/sweep.js";
|
|
14
|
+
import { parseRunStepsListen, startRunStepIngress } from "./core/run-steps/ingress.js";
|
|
31
15
|
import { buildMcpProviders, startMcp } from "./mcp/index.js";
|
|
32
16
|
const serveLog = logger.child({ module: "serve" });
|
|
33
17
|
export async function startServe(options = {}) {
|
|
@@ -39,6 +23,8 @@ export async function startServe(options = {}) {
|
|
|
39
23
|
// Parsed before anything starts, so a malformed CODING_MAX_CONCURRENT or
|
|
40
24
|
// CODING_QUEUE_TIMEOUT_SEC fails fast instead of after HTTP is listening.
|
|
41
25
|
const concurrency = loadCodingConcurrencyConfig();
|
|
26
|
+
// Run steps from the coding proxy (POST /internal/run-steps); parsed early so a bad value stops start-up.
|
|
27
|
+
const runStepsListen = parseRunStepsListen(process.env.WARDBY_RUN_STEPS_LISTEN);
|
|
42
28
|
const providers = options.providers ?? buildMcpProviders().providers;
|
|
43
29
|
const power = providers.power ?? DISABLED_POWER;
|
|
44
30
|
if (power.enabled) {
|
|
@@ -48,6 +34,21 @@ export async function startServe(options = {}) {
|
|
|
48
34
|
}, (err) => serveLog.warn({ err }, "low_power_access_check_failed"));
|
|
49
35
|
}
|
|
50
36
|
const mcp = await startMcp({ providers, schedulerAttached: true });
|
|
37
|
+
// Its own port (not the MCP listener's): only the coding proxy reaches it.
|
|
38
|
+
let runStepIngress;
|
|
39
|
+
if (runStepsListen && providers.runSteps) {
|
|
40
|
+
try {
|
|
41
|
+
runStepIngress = await startRunStepIngress(runStepsListen, { db: prisma, sink: providers.runSteps });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
await mcp.close();
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
serveLog.info({ host: runStepsListen.host, port: runStepsListen.port }, "run steps listener started");
|
|
48
|
+
}
|
|
49
|
+
else if (runStepsListen) {
|
|
50
|
+
serveLog.warn("WARDBY_RUN_STEPS_LISTEN is set but this server has no run-steps writer; not listening");
|
|
51
|
+
}
|
|
51
52
|
const reviewHosts = providers.reviewHosts;
|
|
52
53
|
const reconciler = startReconciler({
|
|
53
54
|
executor: providers.executor,
|
|
@@ -67,6 +68,8 @@ export async function startServe(options = {}) {
|
|
|
67
68
|
});
|
|
68
69
|
const notifications = await startNotifications({ db: prisma, chat: providers.chat ?? {} });
|
|
69
70
|
const selfDefects = { db: prisma, issueTrackers: providers.issueTrackers };
|
|
71
|
+
// Expired run steps (RUN_STEP_RETENTION_DAYS), from the scheduler leader only.
|
|
72
|
+
const runStepSweeper = new RunStepSweeper(prisma);
|
|
70
73
|
const scheduler = startScheduler({
|
|
71
74
|
executor: providers.executor,
|
|
72
75
|
scope: options.scope,
|
|
@@ -82,6 +85,7 @@ export async function startServe(options = {}) {
|
|
|
82
85
|
});
|
|
83
86
|
// Scale idle coding-proxy/native-gateway Deployments to zero (no-op unless WARDBY_LOW_POWER=1).
|
|
84
87
|
await power.sleepIfIdle().catch((err) => serveLog.warn({ err }, "power_sleep_failed"));
|
|
88
|
+
await runStepSweeper.maybeSweep();
|
|
85
89
|
},
|
|
86
90
|
});
|
|
87
91
|
return {
|
|
@@ -92,6 +96,10 @@ export async function startServe(options = {}) {
|
|
|
92
96
|
scheduler.stop();
|
|
93
97
|
reconciler.stop();
|
|
94
98
|
notifications.stop();
|
|
99
|
+
// Before mcp.close(), which flushes the run-steps writer: no batch arrives after the flush.
|
|
100
|
+
const ingress = runStepIngress;
|
|
101
|
+
if (ingress)
|
|
102
|
+
await new Promise((resolve) => ingress.close(() => resolve()));
|
|
95
103
|
await mcp.close();
|
|
96
104
|
},
|
|
97
105
|
};
|
|
@@ -320,6 +320,21 @@ export declare const GraphSnapshotSchema: z.ZodObject<{
|
|
|
320
320
|
}, z.core.$strip>>;
|
|
321
321
|
}, z.core.$strip>;
|
|
322
322
|
}, z.core.$strip>;
|
|
323
|
+
/**
|
|
324
|
+
* One recorded run step (docs/viewer-api.md): a tool call or what the model said, one line. Stored
|
|
325
|
+
* already redacted (src/core/run-steps/writer.ts); a step's call id and detail text are never sent.
|
|
326
|
+
*/
|
|
327
|
+
export declare const RunStepSchema: z.ZodObject<{
|
|
328
|
+
seq: z.ZodNumber;
|
|
329
|
+
at: z.ZodString;
|
|
330
|
+
kind: z.ZodEnum<{
|
|
331
|
+
tool: "tool";
|
|
332
|
+
said: "said";
|
|
333
|
+
}>;
|
|
334
|
+
tool: z.ZodNullable<z.ZodString>;
|
|
335
|
+
summary: z.ZodString;
|
|
336
|
+
status: z.ZodNullable<z.ZodString>;
|
|
337
|
+
}, z.core.$strip>;
|
|
323
338
|
export declare const RunDetailSchema: z.ZodObject<{
|
|
324
339
|
id: z.ZodString;
|
|
325
340
|
parentRunId: z.ZodNullable<z.ZodString>;
|
|
@@ -429,6 +444,19 @@ export declare const RunDetailSchema: z.ZodObject<{
|
|
|
429
444
|
error: z.ZodNullable<z.ZodString>;
|
|
430
445
|
finalText: z.ZodNullable<z.ZodString>;
|
|
431
446
|
childRunIds: z.ZodArray<z.ZodString>;
|
|
447
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
448
|
+
seq: z.ZodNumber;
|
|
449
|
+
at: z.ZodString;
|
|
450
|
+
kind: z.ZodEnum<{
|
|
451
|
+
tool: "tool";
|
|
452
|
+
said: "said";
|
|
453
|
+
}>;
|
|
454
|
+
tool: z.ZodNullable<z.ZodString>;
|
|
455
|
+
summary: z.ZodString;
|
|
456
|
+
status: z.ZodNullable<z.ZodString>;
|
|
457
|
+
}, z.core.$strip>>;
|
|
458
|
+
stepsOmitted: z.ZodNumber;
|
|
459
|
+
stepsDropped: z.ZodNumber;
|
|
432
460
|
coding: z.ZodNullable<z.ZodObject<{
|
|
433
461
|
provider: z.ZodString;
|
|
434
462
|
repository: z.ZodString;
|
|
@@ -484,6 +512,18 @@ export declare const ViewerEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
484
512
|
issue_status: "issue_status";
|
|
485
513
|
host_check: "host_check";
|
|
486
514
|
}>;
|
|
515
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
516
|
+
kind: z.ZodLiteral<"step">;
|
|
517
|
+
runId: z.ZodString;
|
|
518
|
+
seq: z.ZodNumber;
|
|
519
|
+
stepKind: z.ZodEnum<{
|
|
520
|
+
tool: "tool";
|
|
521
|
+
said: "said";
|
|
522
|
+
}>;
|
|
523
|
+
tool: z.ZodNullable<z.ZodString>;
|
|
524
|
+
summary: z.ZodString;
|
|
525
|
+
status: z.ZodNullable<z.ZodString>;
|
|
526
|
+
at: z.ZodString;
|
|
487
527
|
}, z.core.$strip>], "kind">;
|
|
488
528
|
export type RunTriggerInfo = z.infer<typeof RunTriggerSchema>;
|
|
489
529
|
export type Outcome = z.infer<typeof OutcomeSchema>;
|
|
@@ -491,6 +531,7 @@ export type ServiceStatus = z.infer<typeof ServiceStatusSchema>;
|
|
|
491
531
|
export type GraphRun = z.infer<typeof GraphRunSchema>;
|
|
492
532
|
export type GraphSnapshot = z.infer<typeof GraphSnapshotSchema>;
|
|
493
533
|
export type RunDetail = z.infer<typeof RunDetailSchema>;
|
|
534
|
+
export type RunStepView = z.infer<typeof RunStepSchema>;
|
|
494
535
|
export type ViewerEvent = z.infer<typeof ViewerEventSchema>;
|
|
495
536
|
/** How this deployment runs coding jobs, for the desktop viewer's Infrastructure view (GET /admin/api/infra). */
|
|
496
537
|
export declare const InfraInfoSchema: z.ZodObject<{
|
|
@@ -758,6 +799,19 @@ export declare const VIEWER_SCHEMAS: {
|
|
|
758
799
|
error: z.ZodNullable<z.ZodString>;
|
|
759
800
|
finalText: z.ZodNullable<z.ZodString>;
|
|
760
801
|
childRunIds: z.ZodArray<z.ZodString>;
|
|
802
|
+
steps: z.ZodArray<z.ZodObject<{
|
|
803
|
+
seq: z.ZodNumber;
|
|
804
|
+
at: z.ZodString;
|
|
805
|
+
kind: z.ZodEnum<{
|
|
806
|
+
tool: "tool";
|
|
807
|
+
said: "said";
|
|
808
|
+
}>;
|
|
809
|
+
tool: z.ZodNullable<z.ZodString>;
|
|
810
|
+
summary: z.ZodString;
|
|
811
|
+
status: z.ZodNullable<z.ZodString>;
|
|
812
|
+
}, z.core.$strip>>;
|
|
813
|
+
stepsOmitted: z.ZodNumber;
|
|
814
|
+
stepsDropped: z.ZodNumber;
|
|
761
815
|
coding: z.ZodNullable<z.ZodObject<{
|
|
762
816
|
provider: z.ZodString;
|
|
763
817
|
repository: z.ZodString;
|
|
@@ -813,6 +867,18 @@ export declare const VIEWER_SCHEMAS: {
|
|
|
813
867
|
issue_status: "issue_status";
|
|
814
868
|
host_check: "host_check";
|
|
815
869
|
}>;
|
|
870
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
871
|
+
kind: z.ZodLiteral<"step">;
|
|
872
|
+
runId: z.ZodString;
|
|
873
|
+
seq: z.ZodNumber;
|
|
874
|
+
stepKind: z.ZodEnum<{
|
|
875
|
+
tool: "tool";
|
|
876
|
+
said: "said";
|
|
877
|
+
}>;
|
|
878
|
+
tool: z.ZodNullable<z.ZodString>;
|
|
879
|
+
summary: z.ZodString;
|
|
880
|
+
status: z.ZodNullable<z.ZodString>;
|
|
881
|
+
at: z.ZodString;
|
|
816
882
|
}, z.core.$strip>], "kind">;
|
|
817
883
|
readonly "infra-info": z.ZodObject<{
|
|
818
884
|
launcher: z.ZodEnum<{
|
|
@@ -123,10 +123,30 @@ export const GraphSnapshotSchema = z.object({
|
|
|
123
123
|
runs: z.array(GraphRunSchema),
|
|
124
124
|
spend: z.object({ todayUsd: z.number(), groups: z.array(BudgetGroupSpendSchema) }),
|
|
125
125
|
});
|
|
126
|
+
/**
|
|
127
|
+
* One recorded run step (docs/viewer-api.md): a tool call or what the model said, one line. Stored
|
|
128
|
+
* already redacted (src/core/run-steps/writer.ts); a step's call id and detail text are never sent.
|
|
129
|
+
*/
|
|
130
|
+
export const RunStepSchema = z.object({
|
|
131
|
+
seq: z.number().int(),
|
|
132
|
+
at: z.string(),
|
|
133
|
+
kind: z.enum(["tool", "said"]),
|
|
134
|
+
/** The tool's display name (`Bash`, `Edit`, `mcp:<tool>`); null for said. */
|
|
135
|
+
tool: z.string().nullable(),
|
|
136
|
+
summary: z.string(),
|
|
137
|
+
/** `ok`, `error` or `exit N` once the result is back; null until then and for said. */
|
|
138
|
+
status: z.string().nullable(),
|
|
139
|
+
});
|
|
126
140
|
export const RunDetailSchema = GraphRunSchema.extend({
|
|
127
141
|
error: z.string().nullable(),
|
|
128
142
|
finalText: z.string().nullable(),
|
|
129
143
|
childRunIds: z.array(z.string()),
|
|
144
|
+
/** The run's newest steps (at most 200), oldest first; see `stepsOmitted` for earlier ones. */
|
|
145
|
+
steps: z.array(RunStepSchema),
|
|
146
|
+
/** Earlier recorded steps not included in `steps`. */
|
|
147
|
+
stepsOmitted: z.number().int(),
|
|
148
|
+
/** Steps past the per-run limit, never recorded. */
|
|
149
|
+
stepsDropped: z.number().int(),
|
|
130
150
|
coding: z
|
|
131
151
|
.object({
|
|
132
152
|
provider: z.string(),
|
|
@@ -170,6 +190,17 @@ export const ViewerEventSchema = z.discriminatedUnion("kind", [
|
|
|
170
190
|
/** Which table changed (the viewer NOTIFY trigger names them). */
|
|
171
191
|
source: z.enum(["pull_request", "host_status", "issue_status", "host_check"]),
|
|
172
192
|
}),
|
|
193
|
+
/** A run recorded a step, or a step's status arrived (the RunStep NOTIFY trigger). */
|
|
194
|
+
z.object({
|
|
195
|
+
kind: z.literal("step"),
|
|
196
|
+
runId: z.string(),
|
|
197
|
+
seq: RunStepSchema.shape.seq,
|
|
198
|
+
stepKind: RunStepSchema.shape.kind,
|
|
199
|
+
tool: RunStepSchema.shape.tool,
|
|
200
|
+
summary: RunStepSchema.shape.summary,
|
|
201
|
+
status: RunStepSchema.shape.status,
|
|
202
|
+
at: RunStepSchema.shape.at,
|
|
203
|
+
}),
|
|
173
204
|
]);
|
|
174
205
|
/** How this deployment runs coding jobs, for the desktop viewer's Infrastructure view (GET /admin/api/infra). */
|
|
175
206
|
export const InfraInfoSchema = z.object({
|
package/dist/viewer/openapi.js
CHANGED
|
@@ -59,7 +59,7 @@ export const VIEWER_ROUTES = [
|
|
|
59
59
|
path: "/admin/api/runs/{id}",
|
|
60
60
|
operationId: "getRun",
|
|
61
61
|
summary: "Run detail",
|
|
62
|
-
description: "Full detail for one run, including final text.",
|
|
62
|
+
description: "Full detail for one run, including final text and its newest steps.",
|
|
63
63
|
parameters: [{ name: "id", in: "path", required: true, description: "Run id.", schema: { type: "string" } }],
|
|
64
64
|
responses: {
|
|
65
65
|
"200": { description: "The run.", content: { "application/json": { schema: ref("run-detail") } } },
|
|
@@ -85,6 +85,7 @@ export const VIEWER_ROUTES = [
|
|
|
85
85
|
"- `retry: 3000`: suggested client reconnect delay in milliseconds.",
|
|
86
86
|
'- `event: hello`: sent first. Data is `{"connected": <bool>}`, whether the server\'s live-event connection is up.',
|
|
87
87
|
"- `event: run`, `event: service`, `event: outcome`: a run, coding-run service, or outcome changed. Each carries an `id:` line. Payloads match the `viewer-event` schema (see `x-event-schema`).",
|
|
88
|
+
"- `event: step`: a run recorded a step, or a step's status arrived. Carries an `id:` line. Data: the step's `seq`, `stepKind` (`tool` or `said`), `tool`, `summary` (redacted when it was recorded), `status` and `at`, never tool output.",
|
|
88
89
|
'- `event: status`: the server\'s live-event connection changed. Data is `{"connected": <bool>}`.',
|
|
89
90
|
"- `event: resync`: the live-event connection is up (again). Events may have been missed: refetch the graph.",
|
|
90
91
|
`- \`: ping\`: comment sent every ${SSE_HEARTBEAT_MS / 1000} seconds to keep the connection open.`,
|
|
@@ -2,4 +2,6 @@
|
|
|
2
2
|
import type { PrismaClient } from "#prisma";
|
|
3
3
|
import type { RunDetail } from "./api-schema.js";
|
|
4
4
|
import { type IssueSites } from "./graph.js";
|
|
5
|
+
/** The most steps a run detail carries: the newest, oldest first. */
|
|
6
|
+
export declare const RUN_DETAIL_STEPS = 200;
|
|
5
7
|
export declare function loadRunDetail(db: PrismaClient, runId: string, issueSites?: IssueSites): Promise<RunDetail | null>;
|
|
@@ -11,16 +11,19 @@ function serviceDetails(snapshot) {
|
|
|
11
11
|
envNames: isRecord(s.testEnv) ? Object.keys(s.testEnv).sort() : [],
|
|
12
12
|
}));
|
|
13
13
|
}
|
|
14
|
+
/** The most steps a run detail carries: the newest, oldest first. */
|
|
15
|
+
export const RUN_DETAIL_STEPS = 200;
|
|
14
16
|
export async function loadRunDetail(db, runId, issueSites = {}) {
|
|
15
17
|
const [graphRun] = await loadGraphRuns(db, [runId], issueSites);
|
|
16
18
|
if (!graphRun)
|
|
17
19
|
return null;
|
|
18
|
-
const [row, children] = await Promise.all([
|
|
20
|
+
const [row, children, newest, stepCount] = await Promise.all([
|
|
19
21
|
db.run.findUniqueOrThrow({
|
|
20
22
|
where: { id: runId },
|
|
21
23
|
select: {
|
|
22
24
|
error: true,
|
|
23
25
|
finalText: true,
|
|
26
|
+
stepsDropped: true,
|
|
24
27
|
codingRun: {
|
|
25
28
|
select: {
|
|
26
29
|
provider: true,
|
|
@@ -39,13 +42,35 @@ export async function loadRunDetail(db, runId, issueSites = {}) {
|
|
|
39
42
|
select: { id: true },
|
|
40
43
|
orderBy: [{ startedAt: "asc" }, { id: "asc" }],
|
|
41
44
|
}),
|
|
45
|
+
// Stored already redacted; the call id and the detail text are never selected.
|
|
46
|
+
db.runStep.findMany({
|
|
47
|
+
where: { runId },
|
|
48
|
+
orderBy: { seq: "desc" },
|
|
49
|
+
take: RUN_DETAIL_STEPS,
|
|
50
|
+
select: { seq: true, at: true, kind: true, tool: true, summary: true, status: true },
|
|
51
|
+
}),
|
|
52
|
+
db.runStep.count({ where: { runId } }),
|
|
42
53
|
]);
|
|
54
|
+
const steps = newest
|
|
55
|
+
.reverse()
|
|
56
|
+
.filter((s) => s.kind === "tool" || s.kind === "said")
|
|
57
|
+
.map((s) => ({
|
|
58
|
+
seq: s.seq,
|
|
59
|
+
at: s.at.toISOString(),
|
|
60
|
+
kind: s.kind,
|
|
61
|
+
tool: s.tool,
|
|
62
|
+
summary: s.summary,
|
|
63
|
+
status: s.status,
|
|
64
|
+
}));
|
|
43
65
|
const coding = row.codingRun;
|
|
44
66
|
return {
|
|
45
67
|
...graphRun,
|
|
46
68
|
error: row.error,
|
|
47
69
|
finalText: row.finalText,
|
|
48
70
|
childRunIds: children.map((c) => c.id),
|
|
71
|
+
steps,
|
|
72
|
+
stepsOmitted: Math.max(0, stepCount - newest.length),
|
|
73
|
+
stepsDropped: row.stepsDropped,
|
|
49
74
|
coding: coding
|
|
50
75
|
? {
|
|
51
76
|
provider: coding.provider,
|