Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00053cf1d3 |
@@ -17,7 +17,7 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
2. Before the first generation in a new conversation, obtain a canvas name unless an existing `projectId` and `assetFolderId` were supplied. Create or reuse a project and a same-name asset-library folder. Retain `canvasName`, `projectId`, `assetFolderId`, and the current art spec.
|
||||
3. Normalize art requests into a reusable spec. Ask only for missing values that block the selected operation. Reuse the spec until the user changes its style, subject family, palette, format, or constraints.
|
||||
4. Infer the operation from the user's intent. Do not ask the user to select an API unless two operations would produce materially different artifacts.
|
||||
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to operations that accept object references; never substitute a temporary signed URL. For an icon-spritesheet primary spec, additionally create a project resource or asset record with `assetKind="icon-spec"`, then pass the returned resource or asset ID as `referenceId`.
|
||||
5. If a reference exists only as a local file, upload and confirm it first. Pass the stable returned `objectKey` to generation; never substitute a temporary signed URL.
|
||||
6. For generation endpoints that support the fields, include `projectId`, `assetFolderId`, an asset label, and `canvasCompletion` so the result enters both the canvas and its same-name library folder.
|
||||
7. Treat every generation POST as asynchronous. Send one stable `Idempotency-Key` per logical request, retain the returned `operationId`, and poll the returned `statusUrl` or `GET /api/external/v1/generations/{operationId}` according to `pollAfterMs`.
|
||||
8. Consume `result` only after `status=completed`. On `failed`, surface the safe error. On a client timeout or lost response, retain the operation/key; do not create a replacement request.
|
||||
@@ -29,10 +29,9 @@ Prefer `scripts/genarrative_external_api.py` for runnable REST calls. It uses on
|
||||
- Authenticate MCP and business API calls with `Authorization: Bearer <tnr_sk_...>`. Never ask the user to paste a key into chat or place one in repository files.
|
||||
- All eight generation POST routes require `Idempotency-Key` and return HTTP `202`; `202` is durable acceptance, not a media result.
|
||||
- Retry an uncertain submission only with the exact same body and the same idempotency key. A polling timeout is not permission to generate again.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID where each operation permits them. Image edit/redraw is stricter: `sourceReferenceId` accepts only a registered project resource ID or asset ID; upload confirmation alone is not enough. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Use stable references such as `objectKey`, project resource ID, or asset ID in generation requests. Use `/assets/read-url` only for temporary preview/download access.
|
||||
- Preserve both warning channels after completion. A general `warning` can coexist with `sliceWarning`; do not discard either.
|
||||
- Do not invent missing derivatives. A source-preserved warning means the main source remains usable but requested post-processing failed. A slice warning means the complete transparent sheet is usable but individual slices are absent.
|
||||
- For successful `style="pixelArt"`, treat completed-result and nested resource/asset dimensions as the final logical-grid PNG dimensions. They may differ from `size`, `imageSize`, the provider image, and `canvasCompletion.placeholder`; do not rescale or reject the artifact to match those inputs.
|
||||
- Keep generated artifacts in the canvas and asset library together. Character animation accepts `assetFolderId` and `assetLabel`; its completed result directly returns the final `assetKind="character-animation"` resource and asset with formal sequence fields. Do not create a duplicate first-frame record.
|
||||
|
||||
## Documentation Navigation
|
||||
|
||||
@@ -8,7 +8,7 @@ All paths below are relative to `https://www.genarrative.world`. Discovery and S
|
||||
|
||||
| Operation | Method and path | Minimum input |
|
||||
| --- | --- | --- |
|
||||
| List projects | `GET /api/external/v1/editor/projects` | Authentication; optional `view=full\|summary` (default `full`) |
|
||||
| List projects | `GET /api/external/v1/editor/projects` | Authentication |
|
||||
| Create project | `POST /api/external/v1/editor/projects` | Optional `title` |
|
||||
| Load recent project | `GET /api/external/v1/editor/projects/recent` | Authentication |
|
||||
| Get project | `GET /api/external/v1/editor/projects/{projectId}` | `projectId` |
|
||||
@@ -19,13 +19,6 @@ All paths below are relative to `https://www.genarrative.world`. Discovery and S
|
||||
|
||||
Canvas save uses optimistic revision control. Pass the last authoritative `expectedRevision`; on conflict, reload instead of replaying a stale full layout.
|
||||
|
||||
Project listing supports two views:
|
||||
|
||||
- `view=full` is the REST default and returns the complete project, canvas, layers, and resources.
|
||||
- `view=summary` returns only `projectId`, `title`, `updatedAt`, and nullable `cover`, so callers can display, search, disambiguate same-name projects, and select a safe target without loading every canvas snapshot.
|
||||
- Hosted MCP `list_editor_projects` always uses `summary`; call `get_editor_project` after selecting a `projectId` when complete authoritative state is required.
|
||||
- `cover` contains only `resourceId`, stable `objectKey`, dimensions, and `updatedAt`. It never embeds image bytes, a Data URL, or a signed URL. To display it, pass `cover.objectKey` to `get_external_asset_read_url`; signed URLs are temporary and must not be persisted or reused as generation references.
|
||||
|
||||
## Asset and Upload Operations
|
||||
|
||||
| Operation | Method and path | Minimum input |
|
||||
@@ -50,12 +43,12 @@ Every generation row requires a stable `Idempotency-Key` header and returns HTTP
|
||||
| Capability | POST path | Required body fields | Common optional body fields |
|
||||
| --- | --- | --- | --- |
|
||||
| Image generation | `/api/external/v1/editor/images/generations` | `prompt` | `kind`, `style`, `model`, `aspectRatio`, `imageSize`, `size`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceReferenceId` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceId`, `iconDescriptions` | `sliceLayout`, `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Image edit/redraw | `/api/external/v1/editor/images/edits` | `prompt`, `sourceImageSrc` | `referenceImageSrcs`, `model`, `size`, `projectId`, `assetFolderId`, `assetLabel`, `sourceResourceId`, `targetLayerId`, `canvasCompletion` |
|
||||
| Icon spritesheet | `/api/external/v1/editor/icon-spritesheets/generations` | `referenceImageSrc`, `iconDescriptions` | `style`, `referenceImageSrcs`, `screenColor`, `model`, `aspectRatio`, `imageSize`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| UI asset extraction | `/api/external/v1/editor/ui-designs/assets/extractions` | `sourceImageSrc`, `aspectRatio`, `imageSize` | `screenColor`, `model`, `referenceImageSrcs`, `projectId`, `assetFolderId`, `spritesheetLabel`, `canvasCompletion` |
|
||||
| Character animation | `/api/external/v1/editor/character-animations/generations` | `sourceLayerId`, `sourceImageSrc`, `sourceWidth`, `sourceHeight`, `promptText`, `resolution`, `ratio`, `frameCount`, `durationSeconds`, `model` | `projectId`, `sourceResourceId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Video generation | `/api/external/v1/editor/videos/generations` | `prompt`, `model`, `aspectRatio`, `durationSeconds`, `resolution`, `mode`, `sound` | `referenceImageSrcs`, `referenceVideoSrcs`, `referenceAudioSrcs`, `webSearchEnabled`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion` |
|
||||
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt` | `model`, `duration`, `loop`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Sound effect | `/api/external/v1/editor/audios/sound-effects/generations` | `prompt`, `duration` | `model`, `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
| Background music | `/api/external/v1/editor/audios/background-music/generations` | `gptDescriptionPrompt`, `makeInstrumental` | `projectId`, `assetFolderId`, `assetLabel`, `canvasCompletion`, `generationInputs` |
|
||||
|
||||
Poll all eight through:
|
||||
@@ -77,29 +70,24 @@ Supply the `operationId` returned by submission. Poll no faster than `pollAfterM
|
||||
|
||||
## Reference Field Mapping
|
||||
|
||||
After confirming a local upload, pass its stable `objectKey` into operations that accept object references:
|
||||
After confirming a local upload, pass its stable `objectKey` into:
|
||||
|
||||
| Target capability | Field |
|
||||
| --- | --- |
|
||||
| Image generation | `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceReferenceId` must be a registered project resource ID or asset ID; additional references remain in `referenceImageSrcs` |
|
||||
| Icon spritesheet | Register the primary spec as an `assetKind="icon-spec"` project resource or asset, then pass its returned ID as `referenceId`; additional style references remain in `referenceImageSrcs` |
|
||||
| Image edit/redraw | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Icon spritesheet | `referenceImageSrc`; additional style references in `referenceImageSrcs` |
|
||||
| UI design extraction | `sourceImageSrc`; additional references in `referenceImageSrcs` |
|
||||
| Character animation | `sourceImageSrc` |
|
||||
| Video with image references | `referenceImageSrcs` |
|
||||
|
||||
For image edit/redraw, confirming an upload is not sufficient: create a project resource or asset-library record first, then pass that record's ID as `sourceReferenceId`. The main source never accepts objectKey, URL, Data URL, or Blob URL. Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
The icon-spritesheet primary `referenceId` is intentionally stricter than ordinary image references: it accepts only a current-owner project resource ID or asset ID whose authoritative `assetKind` is `icon-spec`. It does not accept an `objectKey`, URL, Data URL, or Blob URL.
|
||||
|
||||
`sliceLayout: "grid-2x2"` is an opt-in contract for four fixed game-runtime assets. The provider prompt and server persistence both preserve the ordered slots left-top, right-top, left-bottom, right-bottom. Omit it to retain the default connected-component slicing behaviour for ordinary free-form icon sheets.
|
||||
Use video/audio reference arrays only with models that support them. Do not pass an expiring signed read URL as a generation reference.
|
||||
|
||||
## Common Values
|
||||
|
||||
Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
|
||||
- Image `kind`: `spec`, `character`, `quick-edit`, `ui-design`, `publication-material`; ordinary image generation may omit it.
|
||||
- External v1 currently has no structured game-scene generation operation. Do not send `kind: "scene"` or `assetKind: "scene"` through generic image generation; the server rejects both before queueing.
|
||||
- Image `model`: `gpt-image-2`, `gemini-3.1-flash-image-preview`, `nanobanana2`, `nano-banana`.
|
||||
- Image `aspectRatio`: `1:1`, `2:3`, `3:2`, `9:16`, `16:9`.
|
||||
- Image `imageSize`: `0.5K`, `1K`, `2K`.
|
||||
@@ -107,7 +95,6 @@ Use OpenAPI as the final authority; these common values are a routing aid:
|
||||
- Video `aspectRatio`: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`.
|
||||
- Video `resolution`: `480p`, `720p`, `1080p`; `mode`: `std`; `sound`: `on` or `off`.
|
||||
- Character animation uses `model: "seedance2.0-fast"`; `resolution`: `480p` or `720p`; `frameCount`: `32`, `40`, or `48`; `durationSeconds`: `4`, `5`, or `6`; `ratio`: `same`, `1:1`, `4:3`, `16:9`, `9:16`, or `3:4`.
|
||||
- Sound effect uses canonical model `eleven_text_to_sound_v2`; omit `duration` or send `null` for automatic duration, otherwise send a finite `0.5-30` number. `loop` defaults to `false` and remains independent from Prompt text.
|
||||
- UI extraction uses `aspectRatio: "1:1"`; use `imageSize: "1K"` for normal/small extraction and `2K` for dense designs.
|
||||
|
||||
Do not hard-code this list as a replacement client schema. In particular, the top-level image `style` field is intentionally extensible; see `requests-and-outputs.md` for its fallback behavior.
|
||||
|
||||
@@ -78,9 +78,9 @@ Keep the existing autonomous-build task graph. Do not add a parallel task system
|
||||
|
||||
1. `art-director` generates `assets/art-spec.png` with image generation, `kind: "spec"`, then registers it as `assetKind: "icon-spec"`. This image is the authoritative visual spec; `generationInputs.artSpec` is supporting structured context.
|
||||
2. `design-foundation` generates `assets/ui-prototype.png` with `kind: "ui-design"`, using the registered art-spec resource ID in `referenceImageSrcs`.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceId` plus concrete `iconDescriptions`. For the four-category game contract it must also send `sliceLayout: "grid-2x2"`; this is an explicit fixed-slot contract, not a client-side guessed crop.
|
||||
3. `art-asset-plan` generates transparent `assets/art-spritesheet.png` through icon spritesheet generation, using the same registered art-spec resource ID as `referenceImageSrc` plus concrete `iconDescriptions`.
|
||||
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require response `sliceLayout: "grid-2x2"` and exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
For a playable Canvas game, do not stop at generation. Make `code-prototype` depend on `art-asset-plan` and consume the persisted `iconImageSrcs` slices for core players, blocks or targets, scene obstacles, and feedback. For the four-category game-chat contract, require exactly four slices before registering the local runtime sheet; both fewer and extra components fail closed. Treat `art-spec.png` as reference-only. A full-sheet `<img>`, CSS background, path-only mention, guessed equal-grid crop, or code-drawn replacement for core entities is not runtime asset use. If slicing produces `sliceWarning`, keep the complete transparent sheet as a valid editor artifact, but fail the playable game asset gate until real slice files or verified atlas coordinates exist; never invent coordinates or replace the icon-spritesheet route with ordinary image generation.
|
||||
|
||||
Never use `assets/ui-prototype.png` as the spritesheet visual-spec reference. UI extraction is outside this canonical DAG.
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ A minimal `canvasCompletion` is:
|
||||
}
|
||||
```
|
||||
|
||||
`dialogId` is optional. The placeholder supplies canvas placement and completion coordinates; it is not a final media pixel-size constraint. For successful pixel-art snapping, the result layer uses the final logical-grid PNG dimensions even when they differ from the placeholder. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
`dialogId` is optional. Do not reconstruct canvas state from completion results. Reload the project and asset library when complete authoritative snapshots are needed.
|
||||
|
||||
Character animation accepts `assetFolderId` and `assetLabel` and persists the final transparent sequence directly. Its completed compact result includes the authoritative `assetKind="character-animation"` resource and asset with `imageSequenceFrames` and `imageSequenceDurationMs`. Use those records directly and never synthesize a duplicate asset from the first frame.
|
||||
|
||||
@@ -104,8 +104,6 @@ For the lower-level asset/resource creation endpoints, `generationInputs` is rep
|
||||
|
||||
## Art Spec and Image Request
|
||||
|
||||
Generic External v1 image generation does not expose the main-site structured game-scene contract. `kind: "scene"` and `assetKind: "scene"` are both invalid and return HTTP `400` before any generation job is queued. Do not replace the structured scene fields and server-owned prompt assembly with a generic image prompt.
|
||||
|
||||
Carry the current art spec in `generationInputs.artSpec` and reflect important constraints in the prompt:
|
||||
|
||||
```json
|
||||
@@ -145,7 +143,7 @@ Carry the current art spec in `generationInputs.artSpec` and reflect important c
|
||||
The top-level `style` field is not the art spec's visual-style prose. It appends a short server-side clause to the prompt sent to the provider and enables deterministic post-processing:
|
||||
|
||||
- Omitted, `null`, empty string, or `"none"`: no clause is appended and no post-processing runs, without warning.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. On successful snapping, each detected grid cell becomes one output pixel and the logical-grid PNG is persisted directly; it is not resized back to `size`, `imageSize`, the provider image, or `canvasCompletion.placeholder`. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- `"pixelArt"`: append one short pixel-art line to the end of the prompt sent to the provider, and enable pixel-art snapping, for ordinary image generation, `kind: "character"`, and icon spritesheet generation. The line is appended, not substituted — the rest of your prompt is unchanged. For the exact per-kind wording, read the `style` field description in the OpenAPI document; it is the contract, and this guide deliberately does not copy it.
|
||||
- Unknown strings, or `"pixelArt"` on unsupported kinds such as `spec`, `quick-edit`, `ui-design`, or `publication-material`: continue without style processing and return `warning.code: "unsupported-image-style"`.
|
||||
- Non-string JSON values: malformed request, HTTP `400`.
|
||||
|
||||
@@ -169,10 +167,6 @@ client.generate_image(
|
||||
)
|
||||
```
|
||||
|
||||
Image edit/redraw has a stricter main-source identity rule. After upload confirmation, create either a project resource or an asset-library record and pass its `resourceId` or `assetId` as `sourceReferenceId`. Do not pass the uploaded objectKey as the main source; objectKey remains valid only for auxiliary `referenceImageSrcs` where the OpenAPI permits it.
|
||||
|
||||
Icon spritesheet generation has a stricter primary-spec contract. After upload confirmation, create a project resource or asset record with `assetKind: "icon-spec"`, retain its returned `resourceId` or `assetId`, and pass that ID as `referenceId`. The primary spec does not accept the uploaded `objectKey` directly; only additional style references may continue to use stable object keys in `referenceImageSrcs`.
|
||||
|
||||
For character animation from a local-only source, use actual dimensions and a stable synthetic layer ID:
|
||||
|
||||
```json
|
||||
@@ -198,8 +192,7 @@ Do not guess dimensions or pass a temporary signed read URL. See `authentication
|
||||
|
||||
The completed `result` may contain stable artifact fields such as:
|
||||
|
||||
- `objectKey`, media type, dimensions, or task ID. For successful `pixelArt`, image `width`/`height`, icon `spritesheetWidth`/`spritesheetHeight`, and nested resource/asset dimensions are the actual final logical-grid PNG dimensions rather than requested, provider, or placeholder dimensions.
|
||||
- Sound-effect `durationSeconds` is the probed MP3 duration and `loop` is the frozen request boolean; neither is inferred from Prompt text.
|
||||
- `objectKey`, media type, dimensions, or task ID.
|
||||
- `resource`, `resourceId`, or equivalent canvas reference.
|
||||
- `asset`, `assetId`, or equivalent library reference.
|
||||
- `spritesheetResource`, `spritesheetAsset`, and stable spritesheet metadata.
|
||||
@@ -211,10 +204,6 @@ It deliberately excludes a complete project/canvas/library snapshot, Data URL, B
|
||||
|
||||
Interpret warnings only after the query reaches `status=completed`. The query-level `warning` is display-ready text. Compact `result.warning` and `result.sliceWarning` preserve structured artifact semantics.
|
||||
|
||||
### Delivery-size normalization result before pixel-art snapping
|
||||
|
||||
`result.warning.code: "dimension-restore-fallback"` records only the delivery-size normalization result established before any subsequent `pixelArt` snapping: the provider image could not be safely normalized, so its dimensions were preserved at that processing boundary. It does not describe or constrain the dimensions after `pixelArt`; if snapping succeeds, use the completed result's actual logical-grid dimensions as authoritative.
|
||||
|
||||
### Source-preserved post-processing failure
|
||||
|
||||
When `result.warning.code` is `postprocess-failed-source-preserved`:
|
||||
|
||||
@@ -548,28 +548,22 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def edit_image(self, prompt: str, source_reference_id: str, **fields: Any) -> Any:
|
||||
source_reference_id = source_reference_id.strip()
|
||||
if not source_reference_id:
|
||||
raise GenarrativeApiError("source_reference_id must be a registered resource or asset ID")
|
||||
def edit_image(self, prompt: str, source_image_src: str, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 1024, 1024)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/images/edits",
|
||||
{"prompt": prompt, "sourceReferenceId": source_reference_id, **fields},
|
||||
{"prompt": prompt, "sourceImageSrc": source_image_src, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_icon_spritesheet(
|
||||
self,
|
||||
reference_id: str,
|
||||
reference_image_src: str,
|
||||
icon_descriptions: list[str],
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
reference_id = normalize_optional_text(reference_id)
|
||||
if not reference_id:
|
||||
raise GenarrativeApiError("reference_id must be a registered icon-spec resource or asset ID")
|
||||
descriptions = [item.strip() for item in icon_descriptions if item.strip()]
|
||||
if not descriptions:
|
||||
raise GenarrativeApiError("icon_descriptions must contain at least one non-empty item")
|
||||
@@ -580,9 +574,9 @@ class GenarrativeExternalClient:
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/icon-spritesheets/generations",
|
||||
{
|
||||
**fields,
|
||||
"referenceId": reference_id,
|
||||
"referenceImageSrc": reference_image_src,
|
||||
"iconDescriptions": descriptions,
|
||||
**fields,
|
||||
},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
@@ -655,19 +649,13 @@ class GenarrativeExternalClient:
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
def generate_sound_effect(
|
||||
self,
|
||||
prompt: str,
|
||||
duration: float | None = None,
|
||||
loop: bool = False,
|
||||
**fields: Any,
|
||||
) -> Any:
|
||||
def generate_sound_effect(self, prompt: str, duration: int, **fields: Any) -> Any:
|
||||
self._apply_canvas_session_fields(fields, prompt, 360, 120)
|
||||
prompt = self._apply_art_spec(fields, prompt)
|
||||
idempotency_key = fields.pop("idempotencyKey", None)
|
||||
return self.submit_and_wait_generation(
|
||||
"/api/external/v1/editor/audios/sound-effects/generations",
|
||||
{"prompt": prompt, "duration": duration, "loop": loop, **fields},
|
||||
{"prompt": prompt, "duration": duration, **fields},
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
|
||||
@@ -778,11 +766,9 @@ def _self_test() -> None:
|
||||
["蛇头向上", "蛇身直线", "转角", "尾部", "四类食物"],
|
||||
canvasSession=session,
|
||||
assetLabel="贪吃蛇透明图集",
|
||||
referenceId="must-not-override-explicit-reference",
|
||||
iconDescriptions=["不得覆盖显式图标描述"],
|
||||
)
|
||||
assert calls[0]["path"] == "/api/external/v1/editor/icon-spritesheets/generations"
|
||||
assert calls[0]["body"]["referenceId"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["referenceImageSrc"] == "editor-resource-spec"
|
||||
assert calls[0]["body"]["screenColor"] == "auto"
|
||||
assert calls[0]["body"]["iconDescriptions"][0] == "蛇头向上"
|
||||
assert calls[1]["path"] == "/api/external/v1/generations/task-operation-demo"
|
||||
|
||||
@@ -130,11 +130,6 @@ VECTOR_ENGINE_BASE_URL="https://api.vectorengine.cn"
|
||||
VECTOR_ENGINE_API_KEY=""
|
||||
VECTOR_ENGINE_IMAGE_REQUEST_TIMEOUT_MS="1000000"
|
||||
|
||||
# ElevenLabs editor sound-effect generation is server-side only.
|
||||
ELEVENLABS_BASE_URL="https://api.elevenlabs.io"
|
||||
ELEVENLABS_API_KEY=""
|
||||
ELEVENLABS_REQUEST_TIMEOUT_MS="180000"
|
||||
|
||||
# 阿里云 OSS 配置。
|
||||
# Rust `server-rs` 的 `api-server` 会优先从 `.env` / `.env.local` 读取这些变量,
|
||||
# 用于签发浏览器 PostObject 直传票据,并保持 `/generated-*` 旧路径习惯。
|
||||
|
||||
@@ -46,7 +46,6 @@ module.exports = {
|
||||
{
|
||||
files: [
|
||||
'apps/admin-web/src/pages/*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx',
|
||||
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
|
||||
],
|
||||
rules: {
|
||||
@@ -73,7 +72,6 @@ module.exports = {
|
||||
'src/components/image-editor/**/*.{ts,tsx}',
|
||||
'src/components/project/**/*.{ts,tsx}',
|
||||
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
|
||||
'src/components/platform-entry/PlatformActiveMobileWelcomeDialog*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
|
||||
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
|
||||
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
|
||||
|
||||
@@ -69,8 +69,23 @@ jobs:
|
||||
- name: Install npm dependencies
|
||||
run: bash scripts/ci-npm-ci-with-retry.sh
|
||||
|
||||
- name: Run repository checks
|
||||
run: npm run check:repository-ci
|
||||
- name: Run repository lint gates
|
||||
run: npm run lint
|
||||
|
||||
- name: Build web applications
|
||||
run: npm run build
|
||||
|
||||
- name: Validate content data
|
||||
run: npm run check:content
|
||||
|
||||
- name: Check committed whitespace
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
base_ref="${SPACETIME_SCHEMA_BASE_REF:-}"
|
||||
test -n "${base_ref}"
|
||||
git cat-file -e "${base_ref}^{commit}"
|
||||
git diff --check "${base_ref}"...HEAD
|
||||
|
||||
frontend-tests:
|
||||
name: Frontend tests
|
||||
|
||||
@@ -39,8 +39,6 @@ temp*build*/
|
||||
/apps/ai-game-creator-shell/game-creator.config.local.json
|
||||
/apps/mobile-shell/.expo/
|
||||
/apps/mobile-shell/.expo-export-smoke/
|
||||
/apps/preview-deployer-web/dist/
|
||||
/apps/preview-deployer-web/node_modules/
|
||||
/server-rs/.spacetimedb/
|
||||
/server-rs/.data/
|
||||
/public/generated-animations
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name: genarrative-dev-stack-port-routing
|
||||
short_description: 修改 Genarrative 本地 dev 启动端口、代理目标、端口冲突处理时使用。
|
||||
description: 在 Genarrative 中修改 npm run dev / dev:spacetime / dev:api-server / dev:bgfilter-worker / dev:web / dev:admin-web 的本地启动端口、端口可用性探测、端口漂移、SpacetimeDB publish server、Rust 进程环境变量、Vite 代理目标和后台 admin-web 启动串联时使用。
|
||||
version: 1.2.0
|
||||
version: 1.1.0
|
||||
author: Hermes Agent
|
||||
license: MIT
|
||||
metadata:
|
||||
@@ -17,7 +17,7 @@ metadata:
|
||||
|
||||
## 适用场景
|
||||
|
||||
- 修改 `scripts/dev.mjs`、`scripts/dev-utils.mjs`、`scripts/dev-stack-port-utils.mjs` 或 AI 游戏创作客户端 dev 启动器。
|
||||
- 修改 `scripts/dev.mjs`、`scripts/dev-utils.mjs`、`scripts/dev-stack-port-utils.mjs`。
|
||||
- 处理 `3000`、`3101`、`3102`、`8082` 等端口被占用导致本地开发栈启动失败。
|
||||
- 排查 Vite 代理仍指向旧 api-server 端口、前端打开了旧 dev server、后台代理错配。
|
||||
- 调整 SpacetimeDB standalone、publish、Rust `api-server`、主站 Vite、后台 Vite 的启动顺序。
|
||||
@@ -32,11 +32,10 @@ metadata:
|
||||
3. SpacetimeDB standalone:`3101`,健康检查为 `http://127.0.0.1:<spacetime-port>/v1/ping`。
|
||||
4. 后台 Vite:`3102`,后台地址为 `http://127.0.0.1:<admin-web-port>/admin/`。
|
||||
5. 独立 BgFilter worker:`8083`,就绪检查为 `http://127.0.0.1:<bgfilter-worker-port>/readyz`。
|
||||
6. AI 游戏创作 Vite:非 Linux 兼容首选 `3080`;Linux 使用当前用户端口段的 `start + 5`。
|
||||
|
||||
端口不可用时,脚本会从优先端口开始向后寻找可用端口。后续流程必须以解析后的实际端口为准,不能继续使用默认端口。
|
||||
|
||||
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`adminWeb = start + 3`、`bgfilterWorker = start + 4`、`agcVite = start + 5`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有统一端口探测和漂移逻辑,不读系统级注册表。
|
||||
Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range` 会先向系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json` 申请一个端口段,再把该段映射为 `web = start`、`api = start + 1`、`spacetime = start + 2`、`adminWeb = start + 3`、`bgfilterWorker = start + 4`。注册表锁文件是 `/var/tmp/genarrative-dev-port-ranges/registry.lock`,可通过 `GENARRATIVE_DEV_PORT_RANGE_REGISTRY_DIR` 覆盖目录。自动分配从 `10000-10099` 起,每次占用 100 个端口块,后续块按 `10100-10199`、`10200-10299` 递增;当前口径是“一个用户固定占用一个段,后续启动继续复用这段并在段内漂移”;该注册表只在 Linux 上生效;Windows 继续沿用原有统一端口探测和漂移逻辑,不读系统级注册表。
|
||||
|
||||
## 实现入口
|
||||
|
||||
@@ -52,12 +51,9 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
|
||||
- `scripts/dev.mjs`
|
||||
- 解析 CLI 参数后统一计算 client host、端口、`SPACETIME_SERVER`、`RUST_SERVER_TARGET`。
|
||||
- 完整栈按 SpacetimeDB、publish、BgFilter worker readiness、api-server readiness、主站 Vite、后台 Vite 顺序启动。
|
||||
- Linux 下会先申请系统级端口段并映射成六个预留槽位;主 dev 栈使用前五个,AGC Vite 使用 `start + 5`,自动分配从 `10000-10099` 起。Windows 的主 dev 栈和 AGC 则各自沿用统一端口探测与漂移逻辑。
|
||||
- Linux 下会先申请系统级端口段并把它映射成五个 dev 端口;自动分配从 `10000-10099` 起,Windows 则把第五个服务纳入原有统一参数解析与端口漂移逻辑。
|
||||
- 完整栈和 `dev:api-server` 把两个 Rust 进程作为同一重启单元,先全部停止,再先启动 BgFilter worker、后启动 api-server;不要为同一份 Rust 源码创建两个并发 `cargo` watcher。
|
||||
- 单模块命令复用同一套参数和 env 解析。
|
||||
- `apps/ai-game-creator-shell/scripts/dev-port.mjs`
|
||||
- 复用系统级用户端口段,解析 AGC Vite 的 `start + 5` 首选槽位。
|
||||
- 把最终端口通过 `GENARRATIVE_AGC_VITE_PORT` 同步给 `beforeDevCommand` 和配套后端端口解析器,通过 Tauri CLI `--config` 同步 `build.devUrl`,并通过 Vite CLI `--port` 同步 `strictPort` 监听。
|
||||
|
||||
## 必须保持的传递链路
|
||||
|
||||
@@ -73,7 +69,6 @@ Linux 多用户并发开发时,`GENARRATIVE_DEV_PORT_RANGE` 或 `--port-range`
|
||||
8. 后台 Vite:`ADMIN_API_TARGET`、`GENARRATIVE_API_TARGET`、`GENARRATIVE_API_PORT`、`--port=${ADMIN_WEB_PORT}`。
|
||||
9. 控制台日志:`[dev:ports]` 和 `[dev] web/admin web/api-server/bgfilter-worker/spacetime` 必须显示最终实际地址。
|
||||
10. Linux 端口段注册:`[dev] port-range:` 与 `[dev] port-range-registry:` 只在 Linux 输出,Windows 不应依赖系统级注册表。
|
||||
11. AI 游戏创作客户端:外层启动器解析最终 AGC Vite 端口后,通过 Tauri CLI `--config` 覆盖 `build.devUrl`,把同一 `GENARRATIVE_AGC_VITE_PORT` 传给 `beforeDevCommand` 与配套后端端口解析器,并用 Vite CLI `--port` 启动严格监听;后端端口漂移必须跳过该预留端口。
|
||||
|
||||
如果只改了其中一段,通常会出现:浏览器打开的前端可用,但 `/api/*` 代理到旧端口;后台页面可用但后台 API 失败;SpacetimeDB 启动在新端口但 publish 仍发往旧端口。
|
||||
|
||||
@@ -133,7 +128,6 @@ node scripts/dev-stack-port-utils.mjs resolve-dev-stack spacetime:127.0.0.1:0 ap
|
||||
- [ ] `npm run dev` 的 SpacetimeDB、publish、api-server、主站 Vite、后台 Vite 都使用实际端口。
|
||||
- [ ] BgFilter worker 在 api-server 前 ready,父子共享实际 base URL / Token,Rust watch 只触发一次组合重启。
|
||||
- [ ] `npm run dev:web` 在主站端口不可用时能切换到可用端口。
|
||||
- [ ] `npm run agc` / `npm run agc:game-chat` 在 Linux 使用用户段 `start + 5`,Tauri、Vite、marker 和预检使用同一最终端口。
|
||||
- [ ] 文档同步更新 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。
|
||||
- [ ] 长期踩坑同步更新 `docs/project-memory/shared-memory/pitfalls.md`。
|
||||
- [ ] 修改中文文件后运行 `npm run check:encoding`。
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
npm run format:staged
|
||||
@@ -1 +0,0 @@
|
||||
npm run check:pre-push-master -- "$@"
|
||||
@@ -26,12 +26,6 @@
|
||||
- 后续新增 Markdown 文档文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;历史文档不要求批量重命名,除非本次任务明确涉及。
|
||||
- 工程修改要同步更新对应 `docs/` 文档;产生长期有效的架构约定、接口变化、排障经验、开发流程或协作规则时,同步更新 `docs/project-memory/shared-memory/`。
|
||||
- 默认保持系统简洁:优先复用、修改、扩展现有系统、页面和公共组件,不新建平行系统或平行页面。
|
||||
- 对已明确退役且不存在现役调用方、公开契约、持久化数据、活跃实例或迁移要求的对象,坚持“四不写”:
|
||||
1. 不写历史兼容代码。
|
||||
2. 不写用于维持退役行为的防御性兼容测试。
|
||||
3. 不写仅说明其曾存在或已删除的墓碑注释。
|
||||
4. 不写仅记录其已删除的墓碑文档;直接将权威文档更新为当前状态。
|
||||
- 公开 API、持久化数据、SpacetimeDB schema、跨版本重放、活跃实例和正式迁移不适用“四不写”;必要兼容应最小化、白名单化并配套契约或迁移测试,迁移完成后同步删除兼容实现与对应测试。
|
||||
- UI 面板中不要默认写功能说明、规则描述或开发解释文案;移动端优先,同时保证网页端可正常显示和操作。
|
||||
- 点击按钮弹出独立面板的设计,不要实现成在当前面板下面追加内容。
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
{
|
||||
"agentMode": "codex_app_server",
|
||||
"llm": {
|
||||
"apiKey": "",
|
||||
"baseUrl": "https://dev.genarrative.world/gpt/v1",
|
||||
"model": "gpt-5.6-sol",
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"model": "gpt-4.1",
|
||||
"apiKind": "openai_responses",
|
||||
"reasoningEffort": "max",
|
||||
"stream": true,
|
||||
"reasoningEffort": "high",
|
||||
"stream": false,
|
||||
"webSearchEnabled": false,
|
||||
"contextWindowTokens": 128000,
|
||||
"autoCompactTokenLimit": 64000,
|
||||
@@ -16,5 +15,9 @@
|
||||
"retryBackoffMs": 500
|
||||
},
|
||||
"agentLlm": {},
|
||||
"editorApi": {
|
||||
"baseUrl": "http://127.0.0.1:8082",
|
||||
"apiKey": ""
|
||||
},
|
||||
"mcpServers": {}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>陶泥儿</title>
|
||||
<title>AI 游戏创作</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
-11
@@ -10,9 +10,7 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lexical": "^0.47.0",
|
||||
@@ -2210,15 +2208,6 @@
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-http": {
|
||||
"version": "2.5.9",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.9.tgz",
|
||||
"integrity": "sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-opener": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz",
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat",
|
||||
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense",
|
||||
"agent-runtime:supervisor-game-chat-single-main-playable-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs",
|
||||
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test",
|
||||
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
|
||||
@@ -37,9 +36,7 @@
|
||||
"dependencies": {
|
||||
"@lexical/react": "^0.47.0",
|
||||
"@lexical/utils": "^0.47.0",
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||
"@tauri-apps/plugin-http": "^2.5.9",
|
||||
"@tauri-apps/plugin-opener": "~2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"lexical": "^0.47.0",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2406,36 +2406,11 @@ export function isolatedJoinDeliveryTarget(delivery) {
|
||||
return target;
|
||||
}
|
||||
|
||||
function runtimeMessageCorrelationId(agentId, sessionId, runId) {
|
||||
return createHash('sha256')
|
||||
export function finalMessageId(agentId, sessionId, runId) {
|
||||
const fingerprint = createHash('sha256')
|
||||
.update(`${agentId}\n${sessionId}\n${runId}`)
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
export function finalMessageId(agentId, sessionId, runId) {
|
||||
return `agent-finalization-${runtimeMessageCorrelationId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
).slice(0, 32)}`;
|
||||
}
|
||||
|
||||
export function runtimePublicStatusMessageId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
status,
|
||||
) {
|
||||
const correlationId = runtimeMessageCorrelationId(
|
||||
agentId,
|
||||
sessionId,
|
||||
runId,
|
||||
).slice(0, 32);
|
||||
const statusFingerprint = createHash('sha256')
|
||||
.update(status)
|
||||
.digest('hex')
|
||||
.slice(0, 16);
|
||||
return `runtime-public-status-${correlationId}-${statusFingerprint}`;
|
||||
return `agent-finalization-${fingerprint.slice(0, 32)}`;
|
||||
}
|
||||
|
||||
export function backgroundTaskMessageId(agentId, sessionId, runId, source) {
|
||||
@@ -2469,10 +2444,7 @@ export function disposableProjectPathVariants() {
|
||||
}
|
||||
|
||||
export function formalConfigPathVariants() {
|
||||
return absolutePathVariants(
|
||||
state.options?.configDir,
|
||||
state.isolatedRunner?.appDataDir,
|
||||
);
|
||||
return absolutePathVariants(state.options?.configDir);
|
||||
}
|
||||
|
||||
export function absolutePathVariants(...values) {
|
||||
|
||||
@@ -20,15 +20,7 @@ import {
|
||||
stopOwnedIsolatedRunner,
|
||||
} from './harness/app-data.mjs';
|
||||
import { loadConfig, parseArguments } from './harness/config.mjs';
|
||||
import {
|
||||
activeInteractiveCliSessions,
|
||||
captureOwnedProcessCleanupSnapshot,
|
||||
closeInteractiveCli,
|
||||
destroyInteractiveCliOutputStreams,
|
||||
interactiveCliOutput,
|
||||
verifyOwnedProcessCleanupSnapshot,
|
||||
waitForInteractiveCliStdioClose,
|
||||
} from './harness/process.mjs';
|
||||
import { closeInteractiveCli } from './harness/process.mjs';
|
||||
import { checkPrerequisites } from './harness/project.mjs';
|
||||
import {
|
||||
buildSummary,
|
||||
@@ -274,21 +266,6 @@ if (selfTestRequested) {
|
||||
recordError(error?.code ?? 'unexpected-error', error);
|
||||
} finally {
|
||||
state.cleanupInProgress = true;
|
||||
const stateTrackedInteractiveCliSessions = new Set(
|
||||
[
|
||||
state.userInputCliSession,
|
||||
state.supervisorAutonomousPlayableCliSession,
|
||||
state.supervisorSwarmCliSession,
|
||||
].filter(Boolean),
|
||||
);
|
||||
const interactiveCliSessions = [
|
||||
...new Set([
|
||||
...stateTrackedInteractiveCliSessions,
|
||||
...activeInteractiveCliSessions,
|
||||
]),
|
||||
];
|
||||
const supervisorAutonomousPlayableCliSession =
|
||||
state.supervisorAutonomousPlayableCliSession;
|
||||
if (isUserInputRuntimeSuite() && state.userInputCliSession) {
|
||||
try {
|
||||
await closeInteractiveCli(state.userInputCliSession);
|
||||
@@ -334,15 +311,6 @@ if (selfTestRequested) {
|
||||
}
|
||||
state.supervisorSwarmCliSession = null;
|
||||
}
|
||||
for (const session of interactiveCliSessions) {
|
||||
if (stateTrackedInteractiveCliSessions.has(session)) continue;
|
||||
try {
|
||||
await closeInteractiveCli(session);
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError('interactive-cli-cleanup-failed', error);
|
||||
}
|
||||
}
|
||||
if (isMcpRuntimeSuite() && state.mcp.httpFixture) {
|
||||
try {
|
||||
await stopMcpHttpFixture();
|
||||
@@ -352,42 +320,16 @@ if (selfTestRequested) {
|
||||
recordError('mcp-http-fixture-cleanup-failed', error);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
state.isolatedRunner.appDataDir
|
||||
) {
|
||||
try {
|
||||
const runnerPid = state.isolatedRunner.current?.pid ?? null;
|
||||
const helperPid =
|
||||
state.isolatedRunner.current?.killHandle?.child?.pid ?? null;
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot =
|
||||
await captureOwnedProcessCleanupSnapshot({
|
||||
runnerPid,
|
||||
helperPids: Number.isSafeInteger(helperPid) ? [helperPid] : [],
|
||||
rootPids: [
|
||||
...interactiveCliSessions.map((session) => session.child?.pid),
|
||||
...[...activeCommandChildren].map((child) => child.pid),
|
||||
].filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
||||
});
|
||||
const observed =
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot
|
||||
.observedCounts;
|
||||
assert(
|
||||
observed.runner === 1 && observed.helper === 1,
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-incomplete',
|
||||
);
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
|
||||
try {
|
||||
await stopOwnedIsolatedRunner();
|
||||
state.isolatedRunner.stopped = true;
|
||||
state.isolatedRunner.cleanupPerformed =
|
||||
await removeIsolatedSuiteAppData();
|
||||
if (!state.isolatedRunner.cleanupPerformed) {
|
||||
state.status = 'FAIL';
|
||||
recordError('isolated-appdata-cleanup-sentinel-missing');
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
const safeCleanupErrorCode =
|
||||
@@ -400,51 +342,8 @@ if (selfTestRequested) {
|
||||
state.isolatedRunner.current?.killHandle,
|
||||
).catch(() => {});
|
||||
}
|
||||
}
|
||||
for (const session of interactiveCliSessions) {
|
||||
try {
|
||||
await waitForInteractiveCliStdioClose(session, 10_000);
|
||||
} catch (error) {
|
||||
destroyInteractiveCliOutputStreams(session);
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
error?.code === 'interactive-cli-stdio-close-timeout'
|
||||
? error.code
|
||||
: 'interactive-cli-stdio-cleanup-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (supervisorAutonomousPlayableCliSession) {
|
||||
state.supervisorAutonomousPlayable.cliOutput = interactiveCliOutput(
|
||||
supervisorAutonomousPlayableCliSession,
|
||||
);
|
||||
}
|
||||
if (isIsolatedRunnerSuite() && state.isolatedRunner.appDataDir) {
|
||||
if (state.isolatedRunner.stopped) {
|
||||
try {
|
||||
state.isolatedRunner.cleanupPerformed =
|
||||
await removeIsolatedSuiteAppData();
|
||||
if (!state.isolatedRunner.cleanupPerformed) {
|
||||
state.status = 'FAIL';
|
||||
recordError('isolated-appdata-cleanup-sentinel-missing');
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
const safeCleanupErrorCode =
|
||||
isNonEmptyString(error?.code) &&
|
||||
/^(?:isolated|source)-[a-z0-9-]+$/u.test(error.code)
|
||||
? error.code
|
||||
: 'isolated-appdata-cleanup-failed';
|
||||
recordError(safeCleanupErrorCode, error);
|
||||
}
|
||||
}
|
||||
const killMethod =
|
||||
state.isolatedRunner.pidfdClaimCount > 0
|
||||
? process.platform === 'win32'
|
||||
? 'windows-process-handle'
|
||||
: 'linux-pidfd'
|
||||
: null;
|
||||
state.isolatedRunner.pidfdClaimCount > 0 ? 'linux-pidfd' : null;
|
||||
if (isSteerRunnerKillSuite()) {
|
||||
state.evidence.steerRunnerStopped = state.isolatedRunner.stopped;
|
||||
state.evidence.steerAppDataCleanupPerformed =
|
||||
@@ -762,61 +661,6 @@ if (selfTestRequested) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
|
||||
state.isolatedRunner.appDataDir
|
||||
) {
|
||||
const snapshot =
|
||||
state.supervisorAutonomousPlayable.ownedProcessCleanupSnapshot;
|
||||
if (snapshot) {
|
||||
try {
|
||||
const cleanup = await verifyOwnedProcessCleanupSnapshot(snapshot);
|
||||
state.evidence.ownedProcessIdentityCaptured = true;
|
||||
state.evidence.ownedRunnerObservedCount =
|
||||
snapshot.observedCounts.runner;
|
||||
state.evidence.ownedHelperObservedCount =
|
||||
snapshot.observedCounts.helper;
|
||||
state.evidence.ownedNodeDescendantObservedCount =
|
||||
snapshot.observedCounts.node;
|
||||
state.evidence.ownedBrowserDescendantObservedCount =
|
||||
snapshot.observedCounts.browser;
|
||||
state.evidence.ownedCommandDescendantObservedCount =
|
||||
snapshot.observedCounts.command;
|
||||
state.evidence.ownedRunnerResidualCount =
|
||||
cleanup.residualCounts.runner;
|
||||
state.evidence.ownedHelperResidualCount =
|
||||
cleanup.residualCounts.helper;
|
||||
state.evidence.ownedNodeDescendantResidualCount =
|
||||
cleanup.residualCounts.node;
|
||||
state.evidence.ownedBrowserDescendantResidualCount =
|
||||
cleanup.residualCounts.browser;
|
||||
state.evidence.ownedCommandDescendantResidualCount =
|
||||
cleanup.residualCounts.command;
|
||||
state.evidence.activeCommandChildrenAfterCleanup =
|
||||
cleanup.activeCommandChildCount;
|
||||
state.evidence.activeInteractiveCliSessionsAfterCleanup =
|
||||
cleanup.activeInteractiveCliSessionCount;
|
||||
state.evidence.ownedProcessCleanupPassed = cleanup.clean;
|
||||
if (!cleanup.clean) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-residual-detected',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-verification-failed',
|
||||
error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
state.status = 'FAIL';
|
||||
recordError(
|
||||
'supervisor-autonomous-playable-owned-process-snapshot-missing',
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
isSteerRunnerKillSuite() &&
|
||||
state.projectRoot &&
|
||||
@@ -1366,7 +1210,6 @@ if (selfTestRequested) {
|
||||
const safeSummary = {
|
||||
status: state.status,
|
||||
suite: state.suite,
|
||||
providerUsed: false,
|
||||
blocked: state.blocked,
|
||||
cleanup: {
|
||||
performed: state.cleanupPerformed,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@ import {
|
||||
scopedAgentsSuite,
|
||||
steerRunnerKillSuite,
|
||||
supervisorAutonomousPlayableLaneDefenseSuite,
|
||||
supervisorGameChatSingleMainPlayableSuite,
|
||||
supervisorSwarmAutonomousChatSuite,
|
||||
supervisorSwarmCollaborationPolicyMixedRecoverySuite,
|
||||
supervisorSwarmFinalReplyTransientRetrySuite,
|
||||
@@ -71,7 +70,6 @@ export function parseArguments(args) {
|
||||
suite === supervisorSwarmToolPlanHandoffRunnerKillSuite ||
|
||||
suite === supervisorSwarmAutonomousChatSuite ||
|
||||
suite === supervisorAutonomousPlayableLaneDefenseSuite ||
|
||||
suite === supervisorGameChatSingleMainPlayableSuite ||
|
||||
suite === supervisorSwarmStaticIsolatedAutonomousChatSuite ||
|
||||
suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite ||
|
||||
suite === steerRunnerKillSuite ||
|
||||
|
||||
@@ -19,8 +19,6 @@ import {
|
||||
} from '../suites/supervisor-swarm.mjs';
|
||||
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
|
||||
export const activeInteractiveCliSessions = new Set();
|
||||
|
||||
export async function prepareCliBinary() {
|
||||
const cargo = process.platform === 'win32' ? 'cargo.exe' : 'cargo';
|
||||
await runProcess(
|
||||
@@ -156,70 +154,36 @@ export function startInteractiveCli(args) {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
},
|
||||
);
|
||||
return createInteractiveCliSession(child);
|
||||
}
|
||||
|
||||
export function createInteractiveCliSession(child) {
|
||||
activeCommandChildren.add(child);
|
||||
const session = {
|
||||
child,
|
||||
stdout: Buffer.alloc(0),
|
||||
stderr: Buffer.alloc(0),
|
||||
exited: false,
|
||||
exitInfo: null,
|
||||
exitPromise: null,
|
||||
closed: false,
|
||||
closeInfo: null,
|
||||
closePromise: null,
|
||||
stdioClosed: false,
|
||||
stdioCloseInfo: null,
|
||||
spawnError: null,
|
||||
stdinError: null,
|
||||
};
|
||||
activeInteractiveCliSessions.add(session);
|
||||
session.exitPromise = new Promise((resolve) => {
|
||||
const settle = (result) => {
|
||||
if (session.exited) return;
|
||||
activeCommandChildren.delete(child);
|
||||
session.exited = true;
|
||||
session.closed = true;
|
||||
session.exitInfo = result;
|
||||
session.closeInfo = result;
|
||||
resolve(result);
|
||||
};
|
||||
child.once('error', (error) => {
|
||||
session.spawnError = error;
|
||||
settle({ code: null, signal: null, error });
|
||||
});
|
||||
child.once('exit', (code, signal) => {
|
||||
settle({ code, signal, error: null });
|
||||
});
|
||||
});
|
||||
session.closePromise = new Promise((resolve) => {
|
||||
child.once('close', (code, signal) => {
|
||||
child.on('error', (error) => {
|
||||
activeCommandChildren.delete(child);
|
||||
activeInteractiveCliSessions.delete(session);
|
||||
session.stdioClosed = true;
|
||||
session.stdioCloseInfo = {
|
||||
code,
|
||||
signal,
|
||||
error: session.spawnError,
|
||||
};
|
||||
resolve(session.stdioCloseInfo);
|
||||
session.closed = true;
|
||||
session.closeInfo = { code: null, signal: null, error };
|
||||
resolve(session.closeInfo);
|
||||
});
|
||||
child.on('close', (code, signal) => {
|
||||
activeCommandChildren.delete(child);
|
||||
session.closed = true;
|
||||
session.closeInfo = { code, signal, error: null };
|
||||
resolve(session.closeInfo);
|
||||
});
|
||||
});
|
||||
child.stdin?.on('error', (error) => {
|
||||
session.stdinError ??= error;
|
||||
});
|
||||
child.stdout.on('data', (chunk) => {
|
||||
state.transcriptScanner?.scan('interactive-stdout', chunk);
|
||||
state.projectPathTranscriptScanner?.scan('interactive-stdout', chunk);
|
||||
state.formalConfigPathTranscriptScanner?.scan('interactive-stdout', chunk);
|
||||
session.stdout = appendBounded(session.stdout, chunk, commandOutputLimit);
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
state.transcriptScanner?.scan('interactive-stderr', chunk);
|
||||
state.projectPathTranscriptScanner?.scan('interactive-stderr', chunk);
|
||||
state.formalConfigPathTranscriptScanner?.scan('interactive-stderr', chunk);
|
||||
session.stderr = appendBounded(session.stderr, chunk, commandOutputLimit);
|
||||
});
|
||||
@@ -241,23 +205,16 @@ export async function waitForInteractiveCliOutput(
|
||||
predicate,
|
||||
code,
|
||||
timeoutMs,
|
||||
{ allowAfterProcessExit = false } = {},
|
||||
) {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const output = interactiveCliOutput(session);
|
||||
if (predicate(output)) return output;
|
||||
if (session.exited && !allowAfterProcessExit) {
|
||||
if (session.closed) {
|
||||
if (session === state.supervisorSwarmCliSession) {
|
||||
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
|
||||
}
|
||||
throw codedError(`${code}-cli-exited`);
|
||||
}
|
||||
if (session.stdioClosed) {
|
||||
if (session === state.supervisorSwarmCliSession) {
|
||||
recordSupervisorSwarmChatSessionFailureDiagnostic(session);
|
||||
}
|
||||
throw codedError(`${code}-cli-stdio-closed`);
|
||||
throw codedError(`${code}-cli-closed`);
|
||||
}
|
||||
await sleep(50);
|
||||
}
|
||||
@@ -266,7 +223,7 @@ export async function waitForInteractiveCliOutput(
|
||||
|
||||
export async function waitForInteractiveCliExit(session, timeoutMs) {
|
||||
const result = await Promise.race([
|
||||
session.exitPromise,
|
||||
session.closePromise,
|
||||
sleep(timeoutMs).then(() => null),
|
||||
]);
|
||||
if (!result) throw codedError('interactive-cli-exit-timeout');
|
||||
@@ -279,52 +236,26 @@ export async function waitForInteractiveCliExit(session, timeoutMs) {
|
||||
}
|
||||
|
||||
export async function closeInteractiveCli(session) {
|
||||
if (!session) return null;
|
||||
if (session.exited) return session.exitInfo;
|
||||
if (
|
||||
session.child.stdin.writable &&
|
||||
!session.child.stdin.writableEnded &&
|
||||
!session.child.stdin.destroyed
|
||||
) {
|
||||
if (!session || session.closed) return;
|
||||
if (session.child.stdin.writable) {
|
||||
session.child.stdin.write('/quit\n');
|
||||
}
|
||||
let result = await Promise.race([
|
||||
session.exitPromise,
|
||||
session.closePromise,
|
||||
sleep(3_000).then(() => null),
|
||||
]);
|
||||
if (!result && !session.exited) {
|
||||
if (!result && !session.closed) {
|
||||
session.child.kill('SIGTERM');
|
||||
result = await Promise.race([
|
||||
session.exitPromise,
|
||||
session.closePromise,
|
||||
sleep(2_000).then(() => null),
|
||||
]);
|
||||
}
|
||||
if (!result && !session.exited) {
|
||||
if (!result && !session.closed) {
|
||||
session.child.kill('SIGKILL');
|
||||
result = await Promise.race([
|
||||
session.exitPromise,
|
||||
sleep(5_000).then(() => null),
|
||||
]);
|
||||
result = await session.closePromise;
|
||||
}
|
||||
assert(Boolean(result), 'interactive-cli-cleanup-timeout');
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function waitForInteractiveCliStdioClose(session, timeoutMs) {
|
||||
if (!session || session.stdioClosed) return session?.stdioCloseInfo ?? null;
|
||||
const result = await Promise.race([
|
||||
session.closePromise,
|
||||
sleep(timeoutMs).then(() => null),
|
||||
]);
|
||||
if (!result) throw codedError('interactive-cli-stdio-close-timeout');
|
||||
return result;
|
||||
}
|
||||
|
||||
export function destroyInteractiveCliOutputStreams(session) {
|
||||
if (!session) return;
|
||||
for (const stream of [session.child.stdout, session.child.stderr]) {
|
||||
if (stream && !stream.destroyed) stream.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export async function runProcess(
|
||||
@@ -398,214 +329,6 @@ export async function runProcess(
|
||||
});
|
||||
}
|
||||
|
||||
export async function listSystemProcessIdentities() {
|
||||
if (process.platform === 'win32') {
|
||||
const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT;
|
||||
assert(
|
||||
typeof systemRoot === 'string' && path.isAbsolute(systemRoot),
|
||||
'owned-process-snapshot-system-root-invalid',
|
||||
);
|
||||
const powershell = path.join(
|
||||
systemRoot,
|
||||
'System32/WindowsPowerShell/v1.0/powershell.exe',
|
||||
);
|
||||
const metadata = await fs.lstat(powershell);
|
||||
assert(
|
||||
metadata.isFile() && !metadata.isSymbolicLink(),
|
||||
'owned-process-snapshot-powershell-invalid',
|
||||
);
|
||||
const result = await runProcess(
|
||||
powershell,
|
||||
[
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
'$processes = @(Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,CreationDate,Name); $processes | ConvertTo-Json -Compress',
|
||||
],
|
||||
{
|
||||
cwd: appRoot,
|
||||
timeoutMs: 30_000,
|
||||
env: { ...process.env, NO_COLOR: '1', RUST_BACKTRACE: '0' },
|
||||
},
|
||||
);
|
||||
const parsed = JSON.parse(result.stdout);
|
||||
return (Array.isArray(parsed) ? parsed : [parsed])
|
||||
.map((record) => ({
|
||||
pid: Number(record?.ProcessId),
|
||||
parentPid: Number(record?.ParentProcessId),
|
||||
startedAt: String(record?.CreationDate ?? ''),
|
||||
name: String(record?.Name ?? ''),
|
||||
}))
|
||||
.filter(validSystemProcessIdentity);
|
||||
}
|
||||
assert(
|
||||
process.platform === 'linux' || process.platform === 'darwin',
|
||||
'owned-process-snapshot-platform-unsupported',
|
||||
);
|
||||
const result = await runProcess(
|
||||
'ps',
|
||||
['-A', '-o', 'pid=', '-o', 'ppid=', '-o', 'lstart=', '-o', 'comm='],
|
||||
{ cwd: appRoot, timeoutMs: 30_000 },
|
||||
);
|
||||
return result.stdout
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const fields = line.split(/\s+/u);
|
||||
return {
|
||||
pid: Number(fields[0]),
|
||||
parentPid: Number(fields[1]),
|
||||
startedAt: fields.slice(2, 7).join(' '),
|
||||
name: fields.slice(7).join(' '),
|
||||
};
|
||||
})
|
||||
.filter(validSystemProcessIdentity);
|
||||
}
|
||||
|
||||
function validSystemProcessIdentity(record) {
|
||||
return (
|
||||
Number.isSafeInteger(record?.pid) &&
|
||||
record.pid > 0 &&
|
||||
Number.isSafeInteger(record.parentPid) &&
|
||||
record.parentPid >= 0 &&
|
||||
typeof record.startedAt === 'string' &&
|
||||
record.startedAt.length > 0 &&
|
||||
typeof record.name === 'string' &&
|
||||
record.name.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOwnedProcessCleanupSnapshot(
|
||||
processRecords,
|
||||
{ rootPids = [], runnerPid = null, helperPids = [] } = {},
|
||||
) {
|
||||
assert(
|
||||
Array.isArray(processRecords) &&
|
||||
Array.isArray(rootPids) &&
|
||||
Array.isArray(helperPids),
|
||||
'owned-process-snapshot-input-invalid',
|
||||
);
|
||||
const records = processRecords.filter(validSystemProcessIdentity);
|
||||
const byPid = new Map(records.map((record) => [record.pid, record]));
|
||||
const childrenByParent = new Map();
|
||||
for (const record of records) {
|
||||
const children = childrenByParent.get(record.parentPid) ?? [];
|
||||
children.push(record.pid);
|
||||
childrenByParent.set(record.parentPid, children);
|
||||
}
|
||||
const normalizedRunnerPid = Number.isSafeInteger(runnerPid)
|
||||
? runnerPid
|
||||
: null;
|
||||
const helperPidSet = new Set(
|
||||
helperPids.filter((pid) => Number.isSafeInteger(pid) && pid > 0),
|
||||
);
|
||||
const roots = [
|
||||
...new Set(
|
||||
[...rootPids, normalizedRunnerPid, ...helperPidSet].filter(
|
||||
(pid) => Number.isSafeInteger(pid) && pid > 0,
|
||||
),
|
||||
),
|
||||
];
|
||||
assert(roots.length > 0, 'owned-process-snapshot-root-missing');
|
||||
const ownedPids = new Set();
|
||||
const queue = [...roots];
|
||||
while (queue.length > 0) {
|
||||
const pid = queue.shift();
|
||||
if (ownedPids.has(pid)) continue;
|
||||
ownedPids.add(pid);
|
||||
queue.push(...(childrenByParent.get(pid) ?? []));
|
||||
}
|
||||
const identities = [...ownedPids]
|
||||
.map((pid) => byPid.get(pid))
|
||||
.filter(Boolean)
|
||||
.map((record) => ({
|
||||
pid: record.pid,
|
||||
startedAt: record.startedAt,
|
||||
name: record.name,
|
||||
kind: ownedProcessKind(record, normalizedRunnerPid, helperPidSet),
|
||||
}))
|
||||
.sort((left, right) => left.pid - right.pid);
|
||||
return {
|
||||
identities,
|
||||
observedCounts: countOwnedProcessKinds(identities),
|
||||
};
|
||||
}
|
||||
|
||||
function ownedProcessKind(record, runnerPid, helperPids) {
|
||||
if (record.pid === runnerPid) return 'runner';
|
||||
if (helperPids.has(record.pid)) return 'helper';
|
||||
const name = path.basename(record.name).toLowerCase();
|
||||
if (/^node(?:\.exe)?$/u.test(name)) return 'node';
|
||||
if (/^(?:chrome|chromium|msedge|google-chrome)(?:\.exe)?$/u.test(name)) {
|
||||
return 'browser';
|
||||
}
|
||||
return 'command';
|
||||
}
|
||||
|
||||
function countOwnedProcessKinds(identities) {
|
||||
const counts = {
|
||||
runner: 0,
|
||||
helper: 0,
|
||||
node: 0,
|
||||
browser: 0,
|
||||
command: 0,
|
||||
total: identities.length,
|
||||
};
|
||||
for (const identity of identities) counts[identity.kind] += 1;
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function inspectOwnedProcessCleanupResiduals(
|
||||
snapshot,
|
||||
processRecords,
|
||||
{ activeCommandChildCount = 0, activeInteractiveCliSessionCount = 0 } = {},
|
||||
) {
|
||||
assert(
|
||||
Array.isArray(snapshot?.identities) && Array.isArray(processRecords),
|
||||
'owned-process-residual-input-invalid',
|
||||
);
|
||||
const currentByPid = new Map(
|
||||
processRecords
|
||||
.filter(validSystemProcessIdentity)
|
||||
.map((record) => [record.pid, record]),
|
||||
);
|
||||
const residualIdentities = snapshot.identities.filter((identity) => {
|
||||
const current = currentByPid.get(identity.pid);
|
||||
return (
|
||||
current?.startedAt === identity.startedAt &&
|
||||
current?.name === identity.name
|
||||
);
|
||||
});
|
||||
return {
|
||||
residualCounts: countOwnedProcessKinds(residualIdentities),
|
||||
activeCommandChildCount,
|
||||
activeInteractiveCliSessionCount,
|
||||
clean:
|
||||
residualIdentities.length === 0 &&
|
||||
activeCommandChildCount === 0 &&
|
||||
activeInteractiveCliSessionCount === 0,
|
||||
};
|
||||
}
|
||||
|
||||
export async function captureOwnedProcessCleanupSnapshot(options) {
|
||||
return buildOwnedProcessCleanupSnapshot(
|
||||
await listSystemProcessIdentities(),
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
export async function verifyOwnedProcessCleanupSnapshot(snapshot) {
|
||||
return inspectOwnedProcessCleanupResiduals(
|
||||
snapshot,
|
||||
await listSystemProcessIdentities(),
|
||||
{
|
||||
activeCommandChildCount: activeCommandChildren.size,
|
||||
activeInteractiveCliSessionCount: activeInteractiveCliSessions.size,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function appendBounded(current, chunk, limit) {
|
||||
const combined = Buffer.concat([current, chunk]);
|
||||
return combined.length <= limit
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { assert, hashValue } from '../assertions/core.mjs';
|
||||
import { assert } from '../assertions/core.mjs';
|
||||
import { disposableProjectPathVariants } from '../assertions/runtime.mjs';
|
||||
import { fs, os, path, randomUUID } from '../dependencies.mjs';
|
||||
import {
|
||||
@@ -28,10 +28,7 @@ import {
|
||||
isGoalRuntimeSuite,
|
||||
} from '../suites/goal.mjs';
|
||||
import { isResponseStreamSuite } from '../suites/response-stream.mjs';
|
||||
import {
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite,
|
||||
isSupervisorGameChatSingleMainPlayableSuite,
|
||||
} from '../suites/supervisor-autonomous-playable.mjs';
|
||||
import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs';
|
||||
import {
|
||||
isSupervisorSwarmSuite,
|
||||
supervisorSwarmVerificationFixtureSource,
|
||||
@@ -43,97 +40,27 @@ import { effectiveAgentLlmConfig } from './config.mjs';
|
||||
import { runProcess } from './process.mjs';
|
||||
import { isIsolatedRunnerSuite } from './reporting.mjs';
|
||||
|
||||
export function requiredAgentIdsForSuite() {
|
||||
return isUserInputRuntimeSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
mainAgentId,
|
||||
'art-director',
|
||||
'art-asset-plan',
|
||||
]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
supervisorSwarmDesignAgentId,
|
||||
supervisorSwarmQualityAgentId,
|
||||
]
|
||||
: isIsolatedRunnerSuite()
|
||||
? [mainAgentId]
|
||||
: [mainAgentId, 'quality-review'];
|
||||
}
|
||||
|
||||
export function expectedProviderBindingForSuite(config) {
|
||||
if (
|
||||
isSupervisorGameChatSingleMainPlayableSuite() &&
|
||||
config.agentMode !== 'provider'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const agentIds = requiredAgentIdsForSuite();
|
||||
const effectiveConfigs = agentIds.map((agentId) =>
|
||||
effectiveAgentLlmConfig(config, agentId),
|
||||
);
|
||||
if (
|
||||
effectiveConfigs.length === 0 ||
|
||||
effectiveConfigs.some((effective) =>
|
||||
['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].some(
|
||||
(key) =>
|
||||
typeof effective[key] !== 'string' ||
|
||||
effective[key].trim().length === 0,
|
||||
),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const [expected] = effectiveConfigs;
|
||||
const expectedIdentity = [
|
||||
expected.model.trim(),
|
||||
expected.apiKind.trim(),
|
||||
expected.reasoningEffort.trim(),
|
||||
expected.baseUrl.trim(),
|
||||
];
|
||||
if (
|
||||
effectiveConfigs.some(
|
||||
(effective) =>
|
||||
JSON.stringify([
|
||||
effective.model.trim(),
|
||||
effective.apiKind.trim(),
|
||||
effective.reasoningEffort.trim(),
|
||||
effective.baseUrl.trim(),
|
||||
]) !== JSON.stringify(expectedIdentity),
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...(isSupervisorGameChatSingleMainPlayableSuite()
|
||||
? { providerAgentMode: 'provider' }
|
||||
: {}),
|
||||
providerModel: expectedIdentity[0],
|
||||
providerApiKind: expectedIdentity[1],
|
||||
providerReasoningEffort: expectedIdentity[2],
|
||||
providerBaseUrlSha256: hashValue(expectedIdentity[3]),
|
||||
boundAgentIds: [...agentIds].sort(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkPrerequisites(config) {
|
||||
const requiredAgents = requiredAgentIdsForSuite();
|
||||
let llmConfigured = requiredAgents.every((agentId) => {
|
||||
const requiredAgents = isUserInputRuntimeSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? [projectSupervisorAgentId]
|
||||
: isSupervisorSwarmSuite()
|
||||
? [
|
||||
projectSupervisorAgentId,
|
||||
supervisorSwarmDesignAgentId,
|
||||
supervisorSwarmQualityAgentId,
|
||||
]
|
||||
: isIsolatedRunnerSuite()
|
||||
? [mainAgentId]
|
||||
: [mainAgentId, 'quality-review'];
|
||||
const llmConfigured = requiredAgents.every((agentId) => {
|
||||
const effective = effectiveAgentLlmConfig(config, agentId);
|
||||
return ['apiKey', 'baseUrl', 'model'].every(
|
||||
(key) =>
|
||||
typeof effective[key] === 'string' && effective[key].trim().length > 0,
|
||||
);
|
||||
});
|
||||
const providerBinding = expectedProviderBindingForSuite(config);
|
||||
if (isSupervisorGameChatSingleMainPlayableSuite()) {
|
||||
llmConfigured = llmConfigured && providerBinding !== null;
|
||||
}
|
||||
const editorApiConfigured = ['apiKey', 'baseUrl'].every(
|
||||
(key) =>
|
||||
typeof config.editorApi?.[key] === 'string' &&
|
||||
@@ -141,7 +68,6 @@ export async function checkPrerequisites(config) {
|
||||
);
|
||||
return {
|
||||
llmConfigured,
|
||||
providerBinding,
|
||||
chromeAvailable:
|
||||
!isIsolatedRunnerSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
@@ -224,9 +150,7 @@ export function supportedBrowserCandidates(platform, environment) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export async function seedDisposableProject({
|
||||
preserveProductionInitBaseline = false,
|
||||
} = {}) {
|
||||
export async function seedDisposableProject() {
|
||||
const prefix = path.join(os.tmpdir(), 'genarrative-agent-runtime-real-e2e-');
|
||||
const sentinelToken = randomUUID();
|
||||
state.projectRoot = await createSentinelOwnedTempDirectory({
|
||||
@@ -265,42 +189,37 @@ export async function seedDisposableProject({
|
||||
...(isResponseStreamSuite() ? [responseStreamThinkingCanary] : []),
|
||||
];
|
||||
|
||||
const generatedBaselineWrites = preserveProductionInitBaseline
|
||||
? []
|
||||
: [
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
isSupervisorSwarmSuite()
|
||||
? supervisorSwarmVerificationFixtureSource()
|
||||
: isGoalRuntimeSuite() ||
|
||||
isResponseStreamSuite() ||
|
||||
isWebSearchSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? goalRevisionOneVerificationFixtureSource()
|
||||
: goalRevisionTwoVerificationFixtureSource(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
seededGameHtml(),
|
||||
),
|
||||
];
|
||||
await Promise.all([
|
||||
...generatedBaselineWrites,
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'genarrative-agent-runtime-real-e2e-project',
|
||||
private: true,
|
||||
scripts: {
|
||||
test: verificationCommand,
|
||||
'check:e2e': verificationCommand,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'verify-e2e.mjs'),
|
||||
isSupervisorSwarmSuite()
|
||||
? supervisorSwarmVerificationFixtureSource()
|
||||
: isGoalRuntimeSuite() ||
|
||||
isResponseStreamSuite() ||
|
||||
isWebSearchSuite() ||
|
||||
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
||||
? goalRevisionOneVerificationFixtureSource()
|
||||
: goalRevisionTwoVerificationFixtureSource(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'game/index.html'),
|
||||
seededGameHtml(),
|
||||
),
|
||||
fs.writeFile(
|
||||
path.join(state.projectRoot, 'AGENTS.md'),
|
||||
'# Real E2E Repository Context\n\n- REPOSITORY_CONTEXT_MARKER: preserve the visible text and canvas.\n- Read package.json before verification.\n- Never read or expose .env, game-creator.config.json, or .agent/private-secret.txt.\n',
|
||||
@@ -345,16 +264,7 @@ export async function seedDisposableProject({
|
||||
mode: 0o600,
|
||||
},
|
||||
);
|
||||
await initializeDisposableGitRepository(
|
||||
preserveProductionInitBaseline
|
||||
? [
|
||||
'AGENTS.md',
|
||||
'e2e/isolated-a/evidence.txt',
|
||||
'e2e/isolated-b/evidence.txt',
|
||||
'e2e/isolated-c/evidence.txt',
|
||||
]
|
||||
: undefined,
|
||||
);
|
||||
await initializeDisposableGitRepository();
|
||||
}
|
||||
|
||||
export async function initializeDisposableGitRepository(
|
||||
@@ -400,17 +310,12 @@ export function seededGameHtml() {
|
||||
<main>
|
||||
<h1>${visibleText}</h1>
|
||||
<p id="patch-state">REAL_E2E_TARGET:before</p>
|
||||
<p id="objective">Objective: survive until victory. On defeat, use Restart.</p>
|
||||
<canvas id="game" width="640" height="360"></canvas>
|
||||
</main>
|
||||
<script>
|
||||
const canvas = document.getElementById('game');
|
||||
const context = canvas.getContext('2d');
|
||||
let frame = 0;
|
||||
canvas.addEventListener('pointerdown', () => {
|
||||
frame = 0;
|
||||
document.body.dataset.input = 'pointer';
|
||||
});
|
||||
function draw() {
|
||||
frame += 1;
|
||||
context.fillStyle = '#13293d'; context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
@@ -427,23 +332,6 @@ export function seededGameHtml() {
|
||||
`;
|
||||
}
|
||||
|
||||
export function productionDefaultGameIndexHtml() {
|
||||
return `<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Genarrative Game Draft</title>
|
||||
<style>
|
||||
body { margin: 0; display: grid; min-height: 100vh; place-items: center; background: #101827; color: #d9e7ff; font: 16px system-ui, sans-serif; }
|
||||
main { width: min(720px, calc(100vw - 32px)); }
|
||||
</style>
|
||||
</head>
|
||||
<body><main>还没有生成游戏。回到聊天输入创意并确认生成后,这里会写入可试玩原型。</main></body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
export function buildTaskPrompt(suite) {
|
||||
const editorAssetOutcome =
|
||||
suite === 'full'
|
||||
|
||||
@@ -47,45 +47,12 @@ export async function removeDisposableProject() {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function providerUsedFromEvidence(evidence) {
|
||||
const requestIdentityCount = evidence?.providerRequestIdentityCount;
|
||||
const startedCount = evidence?.providerLifecycleStartedCount;
|
||||
const terminalCount = evidence?.providerLifecycleTerminalCount;
|
||||
const completedCount = evidence?.providerLifecycleCompletedCount;
|
||||
return (
|
||||
evidence?.evidenceCompleteness === 'complete' &&
|
||||
evidence.providerAgentMode === 'provider' &&
|
||||
evidence.providerBindingMatched === true &&
|
||||
typeof evidence.providerModel === 'string' &&
|
||||
evidence.providerModel.trim().length > 0 &&
|
||||
typeof evidence.providerApiKind === 'string' &&
|
||||
evidence.providerApiKind.trim().length > 0 &&
|
||||
typeof evidence.providerReasoningEffort === 'string' &&
|
||||
evidence.providerReasoningEffort.trim().length > 0 &&
|
||||
/^[0-9a-f]{64}$/u.test(evidence.providerBaseUrlSha256 ?? '') &&
|
||||
Number.isSafeInteger(evidence.providerBoundAgentCount) &&
|
||||
evidence.providerBoundAgentCount > 0 &&
|
||||
Number.isSafeInteger(requestIdentityCount) &&
|
||||
Number.isSafeInteger(startedCount) &&
|
||||
Number.isSafeInteger(terminalCount) &&
|
||||
Number.isSafeInteger(completedCount) &&
|
||||
requestIdentityCount > 0 &&
|
||||
startedCount === requestIdentityCount &&
|
||||
terminalCount === requestIdentityCount &&
|
||||
completedCount === requestIdentityCount &&
|
||||
evidence.providerLifecycleFailedCount === 0 &&
|
||||
evidence.openProviderLifecycleCount === 0 &&
|
||||
evidence.duplicateProviderLifecycleCount === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function buildSummary() {
|
||||
const secretLeakCount =
|
||||
state.transcriptLeakCount + state.projectLeakCount + state.reportLeakCount;
|
||||
const base = {
|
||||
status: state.status,
|
||||
suite: state.suite,
|
||||
providerUsed: providerUsedFromEvidence(state.evidence),
|
||||
config: state.config,
|
||||
blocked: state.blocked,
|
||||
run: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user