Conversation
Task package: sih-engine/sih/state/plan/run-lifecycle.md (PR1, first half). - Tombstone soft delete on terminal runs (succeeded/failed/ completed_with_gaps/cancelled/interrupted): run body gains deletedAt/deletedBy/purgeAfter plus an append-only run.deleted audit event; steps, events, result and the integrity ledger stay untouched. - Query faces all filter tombstones: run list, snapshot (workflow_status, HTTP GET), cross-run reuse candidate scan, repair source scan, resume, and requestId idempotent replay (rejected with a recovery hint instead of resurrecting ghost data). - Restore clears the tombstone and appends run.restored; the run reappears byte-identical. Repeat deletes are idempotent and never append a second event. - Retention: trashRetentionDays setting (default 30, 0 = manual-only expiry) via the store settings face; changing it restamps tombstones already in trash so the displayed countdown stays truthful. - Exposure: HTTP DELETE /api/runs/:id, POST /api/runs/:id/restore, GET /api/runs?trash=1, GET/POST /api/trash; MCP workflow_delete / workflow_restore (old daemons gated via the trashManagement feature flag); Studio trash view with restore button and retention countdown. - checks/trash.check.mjs pins hide/restore round trips, every filtered face, idempotence, the reuse interaction, retention math and the HTTP/MCP surfaces; tool-count assertions move from 11 to 13.
Task package: sih-engine/sih/state/plan/run-lifecycle.md (PR1, second half).
- Rotation compaction exports due tombstones (purgeAfter <= now) into the
sidecar <data-dir>/archive.db and physically deletes their live runs/steps
rows; the events table never loses a row (program red line). The audit
event archive.rotated {rotationId, runs, manifestHash, runCount, bytes} is
appended in the same transaction as the deletes, anchoring the manifest on
the MiniMax-AI#48 events hash chain.
- manifestHash is computed by one shared implementation
(archiveManifestHash: canonical JSON over runs+steps rows in (runId, id)
order) used by both rotation and verification, so the two hashes cannot
drift; checks/archive-rotate.check.mjs recomputes it independently.
- Archive rows are keyed by (rotationId, runId) instead of a bare runs PK:
a run that is restored, re-deleted and re-rotated writes a fresh copy and
can never rewrite rows an earlier rotation's manifest still covers.
- Verification (verifyArchive) recomputes every rotation's manifest from
the archived rows and compares it against both the rotations record and
the chained event; chained-but-missing rotations and orphan archive rows
fail closed. verifyIntegrity() gains an archive face alongside
events/repair without touching their semantics.
- Restore from archive copies rows back (idempotent upserts), clears the
tombstone and appends run.restored {origin:'archive', rotationId}; a
requestId claimed by a newer live run blocks the restore loudly.
workflow_delete on a rotated run reports the archive state truthfully.
- Triggers: CLI --rotate-archive (with --verify for the shared verify
outlet; null verdicts from empty chains are not failures) and --restore
<runId>, both forwarding to a live service instead of fighting the owner
lock; service startup auto-rotates when tombstones exceed 500 or the
runs table exceeds ~100MB (constants documented in store.mjs), skipped
entirely when trashRetentionDays=0 (manual-only). HTTP face:
POST /api/archive/rotate.
- checks/archive-rotate.check.mjs pins due-only rotation, the events-row
red line, tamper detection (step body, forged rotations hash, deleted
rotation record), archive restore and re-rotation verifiability, the
501-vs-400 startup threshold against a real daemon, and the CLI faces
with real exit codes.
…eage Task package: sih-engine/sih/state/plan/run-lifecycle.md, PR2 (复跑谱系), stacked on PR1's tombstone/restore/rotation commits. A rerun starts a NEW pending_review run from the source's script+input and never writes the source. The child carries rerunOf/lineageRoot/rerunSeq on its body, so the family survives unlimited reruns, trash and archive rotation (members are annotated, never removed). Key decisions: - requestId is synthesized as `<root>#rerun-<n>`; n counts live, tombstoned AND archived family members so a seq is never reused after rotation, with a numeric-suffix retry when the id is already claimed (150-char cap kept). - reuseAcrossRuns defaults to false on reruns (explicit opt-in only): silent adoption would turn a rerun into a fake execution and poison comparison. - Reruns pass through the existing pending_review/approve gate; the rerun action itself only creates the draft, it never auto-executes. Tombstoned sources are refused until restored; rerunning straight from the archive is deliberately not offered (restore first). - GET /api/runs/:id/lineage resolves the family from any member id (live, tombstoned or archived), ordered root-first then by seq, with per-member status/duration/timestamps/result summary; ?results=1 adds full results for the read-only compare face. Durations come from the events ledger, which rotation never moves. - MCP workflow_rerun (input override + reuseAcrossRuns flag) is gated behind a rerunLineage feature flag, mirroring the trash tools' old-daemon guard. - Studio gains a lineage panel (member list with trash/archive flags) and a two-column read-only JSON result compare. - New checks/rerun-lineage.check.mjs pins: source byte-identity across a rerun, family ordering over 3 reruns, default no-reuse vs explicit reuse, trash/rotation refusal with family integrity, requestId collision retry, HTTP lineage shape and the MCP schema. Files: src/store.mjs src/engine.mjs src/http.mjs src/tools.mjs src/main.mjs web/index.html web/app.source.mjs web/app.js web/i18n.mjs web/style.css checks/rerun-lineage.check.mjs checks/package.check.mjs checks/workspace-router.check.mjs test/package.test.mjs README.md dist/main.mjs
Fork preview run 35506090983 (first full windows-latest pass) failed check MiniMax-AI#9 "packaged MCP advertises reuseAcrossRuns and accepts it through the public tool surface" with: EBUSY: resource busy or locked, rmdir 'C:\Users\RUNNER~1\AppData\Local\Temp\wf-cross-mcp-NCVTMk' Every assertion passed; the failure came from the finally block. StdioClientTransport.close() resolving does not mean the spawned dist/main.mjs server process has exited, so the child still held the dataDir (the mkdtemp tmp dir) handle when rm() ran, and win32 refuses to rmdir a busy directory. Linux/darwin unlink open files, which is why all local runs were green. - add a local rmWithRetry helper: retry rm on EBUSY/ENOTEMPTY/EPERM with exponential backoff (200ms base, x2, 5 retries, ~6s total budget), then rethrow as-is -- cleanup failures stay loud, we only give the OS time to release handles - finally: wrap client/transport close in try/catch so a close error cannot skip rm; rethrow the close error after cleanup so it still fails the test Only checks/cross-reuse-mcp.check.mjs is touched; no src/, no dist/, no other checks, no new dependencies. Local verification: npm test exit=0, 126/126.
…-reuse MCP check Round two of the same failure. Fork preview run 35506404071 (with 14a7fd5) failed the same check MiniMax-AI#9 with the same code: EBUSY: resource busy or locked, rmdir '...\wf-cross-mcp-3VJDll' The test ran 7945ms -- the full ~6.2s retry budget burned while the directory stayed locked. Not a transient lock, so retrying cannot fix it. Real root cause (src/main.mjs, stdio branch): when no existing service is found, --stdio spawns a detached daemon -- spawn(process.execPath, args, {cwd:workspace, detached:true, ...}); child.unref() -- with cwd set to the workspace (the mkdtemp tmp dir), service.log and the database inside dataDir (the same tmp dir). By design "the service, workers and dashboard outlive" the chat transport, so client.close()/transport.close() only tear down the stdio face and the daemon keeps running, holding its cwd handle plus service.log/DB handles inside the tree. win32 refuses to rmdir a directory tree a live process is sitting in; Linux/darwin unlink open files, which is why local runs stay green. Layer two, from run 35506090983: transport.close() resolving does not mean a child has fully exited, so handle release can lag an instant even after the daemon is gone. The codebase's own convention already stops the service first (checks/fail-loud.check.mjs:80, checks/lifecycle.check.mjs:19): run dist/main.mjs --stop-service --workspace <dir> --data-dir <dir> after closing the client. Identity matches this test because its --settings file sets workspace=dataDir=dir, the same endpoint.json the daemon registered under. - finally: after client/transport close (still deferred-rethrow), run --stop-service wrapped in try/catch so a stop failure cannot block rm - keep rmWithRetry as the release-lag fallback, still failing loud when exhausted - rewrite the comment to the two-layer root cause with both run ids Only checks/cross-reuse-mcp.check.mjs is touched; no src/, no dist/, no new dependencies. Local verification: npm test exit=0, 126/126; ps confirms the daemon spawned by the check is actually terminated by the stop-service step (no stray wf-cross-mcp-* processes remain).
…r flags Task package: sih-engine/sih/state/plan/run-lifecycle.md, PR2 (复跑谱系), UI patch from the egolite real-panel interaction pass. Two bugs, one render path, fixed as one decision: data refreshes, interaction state survives. Bug 1 (compare view wiped by polling): with two members selected, clicking 对比结果 shows the two-column diff (measured 1090x138 visible); ~6s later the poll re-render hides it again (hidden:true, 0x0). Root cause: renderLineage() ran unconditionally on every poll (refreshCurrent -> renderRun -> renderLineage) and executed `#lineage-diff.hidden=true` plus a full select rebuild each pass, resetting the user's diff visibility and left/right selection every few seconds. Bug 2 (stale deleted flag after trash restore): restoring rerun-1 left its lineage card wearing the 已删除(回收站) flag across 8s+ and multiple poll cycles even though the lineage API already returned deleted:null (three members live, trash empty). Root cause: the flag renders from the client's lineage cache, which was only refetched on selectRun and panel toggle - neither happens on restore, so polls kept painting stale data forever (not a DOM-residue issue: rows were rebuilt from stale input). Fix: - renderLineage() now splits data from interaction. Member cards (status, duration, trash/archive flags, preview) re-render on every pass from whatever the cache holds. The compare controls (selects, open diff) are rebuilt only when member identity changes, gated by a pure signature (language + ordered id:rerunSeq:name, web/lineage-model.mjs). Signature unchanged -> selects untouched, #lineage-diff.hidden untouched. Member added/removed/reordered/renamed or language switch -> full rebuild and the diff resets (member set changed, reset is legitimate). First render is unchanged (empty previous signature always rebuilds). - refreshCurrent() refetches the lineage payload (background mode) when the panel is open, so member state can no longer go stale across polls; background errors keep the last good cache instead of blanking the open panel. Trash restore additionally triggers an immediate refetch. - Regression nail: checks/lineage-panel.check.mjs pins the gate logic in node:test (poll-shaped refresh and flag clears never rebuild; identity/ language/name changes always do). The DOM-level outcome (diff stays visible across 6s+ of polling) cannot be carried by node:test (no DOM); to be re-verified with the egolite interaction protocol: click compare -> still visible after 6s+ of polling. Files: plugins/hetaoBackend/mcode-dynamic-workflows/web/app.source.mjs plugins/hetaoBackend/mcode-dynamic-workflows/web/lineage-model.mjs plugins/hetaoBackend/mcode-dynamic-workflows/web/app.js (rebuilt via scripts/build-web.mjs) plugins/hetaoBackend/mcode-dynamic-workflows/checks/lineage-panel.check.mjs
|
Follow-up ( The render path now splits data from interaction state: member cards (status / duration / deleted / archived flags / result previews) refresh on every poll, while the compare controls (selections, diff visibility) are only rebuilt when the lineage's identity signature changes (member ids/seqs/names/language) — with a toggle handler refetching on panel expansion and an immediate refetch after a trash restore. The signature logic is extracted as pure functions and pinned by |
hetaoBackend
left a comment
There was a problem hiding this comment.
Request changes for exact current head 55b8f48e341648d73692721d76a159c55fbb8921.
Blocking archive-integrity issue:
plugins/hetaoBackend/mcode-dynamic-workflows/src/store.mjs:205-226verifies rotation manifests, butrestoreArchived()at:239-255selects/parses the latest archived run and writes it into the live database without verifying that rotation against its chainedarchive.rotatedevent first. A valid-JSON but modified archived run/step can therefore be restored as trusted live data even thoughverifyArchive()would report tampering. Add a rotation-scoped verification before any restore write and a regression proving tampered restore fails closed without live rows or arun.restoredevent.
Also correct the README tool-count mismatch (README.md:5 says 13 while README.md:85 lists 14). The exact-head Ubuntu, Windows, package and CodeQL checks are green, but [code]smith is skipped and is not evidence.
PR MiniMax-AI#59 re-review: restoreArchived() selected/parsed the latest archived run and wrote it into the live database without verifying that rotation against its chained archive.rotated event first, so a valid-JSON but modified archived run/step could be restored as trusted live data even though verifyArchive() would report the tampering. Verify-then-write: extract the per-rotation check (manifest recompute plus chained archive.rotated event cross-check, same archiveManifestHash face) into verifyRotation(), shared by verifyArchive() and by a fail-closed gate at the top of restoreArchived(). A failed verdict throws before any live write: no runs/steps rows, no run.restored event, and the error names the rotation id plus the reasons. Regression (archive-rotate.check.mjs): tampering an archived step or run body makes restore refuse with the rotation id while the live library keeps zero rows and zero run.restored events; healing the archive lets the same restore pass again (round-trip). Existing healthy-restore coverage unchanged and green. Also correct the README tool-count mismatch (line 5 said 13 tools; the TOOLS array and README's own MCP tool list both have 14). - src/store.mjs: verifyRotation() shared check + restore gate - checks/archive-rotate.check.mjs: tampered-restore fail-closed + round-trip - dist/main.mjs: rebuilt bundle - README.md: 13 -> 14 tools
|
Fixed in
Suites: 137 → 143 green; exact-head Linux clean-tree gate green; Windows + CodeQL via the fork preview mirror (run 35694871485). |
feat(dynamic-workflow): rerun finished workflows into a queryable, comparable lineage
Stacked on the trash/archive PR (community/run-lifecycle) — rebases cleanly once it merges.
Re-running a finished workflow now creates a new run from the original's script+input and never touches the original: before/after comparison of the source run's stored body, steps and events is byte-identical (pinned by test), and the event chain stays valid. Unlimited re-runs are supported by construction — every attempt is a full run with its own process and result.
Lineage
rerunOf(parent),lineageRoot(root),rerunSeq(n-th member of the family) on its body;run.createdevents include the lineage fields.GET /api/runs/:id/lineagereturns the root plus every member ordered by seq with status, duration (read from the event ledger, so archived members are covered too), timestamps and a result excerpt (?results=1for full results). Tombstoned members are flaggeddeleted, archived membersarchived.rotationId— deletion never breaks the family, and restoring returns it intact.<root>#rerun-<n>, n = highest seq across live + tombstoned + archived members + 1) avoid the UNIQUE constraint and the idempotent-replay path; a collision retries with a numeric suffix and the squatter stays out of the family.Safety and interactions
reuseAcrossRunstofalse: with cross-run reuse on, an identical context would adopt the original's succeeded nodes and the re-run would be a fake execution — useless for comparison. The flag can be set explicitly for cheap re-execution. Both behaviors are pinned (fresh dispatch with growing call counts vs. feat(dynamic-workflow): opt-in content-addressed cross-run reuse #49 adoption with zero new calls).pending_reviewrun — approval, execution, fail-loud paths and budgets all apply unchanged.Surfaces
workflow_rerun(runIdrequired; optionalinputoverride, explicitreuseAcrossRuns,name) with a strict schema; the stdio proxy reportsWORKFLOW_SERVICE_UPGRADE_REQUIREDagainst an older background service instead of breaking it.Checks and verification
checks/rerun-lineage.check.mjs(7 cases, real subprocesses / real store / real HTTP): source byte-identity across a re-run; three interleaved re-runs producing a 4-member ordered family with unique requestIds; default-no-reuse vs explicit reuse; trash → archive → restore round trip with the lineage intact and seq accounting skipping archived occupants; requestId collision suffix retry; lineage API shape; MCP schema and honest errors. Suites:npm test130 → 137 green (macOS); Linux clean-tree gate green; Windows + CodeQL via the fork preview mirror. No new dependencies.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.