summer-engine 2.6.5 → 2.7.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -2
- package/.cursor-plugin/plugin.json +4 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +8 -4
- package/CHANGELOG.md +41 -0
- package/GEMINI.md +5 -4
- package/README.md +25 -20
- package/_persona/summer/SKILL.md +1 -0
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +4 -0
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +22 -6
- package/dist/lib/api-client.js +161 -36
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/cloud/engine-bridge.js +9 -1
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/skills-registry.d.ts +25 -1
- package/dist/lib/skills-registry.js +46 -0
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +79 -7
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +143 -5
- package/dist/mcp/tools/project-tools.js +25 -20
- package/dist/mcp/tools/scene-tools.js +101 -24
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +0 -0
- package/package.json +6 -2
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/debugging/debug/SKILL.md +23 -1
- package/skills/deployment/remote-deploy/SKILL.md +107 -0
- package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/SKILL.md +1 -1
- package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
- package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +6 -6
- package/skills/workflow/using-summer/SKILL.md +20 -1
- package/skills/workflow/writing-skills/render-graphs.js +0 -0
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -33,11 +33,16 @@ export type EngineSnapshot = {
|
|
|
33
33
|
* (item 4, client-side drift check). */
|
|
34
34
|
projectMismatch?: boolean;
|
|
35
35
|
};
|
|
36
|
+
type EngineTargetIdentity = {
|
|
37
|
+
instanceId?: string;
|
|
38
|
+
projectId?: string;
|
|
39
|
+
projectIdHash?: string;
|
|
40
|
+
};
|
|
36
41
|
export declare class EngineApiClient {
|
|
37
42
|
private port;
|
|
38
43
|
private token;
|
|
39
|
-
private
|
|
40
|
-
constructor(port: number, token: string,
|
|
44
|
+
private targetIdentity;
|
|
45
|
+
constructor(port: number, token: string, targetIdentity?: EngineTargetIdentity | string);
|
|
41
46
|
static connect(): Promise<EngineApiClient>;
|
|
42
47
|
/** The projectIdHash this session is bound to (undefined if none was reported
|
|
43
48
|
* at connect — e.g. no project open yet). */
|
|
@@ -46,13 +51,15 @@ export declare class EngineApiClient {
|
|
|
46
51
|
* Re-read health and rebind to the currently-open project. Called by
|
|
47
52
|
* summer_get_project_context so the agent can INTENTIONALLY follow a project
|
|
48
53
|
* switch (the deliberate escape hatch after an identity_mismatch). Returns the
|
|
49
|
-
* new bound hash.
|
|
50
|
-
* even when
|
|
54
|
+
* new bound hash. The direct health probe is intentionally unscoped, so it can
|
|
55
|
+
* reach the current engine even when bound requests would be rejected.
|
|
51
56
|
*/
|
|
52
57
|
rebind(): Promise<string | undefined>;
|
|
53
58
|
/** Identity to attach to a MUTATING command's options so the engine can reject
|
|
54
59
|
* a wrong-project write. Empty when unbound (engine then skips the check). */
|
|
55
60
|
private identityOptions;
|
|
61
|
+
private requireBoundIdentity;
|
|
62
|
+
private targetUrl;
|
|
56
63
|
private request;
|
|
57
64
|
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
58
65
|
* the status (202 queued vs 200 legacy). */
|
|
@@ -64,12 +71,20 @@ export declare class EngineApiClient {
|
|
|
64
71
|
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
65
72
|
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
66
73
|
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
67
|
-
*
|
|
74
|
+
* client wait budget; expiry preserves the requestId and last known lifecycle
|
|
75
|
+
* state instead of pretending the accepted operation did not apply.
|
|
68
76
|
*/
|
|
69
77
|
private _requestQueued;
|
|
70
78
|
health(): Promise<unknown>;
|
|
71
79
|
executeOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
|
|
72
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Mutation path for MCP file tools. Unlike legacy executeOps callers, this
|
|
82
|
+
* fails closed when health did not provide a complete target identity. The
|
|
83
|
+
* bound hash is stamped last so a caller cannot override it in options.
|
|
84
|
+
*/
|
|
85
|
+
executeIdentityBoundOps(ops: Record<string, unknown>[], options?: Record<string, unknown>): Promise<unknown>;
|
|
86
|
+
readProjectFile(path: string, maxBytes?: number): Promise<unknown>;
|
|
87
|
+
getSceneState(scenePath?: string): Promise<unknown>;
|
|
73
88
|
getProjectState(): Promise<unknown>;
|
|
74
89
|
getDiagnostics(): Promise<unknown>;
|
|
75
90
|
inspectNode(path: string): Promise<unknown>;
|
|
@@ -125,3 +140,4 @@ export declare class EngineApiClient {
|
|
|
125
140
|
*/
|
|
126
141
|
credentialsChanged(): Promise<boolean>;
|
|
127
142
|
}
|
|
143
|
+
export {};
|
package/dist/lib/api-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { getApiToken, getApiPort, checkEngineHealth } from "./engine.js";
|
|
5
6
|
import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
|
|
6
7
|
function asRecord(value) {
|
|
@@ -67,17 +68,19 @@ function withoutImageData(payload) {
|
|
|
67
68
|
export class EngineApiClient {
|
|
68
69
|
port;
|
|
69
70
|
token;
|
|
70
|
-
// The project this session is
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
constructor(port, token, boundProjectIdHash) {
|
|
71
|
+
// The engine and project this session is bound to. Every request carries the
|
|
72
|
+
// identity captured from health so the engine can reject requests after an
|
|
73
|
+
// in-place project switch or an unexpected instance change. The string form
|
|
74
|
+
// preserves the pre-2.6.6 constructor contract for existing callers that only
|
|
75
|
+
// supplied a projectIdHash.
|
|
76
|
+
targetIdentity;
|
|
77
|
+
constructor(port, token, targetIdentity = {}) {
|
|
78
78
|
this.port = port;
|
|
79
79
|
this.token = token;
|
|
80
|
-
this.
|
|
80
|
+
this.targetIdentity =
|
|
81
|
+
typeof targetIdentity === "string"
|
|
82
|
+
? { projectIdHash: targetIdentity }
|
|
83
|
+
: { ...targetIdentity };
|
|
81
84
|
}
|
|
82
85
|
static async connect() {
|
|
83
86
|
const port = await getApiPort();
|
|
@@ -94,34 +97,64 @@ export class EngineApiClient {
|
|
|
94
97
|
// rebinds to the current project. An in-place project switch keeps the same
|
|
95
98
|
// token, so the cached client retains its original binding and the engine
|
|
96
99
|
// rejects mismatched mutations until the agent explicitly rebinds.
|
|
97
|
-
return new EngineApiClient(port, token,
|
|
100
|
+
return new EngineApiClient(port, token, {
|
|
101
|
+
instanceId: health.instanceId,
|
|
102
|
+
projectId: health.projectId,
|
|
103
|
+
projectIdHash: health.projectIdHash,
|
|
104
|
+
});
|
|
98
105
|
}
|
|
99
106
|
/** The projectIdHash this session is bound to (undefined if none was reported
|
|
100
107
|
* at connect — e.g. no project open yet). */
|
|
101
108
|
getBoundProjectIdHash() {
|
|
102
|
-
return this.
|
|
109
|
+
return this.targetIdentity.projectIdHash;
|
|
103
110
|
}
|
|
104
111
|
/**
|
|
105
112
|
* Re-read health and rebind to the currently-open project. Called by
|
|
106
113
|
* summer_get_project_context so the agent can INTENTIONALLY follow a project
|
|
107
114
|
* switch (the deliberate escape hatch after an identity_mismatch). Returns the
|
|
108
|
-
* new bound hash.
|
|
109
|
-
* even when
|
|
115
|
+
* new bound hash. The direct health probe is intentionally unscoped, so it can
|
|
116
|
+
* reach the current engine even when bound requests would be rejected.
|
|
110
117
|
*/
|
|
111
118
|
async rebind() {
|
|
112
119
|
const health = await checkEngineHealth(this.port);
|
|
113
120
|
if (health) {
|
|
114
|
-
this.
|
|
121
|
+
this.targetIdentity = {
|
|
122
|
+
instanceId: health.instanceId,
|
|
123
|
+
projectId: health.projectId,
|
|
124
|
+
projectIdHash: health.projectIdHash,
|
|
125
|
+
};
|
|
115
126
|
}
|
|
116
|
-
return this.
|
|
127
|
+
return this.targetIdentity.projectIdHash;
|
|
117
128
|
}
|
|
118
129
|
/** Identity to attach to a MUTATING command's options so the engine can reject
|
|
119
130
|
* a wrong-project write. Empty when unbound (engine then skips the check). */
|
|
120
131
|
identityOptions() {
|
|
121
|
-
|
|
132
|
+
const projectIdHash = this.targetIdentity.projectIdHash;
|
|
133
|
+
return projectIdHash ? { projectIdHash } : {};
|
|
134
|
+
}
|
|
135
|
+
requireBoundIdentity() {
|
|
136
|
+
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
137
|
+
if (!instanceId || !projectId || !projectIdHash) {
|
|
138
|
+
throw new Error("Safe project mutation requires a complete engine/project identity. Call summer_get_project_context to bind this MCP session, then retry. Nothing was written.");
|
|
139
|
+
}
|
|
140
|
+
return { projectIdHash };
|
|
141
|
+
}
|
|
142
|
+
targetUrl(path) {
|
|
143
|
+
const url = new URL(path, `http://127.0.0.1:${this.port}`);
|
|
144
|
+
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
145
|
+
if (instanceId)
|
|
146
|
+
url.searchParams.set("instanceId", instanceId);
|
|
147
|
+
if (projectId)
|
|
148
|
+
url.searchParams.set("projectId", projectId);
|
|
149
|
+
if (projectIdHash)
|
|
150
|
+
url.searchParams.set("projectIdHash", projectIdHash);
|
|
151
|
+
if (instanceId && projectId && projectIdHash) {
|
|
152
|
+
url.searchParams.set("projectIdentityVersion", "1");
|
|
153
|
+
}
|
|
154
|
+
return url.toString();
|
|
122
155
|
}
|
|
123
156
|
async request(method, path, body) {
|
|
124
|
-
const url =
|
|
157
|
+
const url = this.targetUrl(path);
|
|
125
158
|
const headers = {
|
|
126
159
|
Authorization: `Bearer ${this.token}`,
|
|
127
160
|
"Content-Type": "application/json",
|
|
@@ -141,7 +174,7 @@ export class EngineApiClient {
|
|
|
141
174
|
/** Raw fetch with auth + timeout — returns the Response so callers can inspect
|
|
142
175
|
* the status (202 queued vs 200 legacy). */
|
|
143
176
|
async _fetchRaw(method, path, body, timeoutMs) {
|
|
144
|
-
return fetch(
|
|
177
|
+
return fetch(this.targetUrl(path), {
|
|
145
178
|
method,
|
|
146
179
|
headers: {
|
|
147
180
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -165,24 +198,98 @@ export class EngineApiClient {
|
|
|
165
198
|
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
166
199
|
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
167
200
|
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
168
|
-
*
|
|
201
|
+
* client wait budget; expiry preserves the requestId and last known lifecycle
|
|
202
|
+
* state instead of pretending the accepted operation did not apply.
|
|
169
203
|
*/
|
|
170
204
|
async _requestQueued(method, path, body, timeoutMs) {
|
|
171
|
-
|
|
205
|
+
// Mint the request ID before submission. If the acknowledgement or a later
|
|
206
|
+
// poll is lost, callers retain the exact identity needed to reconcile the
|
|
207
|
+
// accepted lifecycle instead of issuing an unsafe blind retry.
|
|
208
|
+
let clientRequestId;
|
|
209
|
+
let requestBody = body;
|
|
210
|
+
if (method === "POST") {
|
|
211
|
+
const bodyRecord = asRecord(body) ?? {};
|
|
212
|
+
const existingOptions = asRecord(bodyRecord.options) ?? {};
|
|
213
|
+
clientRequestId =
|
|
214
|
+
stringFrom(existingOptions.requestId) ?? `mcp-${randomUUID()}`;
|
|
215
|
+
requestBody = {
|
|
216
|
+
...bodyRecord,
|
|
217
|
+
options: {
|
|
218
|
+
...existingOptions,
|
|
219
|
+
requestId: clientRequestId,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
let res;
|
|
224
|
+
try {
|
|
225
|
+
res = await this._fetchRaw(method, path, requestBody, timeoutMs);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (!clientRequestId)
|
|
229
|
+
throw error;
|
|
230
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
231
|
+
throw new Error(`Summer Engine did not acknowledge request ${clientRequestId}; dispatch state is unknown. Do not retry blindly. ${detail}`);
|
|
232
|
+
}
|
|
172
233
|
// 202 (queued) + 429 (backpressure, errorClass in body) are handled below;
|
|
173
234
|
// any other non-2xx is a real transport error.
|
|
174
235
|
if (!res.ok && res.status !== 202 && res.status !== 429) {
|
|
175
236
|
const text = await res.text().catch(() => "");
|
|
176
|
-
|
|
237
|
+
const suffix = clientRequestId ? ` (requestId ${clientRequestId})` : "";
|
|
238
|
+
throw new Error(`Engine API error ${res.status}${suffix}: ${text.slice(0, 200)}`);
|
|
177
239
|
}
|
|
178
240
|
const respBody = await res.json().catch(() => ({}));
|
|
179
241
|
const classified = classifyOpsResponse(res.status, respBody);
|
|
180
242
|
if (classified.mode === "legacy") {
|
|
243
|
+
if (res.status === 429) {
|
|
244
|
+
return {
|
|
245
|
+
...(asRecord(respBody) ?? {}),
|
|
246
|
+
requestId: clientRequestId,
|
|
247
|
+
terminalState: stringFrom(asRecord(respBody)?.terminalState) ?? "queue_full",
|
|
248
|
+
dispatchState: "not_sent",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
181
251
|
return respBody;
|
|
182
252
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
253
|
+
// The acknowledgement is authoritative for polling. Current engines echo
|
|
254
|
+
// the client-generated ID, while older engines may ignore options.requestId
|
|
255
|
+
// and mint their own; polling the submitted ID against those engines would
|
|
256
|
+
// wait forever on an unknown lifecycle.
|
|
257
|
+
const acceptedRequestId = classified.requestId || clientRequestId;
|
|
258
|
+
if (!acceptedRequestId) {
|
|
259
|
+
return {
|
|
260
|
+
status: "error",
|
|
261
|
+
error: "Summer Engine accepted an operation without returning a requestId; final state is uncertain.",
|
|
262
|
+
terminalState: "uncertain",
|
|
263
|
+
errorClass: "transient",
|
|
264
|
+
dispatchState: "uncertain",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const terminal = await pollOpToTerminal((waitMs) => this._pollResult(acceptedRequestId, waitMs), {
|
|
269
|
+
totalTimeoutMs: timeoutMs,
|
|
270
|
+
requestId: acceptedRequestId,
|
|
271
|
+
});
|
|
272
|
+
if (clientRequestId && clientRequestId !== acceptedRequestId) {
|
|
273
|
+
terminal.submissionRequestId = clientRequestId;
|
|
274
|
+
}
|
|
275
|
+
return terminal;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
279
|
+
return {
|
|
280
|
+
status: "error",
|
|
281
|
+
error: `Summer Engine accepted request ${acceptedRequestId}, but its final receipt could not be retrieved. ` +
|
|
282
|
+
`The operation may still be running or may already have applied; inspect the target before retrying. ${detail}`,
|
|
283
|
+
terminalState: "uncertain",
|
|
284
|
+
errorClass: "transient",
|
|
285
|
+
dispatchState: "uncertain",
|
|
286
|
+
requestId: acceptedRequestId,
|
|
287
|
+
...(clientRequestId && clientRequestId !== acceptedRequestId
|
|
288
|
+
? { submissionRequestId: clientRequestId }
|
|
289
|
+
: {}),
|
|
290
|
+
lastKnownStatus: "unknown",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
186
293
|
}
|
|
187
294
|
async health() {
|
|
188
295
|
return this.request("GET", "/api/health");
|
|
@@ -193,12 +300,31 @@ export class EngineApiClient {
|
|
|
193
300
|
// async-202 (Block E).
|
|
194
301
|
// Attach the bound project identity so the engine rejects a write aimed at a
|
|
195
302
|
// different project (identity_mismatch, atomic — before any op applies).
|
|
196
|
-
const merged = { ...
|
|
303
|
+
const merged = { ...(options ?? {}), ...this.identityOptions() };
|
|
197
304
|
const body = Object.keys(merged).length ? { ops, options: merged } : { ops };
|
|
198
305
|
return this._requestQueued("POST", "/api/ops", body, 120_000);
|
|
199
306
|
}
|
|
200
|
-
|
|
201
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Mutation path for MCP file tools. Unlike legacy executeOps callers, this
|
|
309
|
+
* fails closed when health did not provide a complete target identity. The
|
|
310
|
+
* bound hash is stamped last so a caller cannot override it in options.
|
|
311
|
+
*/
|
|
312
|
+
async executeIdentityBoundOps(ops, options) {
|
|
313
|
+
const identity = this.requireBoundIdentity();
|
|
314
|
+
return this.executeOps(ops, { ...(options ?? {}), ...identity });
|
|
315
|
+
}
|
|
316
|
+
async readProjectFile(path, maxBytes = 200_000) {
|
|
317
|
+
const query = new URLSearchParams({
|
|
318
|
+
path,
|
|
319
|
+
maxBytes: String(maxBytes),
|
|
320
|
+
});
|
|
321
|
+
return this.request("GET", `/api/state/read-file?${query.toString()}`);
|
|
322
|
+
}
|
|
323
|
+
async getSceneState(scenePath) {
|
|
324
|
+
const query = scenePath
|
|
325
|
+
? `?scene=${encodeURIComponent(scenePath)}`
|
|
326
|
+
: "";
|
|
327
|
+
return this.request("GET", `/api/state/scene${query}`);
|
|
202
328
|
}
|
|
203
329
|
async getProjectState() {
|
|
204
330
|
return this.request("GET", "/api/state/project");
|
|
@@ -305,18 +431,17 @@ export class EngineApiClient {
|
|
|
305
431
|
const { format, mime, ext } = snapshotFormat(payload);
|
|
306
432
|
const buffer = Buffer.from(base64, "base64");
|
|
307
433
|
const localPath = await this.writeSnapshotFile(kind, buffer, ext);
|
|
308
|
-
// Client-side identity drift check (item 4).
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
// project. Best-effort: a failed health read never blocks the (already
|
|
314
|
-
// captured) image.
|
|
434
|
+
// Client-side identity drift check (item 4). Current engines validate the
|
|
435
|
+
// identity query on snapshot reads, but older builds may ignore it. Re-read
|
|
436
|
+
// health and compare to the bound hash so a frame from a switched project is
|
|
437
|
+
// still marked as suspect. Best-effort: a failed health read never blocks the
|
|
438
|
+
// already captured image.
|
|
315
439
|
let projectMismatch;
|
|
316
|
-
|
|
440
|
+
const boundProjectIdHash = this.targetIdentity.projectIdHash;
|
|
441
|
+
if (boundProjectIdHash) {
|
|
317
442
|
try {
|
|
318
443
|
const health = await checkEngineHealth(this.port);
|
|
319
|
-
if (health?.projectIdHash && health.projectIdHash !==
|
|
444
|
+
if (health?.projectIdHash && health.projectIdHash !== boundProjectIdHash) {
|
|
320
445
|
projectMismatch = true;
|
|
321
446
|
}
|
|
322
447
|
}
|
|
@@ -40,6 +40,10 @@ export declare function classifyOpsResponse(httpStatus: number, body: unknown):
|
|
|
40
40
|
export interface PollOpts {
|
|
41
41
|
/** Hard wall-clock cap on the whole poll loop (ms). The per-tool budget. */
|
|
42
42
|
totalTimeoutMs: number;
|
|
43
|
+
/** Stable client-generated correlation ID. Preserved in non-terminal timeout
|
|
44
|
+
* receipts so callers can reconcile the accepted operation instead of
|
|
45
|
+
* blindly retrying it. */
|
|
46
|
+
requestId?: string;
|
|
43
47
|
/** Give up if the op hasn't advanced for this long (ms). Default = totalTimeoutMs. */
|
|
44
48
|
noProgressTimeoutMs?: number;
|
|
45
49
|
/** Server long-poll wait per GET (ms). Default 5000 — the engine holds the
|
|
@@ -54,7 +58,8 @@ export interface PollOpts {
|
|
|
54
58
|
/**
|
|
55
59
|
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
56
60
|
* one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
|
|
57
|
-
* dict with terminalState/errorClass/appliedSeq merged on top, or a
|
|
58
|
-
* `
|
|
61
|
+
* dict with terminalState/errorClass/appliedSeq merged on top, or a truthful
|
|
62
|
+
* non-terminal `still_queued` / `still_running` / `uncertain` receipt when the
|
|
63
|
+
* client budget expires before the engine publishes a terminal.
|
|
59
64
|
*/
|
|
60
65
|
export declare function pollOpToTerminal(pollOnce: (waitMs: number) => Promise<OpResultEnvelope>, opts: PollOpts): Promise<Record<string, unknown>>;
|
|
@@ -30,17 +30,37 @@ export function classifyOpsResponse(httpStatus, body) {
|
|
|
30
30
|
}
|
|
31
31
|
return { mode: "legacy", legacyResult: body };
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
function incompleteResult(requestId, lastStatus) {
|
|
34
|
+
if (lastStatus === "queued") {
|
|
35
|
+
return {
|
|
36
|
+
status: "error",
|
|
37
|
+
error: `Summer Engine accepted request ${requestId ?? "(unknown)"}, but it was still queued when this tool stopped waiting. ` +
|
|
38
|
+
"It may still run; do not retry blindly.",
|
|
39
|
+
terminalState: "still_queued",
|
|
40
|
+
errorClass: "transient",
|
|
41
|
+
dispatchState: "accepted",
|
|
42
|
+
requestId,
|
|
43
|
+
lastKnownStatus: "queued",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const wasRunning = lastStatus === "running";
|
|
47
|
+
return {
|
|
48
|
+
status: "error",
|
|
49
|
+
error: `Summer Engine did not provide a final receipt for request ${requestId ?? "(unknown)"}. ` +
|
|
50
|
+
`${wasRunning ? "The operation was still running" : "Its dispatch state is uncertain"}; inspect the target before retrying.`,
|
|
51
|
+
terminalState: wasRunning ? "still_running" : "uncertain",
|
|
52
|
+
errorClass: "transient",
|
|
53
|
+
dispatchState: wasRunning ? "sent" : "uncertain",
|
|
54
|
+
requestId,
|
|
55
|
+
lastKnownStatus: lastStatus ?? "unknown",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
41
60
|
* one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
|
|
42
|
-
* dict with terminalState/errorClass/appliedSeq merged on top, or a
|
|
43
|
-
* `
|
|
61
|
+
* dict with terminalState/errorClass/appliedSeq merged on top, or a truthful
|
|
62
|
+
* non-terminal `still_queued` / `still_running` / `uncertain` receipt when the
|
|
63
|
+
* client budget expires before the engine publishes a terminal.
|
|
44
64
|
*/
|
|
45
65
|
export async function pollOpToTerminal(pollOnce, opts) {
|
|
46
66
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -57,6 +77,9 @@ export async function pollOpToTerminal(pollOnce, opts) {
|
|
|
57
77
|
const env = await pollOnce(pollWaitMs);
|
|
58
78
|
if (isTerminalStatus(env.status)) {
|
|
59
79
|
const out = { ...(env.result ?? {}) };
|
|
80
|
+
const requestId = opts.requestId ?? env.requestId;
|
|
81
|
+
if (requestId)
|
|
82
|
+
out.requestId = requestId;
|
|
60
83
|
if (env.terminalState)
|
|
61
84
|
out.terminalState = env.terminalState;
|
|
62
85
|
if (env.errorClass)
|
|
@@ -76,7 +99,7 @@ export async function pollOpToTerminal(pollOnce, opts) {
|
|
|
76
99
|
lastProgress = progressJson;
|
|
77
100
|
}
|
|
78
101
|
if (t - start >= opts.totalTimeoutMs || t - lastAdvanceAt >= noProgressMs) {
|
|
79
|
-
return
|
|
102
|
+
return incompleteResult(opts.requestId ?? env.requestId, env.status);
|
|
80
103
|
}
|
|
81
104
|
await sleep(pacingMs);
|
|
82
105
|
}
|
|
@@ -50,7 +50,15 @@ async function connectForProject(projectRoot) {
|
|
|
50
50
|
// rescan; the executor rejects mismatches too, but skip the round trip.
|
|
51
51
|
if (health.projectIdHash && health.projectIdHash !== projectIdHash)
|
|
52
52
|
return null;
|
|
53
|
-
return {
|
|
53
|
+
return {
|
|
54
|
+
client: new EngineApiClient(port, token, {
|
|
55
|
+
instanceId: health.instanceId,
|
|
56
|
+
projectId: health.projectId,
|
|
57
|
+
projectIdHash,
|
|
58
|
+
}),
|
|
59
|
+
projectIdHash,
|
|
60
|
+
scene: health.scene,
|
|
61
|
+
};
|
|
54
62
|
}
|
|
55
63
|
catch {
|
|
56
64
|
return null;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type DoctorResult } from "../commands/doctor.js";
|
|
2
|
+
export interface DebugReportOptions {
|
|
3
|
+
issue?: string;
|
|
4
|
+
outputPath?: string;
|
|
5
|
+
includePlaySession?: boolean;
|
|
6
|
+
playWaitMs?: number;
|
|
7
|
+
maxConsoleLines?: number;
|
|
8
|
+
maxDebuggerEntries?: number;
|
|
9
|
+
includeDoctor?: boolean;
|
|
10
|
+
baseDir?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DebugReportArtifact {
|
|
13
|
+
path: string;
|
|
14
|
+
markdown: string;
|
|
15
|
+
report: DebugReport;
|
|
16
|
+
}
|
|
17
|
+
export interface DebugReport {
|
|
18
|
+
generatedAt: string;
|
|
19
|
+
issue: string | null;
|
|
20
|
+
environment: {
|
|
21
|
+
cliVersion: string;
|
|
22
|
+
nodeVersion: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
release: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
hostname: string;
|
|
27
|
+
};
|
|
28
|
+
doctor: DoctorResult | null;
|
|
29
|
+
mcp: {
|
|
30
|
+
logPath: string;
|
|
31
|
+
recentLog: string[];
|
|
32
|
+
};
|
|
33
|
+
engine: {
|
|
34
|
+
connected: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
health?: unknown;
|
|
37
|
+
project?: unknown;
|
|
38
|
+
scene?: unknown;
|
|
39
|
+
diagnostics?: unknown;
|
|
40
|
+
console?: unknown;
|
|
41
|
+
debuggerErrors?: unknown;
|
|
42
|
+
debuggerWarnings?: unknown;
|
|
43
|
+
playSession?: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function defaultDebugReportFilename(date?: Date): string;
|
|
47
|
+
export declare function redactDebugReport(report: DebugReport): DebugReport;
|
|
48
|
+
export declare function renderDebugReportMarkdown(report: DebugReport): string;
|
|
49
|
+
export declare function collectDebugReport(options?: DebugReportOptions): Promise<DebugReport>;
|
|
50
|
+
export declare function createDebugReportArtifact(options?: DebugReportOptions): Promise<DebugReportArtifact>;
|