@quolu/lattice 0.1.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -4
- package/bin/lattice-bridge.mjs +87 -0
- package/bin/lattice-dashboard.mjs +70 -0
- package/bin/lattice-mcp.mjs +9 -9
- package/bin/lattice.mjs +61 -8
- package/docs/bridge-setup.md +87 -0
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -0
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -0
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -0
- package/package.json +19 -7
- package/sensor/dist/bin/command-supervision.d.ts +1 -1
- package/sensor/dist/bin/command-supervision.js +4 -4
- package/sensor/dist/bin/command-supervision.js.map +1 -1
- package/sensor/dist/bin/fatal-handler.js +2 -2
- package/sensor/dist/bin/fatal-handler.js.map +1 -1
- package/sensor/dist/bin/{codegraph.js → lattice-sensor.js} +207 -328
- package/sensor/dist/bin/node-version-check.d.ts +7 -5
- package/sensor/dist/bin/node-version-check.d.ts.map +1 -1
- package/sensor/dist/bin/node-version-check.js +19 -14
- package/sensor/dist/bin/node-version-check.js.map +1 -1
- package/sensor/dist/context/index.d.ts +1 -1
- package/sensor/dist/context/index.js +6 -6
- package/sensor/dist/context/index.js.map +1 -1
- package/sensor/dist/db/index.d.ts +5 -5
- package/sensor/dist/db/index.d.ts.map +1 -1
- package/sensor/dist/db/index.js +7 -7
- package/sensor/dist/db/index.js.map +1 -1
- package/sensor/dist/db/migrations.js +1 -1
- package/sensor/dist/db/migrations.js.map +1 -1
- package/sensor/dist/db/queries.d.ts +1 -1
- package/sensor/dist/db/queries.js +5 -5
- package/sensor/dist/db/queries.js.map +1 -1
- package/sensor/dist/db/schema.sql +2 -2
- package/sensor/dist/db/sqlite-adapter.d.ts +2 -2
- package/sensor/dist/db/sqlite-adapter.js +3 -3
- package/sensor/dist/db/sqlite-adapter.js.map +1 -1
- package/sensor/dist/db/wal-valve.d.ts +1 -1
- package/sensor/dist/db/wal-valve.d.ts.map +1 -1
- package/sensor/dist/db/wal-valve.js +2 -2
- package/sensor/dist/db/wal-valve.js.map +1 -1
- package/sensor/dist/directory.d.ts +27 -63
- package/sensor/dist/directory.d.ts.map +1 -1
- package/sensor/dist/directory.js +78 -134
- package/sensor/dist/directory.js.map +1 -1
- package/sensor/dist/errors.d.ts +15 -15
- package/sensor/dist/errors.d.ts.map +1 -1
- package/sensor/dist/errors.js +21 -21
- package/sensor/dist/errors.js.map +1 -1
- package/sensor/dist/extraction/dynamic-import.d.ts +1 -1
- package/sensor/dist/extraction/dynamic-import.js +1 -1
- package/sensor/dist/extraction/extraction-version.d.ts +1 -1
- package/sensor/dist/extraction/extraction-version.js +1 -1
- package/sensor/dist/extraction/generated-detection.d.ts +1 -1
- package/sensor/dist/extraction/generated-detection.js +1 -1
- package/sensor/dist/extraction/grammars.d.ts +3 -3
- package/sensor/dist/extraction/grammars.js +5 -5
- package/sensor/dist/extraction/grammars.js.map +1 -1
- package/sensor/dist/extraction/index.d.ts +6 -6
- package/sensor/dist/extraction/index.js +50 -50
- package/sensor/dist/extraction/index.js.map +1 -1
- package/sensor/dist/extraction/kernel/decode.d.ts +1 -1
- package/sensor/dist/extraction/kernel/decode.js +1 -1
- package/sensor/dist/extraction/kernel/index.d.ts +3 -3
- package/sensor/dist/extraction/kernel/index.js +6 -6
- package/sensor/dist/extraction/kernel/index.js.map +1 -1
- package/sensor/dist/extraction/kernel/layout.d.ts +1 -1
- package/sensor/dist/extraction/kernel/layout.js +1 -1
- package/sensor/dist/extraction/kernel/loader.d.ts +6 -6
- package/sensor/dist/extraction/kernel/loader.js +13 -13
- package/sensor/dist/extraction/kernel/loader.js.map +1 -1
- package/sensor/dist/extraction/languages/cfquery.d.ts +1 -1
- package/sensor/dist/extraction/languages/cfquery.js +1 -1
- package/sensor/dist/extraction/languages/lua.d.ts +11 -0
- package/sensor/dist/extraction/languages/lua.d.ts.map +1 -1
- package/sensor/dist/extraction/languages/lua.js +1 -0
- package/sensor/dist/extraction/languages/lua.js.map +1 -1
- package/sensor/dist/extraction/parse-pool.d.ts +5 -5
- package/sensor/dist/extraction/parse-pool.d.ts.map +1 -1
- package/sensor/dist/extraction/parse-pool.js +4 -4
- package/sensor/dist/extraction/parse-worker.js +1 -1
- package/sensor/dist/extraction/parse-worker.js.map +1 -1
- package/sensor/dist/extraction/store-writer.d.ts +1 -1
- package/sensor/dist/extraction/store-writer.js +1 -1
- package/sensor/dist/extraction/svelte-extractor.d.ts +1 -1
- package/sensor/dist/extraction/svelte-extractor.js +1 -1
- package/sensor/dist/extraction/tree-sitter-helpers.js +1 -1
- package/sensor/dist/extraction/tree-sitter.d.ts +2 -2
- package/sensor/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/sensor/dist/extraction/tree-sitter.js +71 -4
- package/sensor/dist/extraction/tree-sitter.js.map +1 -1
- package/sensor/dist/extraction/wasm-runtime-flags.d.ts +2 -2
- package/sensor/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/sensor/dist/extraction/wasm-runtime-flags.js +5 -5
- package/sensor/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/sensor/dist/index.d.ts +36 -36
- package/sensor/dist/index.d.ts.map +1 -1
- package/sensor/dist/index.js +71 -71
- package/sensor/dist/index.js.map +1 -1
- package/sensor/dist/mcp/daemon-manager.js +1 -1
- package/sensor/dist/mcp/daemon-paths.d.ts +3 -3
- package/sensor/dist/mcp/daemon-paths.js +7 -7
- package/sensor/dist/mcp/daemon-paths.js.map +1 -1
- package/sensor/dist/mcp/daemon-registry.d.ts +2 -2
- package/sensor/dist/mcp/daemon-registry.js +5 -5
- package/sensor/dist/mcp/daemon.d.ts +7 -7
- package/sensor/dist/mcp/daemon.d.ts.map +1 -1
- package/sensor/dist/mcp/daemon.js +21 -21
- package/sensor/dist/mcp/daemon.js.map +1 -1
- package/sensor/dist/mcp/dynamic-boundaries.js +1 -1
- package/sensor/dist/mcp/engine.d.ts +9 -9
- package/sensor/dist/mcp/engine.d.ts.map +1 -1
- package/sensor/dist/mcp/engine.js +44 -44
- package/sensor/dist/mcp/engine.js.map +1 -1
- package/sensor/dist/mcp/index.d.ts +14 -14
- package/sensor/dist/mcp/index.d.ts.map +1 -1
- package/sensor/dist/mcp/index.js +41 -41
- package/sensor/dist/mcp/index.js.map +1 -1
- package/sensor/dist/mcp/liveness-watchdog.js +7 -7
- package/sensor/dist/mcp/liveness-watchdog.js.map +1 -1
- package/sensor/dist/mcp/ppid-watchdog.d.ts +3 -3
- package/sensor/dist/mcp/ppid-watchdog.js +2 -2
- package/sensor/dist/mcp/proxy.d.ts.map +1 -1
- package/sensor/dist/mcp/proxy.js +37 -37
- package/sensor/dist/mcp/proxy.js.map +1 -1
- package/sensor/dist/mcp/query-pool.d.ts +3 -3
- package/sensor/dist/mcp/query-pool.js +7 -7
- package/sensor/dist/mcp/query-pool.js.map +1 -1
- package/sensor/dist/mcp/query-worker.d.ts +2 -2
- package/sensor/dist/mcp/query-worker.js +7 -7
- package/sensor/dist/mcp/query-worker.js.map +1 -1
- package/sensor/dist/mcp/server-instructions.d.ts +10 -12
- package/sensor/dist/mcp/server-instructions.d.ts.map +1 -1
- package/sensor/dist/mcp/server-instructions.js +39 -37
- package/sensor/dist/mcp/server-instructions.js.map +1 -1
- package/sensor/dist/mcp/session.d.ts +2 -2
- package/sensor/dist/mcp/session.js +18 -18
- package/sensor/dist/mcp/session.js.map +1 -1
- package/sensor/dist/mcp/startup-handshake.d.ts +3 -3
- package/sensor/dist/mcp/startup-handshake.d.ts.map +1 -1
- package/sensor/dist/mcp/startup-handshake.js +3 -3
- package/sensor/dist/mcp/startup-handshake.js.map +1 -1
- package/sensor/dist/mcp/tools.d.ts +53 -53
- package/sensor/dist/mcp/tools.d.ts.map +1 -1
- package/sensor/dist/mcp/tools.js +180 -216
- package/sensor/dist/mcp/tools.js.map +1 -1
- package/sensor/dist/mcp/transport.d.ts +1 -1
- package/sensor/dist/mcp/transport.js +6 -6
- package/sensor/dist/mcp/transport.js.map +1 -1
- package/sensor/dist/mcp/version.d.ts +2 -2
- package/sensor/dist/mcp/version.d.ts.map +1 -1
- package/sensor/dist/mcp/version.js +3 -3
- package/sensor/dist/mcp/version.js.map +1 -1
- package/sensor/dist/project-config.d.ts +6 -6
- package/sensor/dist/project-config.d.ts.map +1 -1
- package/sensor/dist/project-config.js +10 -10
- package/sensor/dist/project-config.js.map +1 -1
- package/sensor/dist/resolution/callback-synthesizer.js +4 -4
- package/sensor/dist/resolution/callback-synthesizer.js.map +1 -1
- package/sensor/dist/resolution/frameworks/drupal.d.ts +3 -3
- package/sensor/dist/resolution/frameworks/drupal.js +3 -3
- package/sensor/dist/resolution/frameworks/fabric.js +1 -1
- package/sensor/dist/resolution/frameworks/fabric.js.map +1 -1
- package/sensor/dist/resolution/frameworks/java.js +1 -1
- package/sensor/dist/resolution/frameworks/java.js.map +1 -1
- package/sensor/dist/resolution/frameworks/svelte.js +1 -1
- package/sensor/dist/resolution/frameworks/svelte.js.map +1 -1
- package/sensor/dist/resolution/goframe-synthesizer.d.ts +1 -1
- package/sensor/dist/resolution/goframe-synthesizer.js +1 -1
- package/sensor/dist/resolution/index.d.ts +1 -1
- package/sensor/dist/resolution/index.js +9 -9
- package/sensor/dist/resolution/index.js.map +1 -1
- package/sensor/dist/resolution/name-matcher.js +2 -2
- package/sensor/dist/resolution/name-matcher.js.map +1 -1
- package/sensor/dist/resolution/resolver-pool.d.ts +2 -2
- package/sensor/dist/resolution/resolver-pool.js +4 -4
- package/sensor/dist/resolution/resolver-pool.js.map +1 -1
- package/sensor/dist/resolution/resolver-worker.js +2 -2
- package/sensor/dist/resolution/resolver-worker.js.map +1 -1
- package/sensor/dist/resolution/workspace-packages.js +1 -1
- package/sensor/dist/resolution/workspace-packages.js.map +1 -1
- package/sensor/dist/search/identifier-segments.d.ts +1 -1
- package/sensor/dist/search/identifier-segments.js +1 -1
- package/sensor/dist/search/query-utils.d.ts +1 -0
- package/sensor/dist/search/query-utils.d.ts.map +1 -1
- package/sensor/dist/search/query-utils.js +10 -3
- package/sensor/dist/search/query-utils.js.map +1 -1
- package/sensor/dist/sync/git-hooks.d.ts +9 -9
- package/sensor/dist/sync/git-hooks.d.ts.map +1 -1
- package/sensor/dist/sync/git-hooks.js +15 -15
- package/sensor/dist/sync/git-hooks.js.map +1 -1
- package/sensor/dist/sync/watch-policy.d.ts +2 -2
- package/sensor/dist/sync/watch-policy.js +5 -5
- package/sensor/dist/sync/watch-policy.js.map +1 -1
- package/sensor/dist/sync/watcher.d.ts +2 -2
- package/sensor/dist/sync/watcher.d.ts.map +1 -1
- package/sensor/dist/sync/watcher.js +16 -16
- package/sensor/dist/sync/watcher.js.map +1 -1
- package/sensor/dist/sync/worktree.d.ts +5 -5
- package/sensor/dist/sync/worktree.d.ts.map +1 -1
- package/sensor/dist/sync/worktree.js +9 -9
- package/sensor/dist/sync/worktree.js.map +1 -1
- package/sensor/dist/telemetry/index.d.ts +8 -8
- package/sensor/dist/telemetry/index.d.ts.map +1 -1
- package/sensor/dist/telemetry/index.js +16 -16
- package/sensor/dist/telemetry/index.js.map +1 -1
- package/sensor/dist/types.d.ts +2 -2
- package/sensor/dist/types.js +1 -1
- package/sensor/dist/ui/color.d.ts +2 -2
- package/sensor/dist/ui/color.js +2 -2
- package/sensor/dist/ui/glyphs.d.ts +3 -3
- package/sensor/dist/ui/glyphs.js +7 -7
- package/sensor/dist/ui/glyphs.js.map +1 -1
- package/sensor/dist/utils.d.ts +4 -4
- package/sensor/dist/utils.js +8 -8
- package/sensor/dist/utils.js.map +1 -1
- package/sensor/package.json +8 -13
- package/src/artifact-contracts.mjs +2 -2
- package/src/boundary-compiler.mjs +28 -28
- package/src/boundary-observation-compiler-v2.mjs +10 -10
- package/src/bounded-seam.mjs +144 -0
- package/src/bridge-cli.mjs +227 -0
- package/src/bridge-config.mjs +346 -0
- package/src/bridge-daemon.mjs +328 -0
- package/src/bridge-launch-agent.mjs +311 -0
- package/src/bridge-server.mjs +309 -0
- package/src/cli-help.mjs +163 -0
- package/src/control-compiler.mjs +30 -30
- package/src/dag-chain.mjs +261 -0
- package/src/factory-diagnostics.mjs +8 -7
- package/src/hash-chain.mjs +76 -0
- package/src/node-version-guard.mjs +5 -4
- package/src/project-cli.mjs +392 -0
- package/src/project-identity.mjs +71 -0
- package/src/rc1-evidence-bundle.mjs +16 -16
- package/src/rc1-v4-campaign.mjs +15 -14
- package/src/rc1-v5-artifact-set.mjs +2 -2
- package/src/rc1-v5-campaign.mjs +15 -14
- package/src/rc1-v6-artifact-set.mjs +18 -18
- package/src/rc1-v6-campaign.mjs +16 -16
- package/src/rc1-v6-causal-binding.mjs +16 -14
- package/src/rc1-v6-measurement.mjs +20 -38
- package/src/rc2-artifact-set.mjs +45 -40
- package/src/rc2-campaign.mjs +48 -66
- package/src/rc2-delivery-policy-front-end.mjs +30 -30
- package/src/rc2-rc1-transfer-front-end.mjs +5 -5
- package/src/rc3-actual-dogfood.mjs +6 -6
- package/src/rc3-dogfood-scaffold.mjs +12 -7
- package/src/rc3-scripted-campaign.mjs +75 -21
- package/src/rc4-stage1-dogfood.mjs +24 -7
- package/src/runtime-cli.mjs +2424 -45
- package/src/runtime-contracts.mjs +7 -7
- package/src/runtime-control-store.mjs +547 -0
- package/src/runtime-controller-protocol.mjs +517 -0
- package/src/runtime-direct-os-observer.mjs +284 -0
- package/src/runtime-engine.mjs +2 -2
- package/src/runtime-errors.mjs +1 -1
- package/src/runtime-event-store.mjs +15 -43
- package/src/runtime-front-end.mjs +26 -26
- package/src/runtime-gate-store.mjs +477 -0
- package/src/runtime-hold-recompile.mjs +214 -23
- package/src/runtime-lifecycle-lock.mjs +290 -0
- package/src/runtime-managed-supervisor.mjs +1276 -0
- package/src/runtime-multi-epoch-store.mjs +761 -0
- package/src/runtime-projection.mjs +62 -0
- package/src/seam-transform.mjs +38 -38
- package/src/{codegraph-adapter.mjs → sensor-adapter.mjs} +7 -7
- package/src/sensor-cli.mjs +107 -0
- package/src/sensor-runtime.mjs +48 -0
- package/src/timestamp-contract.mjs +8 -0
- package/src/todo-chain.mjs +178 -0
- package/src/todo-cli.mjs +1032 -0
- package/src/todo-contracts.mjs +501 -0
- package/src/todo-dashboard-registry.mjs +318 -0
- package/src/todo-gantt-html.mjs +416 -0
- package/src/todo-gantt-layout.mjs +603 -0
- package/src/todo-gantt-live.mjs +269 -0
- package/src/todo-gantt-presentation.mjs +217 -0
- package/src/todo-gantt-svg.mjs +249 -0
- package/src/todo-markdown-renderer.mjs +260 -0
- package/src/todo-migration.mjs +260 -0
- package/src/todo-narrative-anchor.mjs +130 -0
- package/src/todo-revision.mjs +425 -0
- package/src/todo-status.mjs +340 -0
- package/src/todo-store.mjs +3451 -0
- package/src/treatment-compiler.mjs +28 -28
- package/src/treatment-runner.mjs +42 -44
- package/sensor/dist/bin/codegraph.d.ts +0 -26
- package/sensor/dist/bin/codegraph.d.ts.map +0 -1
- package/sensor/dist/bin/codegraph.js.map +0 -1
- package/sensor/dist/bin/uninstall.d.ts +0 -14
- package/sensor/dist/bin/uninstall.d.ts.map +0 -1
- package/sensor/dist/bin/uninstall.js +0 -36
- package/sensor/dist/bin/uninstall.js.map +0 -1
- package/sensor/dist/installer/beta-signup.d.ts +0 -54
- package/sensor/dist/installer/beta-signup.d.ts.map +0 -1
- package/sensor/dist/installer/beta-signup.js +0 -178
- package/sensor/dist/installer/beta-signup.js.map +0 -1
- package/sensor/dist/installer/config-writer.d.ts +0 -28
- package/sensor/dist/installer/config-writer.d.ts.map +0 -1
- package/sensor/dist/installer/config-writer.js +0 -91
- package/sensor/dist/installer/config-writer.js.map +0 -1
- package/sensor/dist/installer/index.d.ts +0 -142
- package/sensor/dist/installer/index.d.ts.map +0 -1
- package/sensor/dist/installer/index.js +0 -622
- package/sensor/dist/installer/index.js.map +0 -1
- package/sensor/dist/installer/instructions-template.d.ts +0 -41
- package/sensor/dist/installer/instructions-template.d.ts.map +0 -1
- package/sensor/dist/installer/instructions-template.js +0 -53
- package/sensor/dist/installer/instructions-template.js.map +0 -1
- package/sensor/dist/installer/targets/antigravity.d.ts +0 -57
- package/sensor/dist/installer/targets/antigravity.d.ts.map +0 -1
- package/sensor/dist/installer/targets/antigravity.js +0 -308
- package/sensor/dist/installer/targets/antigravity.js.map +0 -1
- package/sensor/dist/installer/targets/claude.d.ts +0 -62
- package/sensor/dist/installer/targets/claude.d.ts.map +0 -1
- package/sensor/dist/installer/targets/claude.js +0 -454
- package/sensor/dist/installer/targets/claude.js.map +0 -1
- package/sensor/dist/installer/targets/codex.d.ts +0 -18
- package/sensor/dist/installer/targets/codex.d.ts.map +0 -1
- package/sensor/dist/installer/targets/codex.js +0 -185
- package/sensor/dist/installer/targets/codex.js.map +0 -1
- package/sensor/dist/installer/targets/cursor.d.ts +0 -35
- package/sensor/dist/installer/targets/cursor.d.ts.map +0 -1
- package/sensor/dist/installer/targets/cursor.js +0 -254
- package/sensor/dist/installer/targets/cursor.js.map +0 -1
- package/sensor/dist/installer/targets/gemini.d.ts +0 -26
- package/sensor/dist/installer/targets/gemini.d.ts.map +0 -1
- package/sensor/dist/installer/targets/gemini.js +0 -165
- package/sensor/dist/installer/targets/gemini.js.map +0 -1
- package/sensor/dist/installer/targets/hermes.d.ts +0 -18
- package/sensor/dist/installer/targets/hermes.d.ts.map +0 -1
- package/sensor/dist/installer/targets/hermes.js +0 -359
- package/sensor/dist/installer/targets/hermes.js.map +0 -1
- package/sensor/dist/installer/targets/kiro.d.ts +0 -27
- package/sensor/dist/installer/targets/kiro.d.ts.map +0 -1
- package/sensor/dist/installer/targets/kiro.js +0 -178
- package/sensor/dist/installer/targets/kiro.js.map +0 -1
- package/sensor/dist/installer/targets/opencode.d.ts +0 -38
- package/sensor/dist/installer/targets/opencode.d.ts.map +0 -1
- package/sensor/dist/installer/targets/opencode.js +0 -288
- package/sensor/dist/installer/targets/opencode.js.map +0 -1
- package/sensor/dist/installer/targets/registry.d.ts +0 -35
- package/sensor/dist/installer/targets/registry.d.ts.map +0 -1
- package/sensor/dist/installer/targets/registry.js +0 -91
- package/sensor/dist/installer/targets/registry.js.map +0 -1
- package/sensor/dist/installer/targets/shared.d.ts +0 -101
- package/sensor/dist/installer/targets/shared.d.ts.map +0 -1
- package/sensor/dist/installer/targets/shared.js +0 -264
- package/sensor/dist/installer/targets/shared.js.map +0 -1
- package/sensor/dist/installer/targets/toml.d.ts +0 -52
- package/sensor/dist/installer/targets/toml.d.ts.map +0 -1
- package/sensor/dist/installer/targets/toml.js +0 -147
- package/sensor/dist/installer/targets/toml.js.map +0 -1
- package/sensor/dist/installer/targets/types.d.ts +0 -108
- package/sensor/dist/installer/targets/types.d.ts.map +0 -1
- package/sensor/dist/installer/targets/types.js +0 -16
- package/sensor/dist/installer/targets/types.js.map +0 -1
- package/sensor/dist/upgrade/index.d.ts +0 -171
- package/sensor/dist/upgrade/index.d.ts.map +0 -1
- package/sensor/dist/upgrade/index.js +0 -659
- package/sensor/dist/upgrade/index.js.map +0 -1
- package/sensor/dist/upgrade/remove-binary.d.ts +0 -87
- package/sensor/dist/upgrade/remove-binary.d.ts.map +0 -1
- package/sensor/dist/upgrade/remove-binary.js +0 -289
- package/sensor/dist/upgrade/remove-binary.js.map +0 -1
- package/sensor/dist/upgrade/update-check.d.ts +0 -92
- package/sensor/dist/upgrade/update-check.d.ts.map +0 -1
- package/sensor/dist/upgrade/update-check.js +0 -258
- package/sensor/dist/upgrade/update-check.js.map +0 -1
- package/src/bootstrap.mjs +0 -88
package/src/runtime-cli.mjs
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { constants as fsConstants } from 'node:fs';
|
|
4
|
+
import {
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
open,
|
|
9
|
+
readFile,
|
|
10
|
+
readdir,
|
|
11
|
+
realpath,
|
|
12
|
+
rename,
|
|
13
|
+
rm,
|
|
14
|
+
unlink,
|
|
15
|
+
writeFile,
|
|
16
|
+
} from 'node:fs/promises';
|
|
3
17
|
import path from 'node:path';
|
|
4
18
|
|
|
5
|
-
import { digestArtifact } from './artifact-contracts.mjs';
|
|
6
|
-
import {
|
|
19
|
+
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
20
|
+
import { collectSensorEvidence } from './sensor-adapter.mjs';
|
|
21
|
+
import { detectCheckpointFindings } from './runtime-diff-observer.mjs';
|
|
7
22
|
import {
|
|
8
23
|
compileRuntimePlanV1,
|
|
9
24
|
evidenceFromCollectedOutcomes,
|
|
@@ -12,15 +27,54 @@ import {
|
|
|
12
27
|
validateRunRequest,
|
|
13
28
|
validateRuntimeBoundaryManifest,
|
|
14
29
|
validateRuntimePlan,
|
|
30
|
+
verifyRuntimePlanBinding,
|
|
31
|
+
selfDigest,
|
|
15
32
|
} from './runtime-contracts.mjs';
|
|
16
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
buildNextRunEvent,
|
|
35
|
+
buildExecutorPackets,
|
|
36
|
+
closeRunIfComplete,
|
|
37
|
+
initializeRunEvents,
|
|
38
|
+
} from './runtime-engine.mjs';
|
|
17
39
|
import {
|
|
18
40
|
computeReadyFrontier,
|
|
19
41
|
recomputeReceiptDecisions,
|
|
42
|
+
classifyObservedDiff,
|
|
20
43
|
} from './runtime-decision-verifier.mjs';
|
|
21
44
|
import { verifyRunEventChain } from './runtime-event-store.mjs';
|
|
22
|
-
import { projectRuntimeState } from './runtime-projection.mjs';
|
|
45
|
+
import { projectRuntimeState, projectRuntimeStatusOverlays } from './runtime-projection.mjs';
|
|
46
|
+
import {
|
|
47
|
+
decideHoldAndCarryOver,
|
|
48
|
+
recompileNextEpochPlan,
|
|
49
|
+
validateRuntimeRecompileRequest,
|
|
50
|
+
} from './runtime-hold-recompile.mjs';
|
|
23
51
|
import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
52
|
+
import {
|
|
53
|
+
activateEpochOneStore,
|
|
54
|
+
commitStagedSuccessorEpoch,
|
|
55
|
+
commitReleaseEpochBarrier,
|
|
56
|
+
RuntimeEpochStoreError,
|
|
57
|
+
isManagedRunFrozen,
|
|
58
|
+
readCommittedEpochStore,
|
|
59
|
+
recordRuntimeFinding,
|
|
60
|
+
stageSuccessorEpoch,
|
|
61
|
+
validateRuntimeEpochBundle,
|
|
62
|
+
validateRuntimeFindingCandidate,
|
|
63
|
+
validateRuntimeFindingRecord,
|
|
64
|
+
} from './runtime-multi-epoch-store.mjs';
|
|
65
|
+
import { createRuntimeControlRequest, validateRuntimeControlResponse } from './runtime-controller-protocol.mjs';
|
|
66
|
+
import { createRuntimeControlStore } from './runtime-control-store.mjs';
|
|
67
|
+
import { createRuntimeGateStore } from './runtime-gate-store.mjs';
|
|
68
|
+
import { acquireRuntimeLifecycleLock } from './runtime-lifecycle-lock.mjs';
|
|
69
|
+
import {
|
|
70
|
+
ManagedRuntimeError,
|
|
71
|
+
launchDurableSupervisor,
|
|
72
|
+
observeManagedProcessStartIdentity,
|
|
73
|
+
prepareManagedSupervisorRestart,
|
|
74
|
+
resolveActiveRuntimePaths,
|
|
75
|
+
sendRuntimeActivationRequest,
|
|
76
|
+
sendRuntimeControlRequest,
|
|
77
|
+
} from './runtime-managed-supervisor.mjs';
|
|
24
78
|
|
|
25
79
|
/**
|
|
26
80
|
* RC3-D CLI surface(ADR 0044 Decision 8)。
|
|
@@ -28,17 +82,20 @@ import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
|
28
82
|
* lattice plan compile --request <run-request.json>
|
|
29
83
|
* lattice plan verify --request <run-request.json> --plan <plan.json>
|
|
30
84
|
* lattice run start --request <run-request.json> --executor <adapter>
|
|
31
|
-
* lattice run observe --run
|
|
32
|
-
* lattice run status --run
|
|
33
|
-
* lattice
|
|
85
|
+
* lattice run observe --run .lattice/runs/<run-id>
|
|
86
|
+
* lattice run status --run .lattice/runs/<run-id>
|
|
87
|
+
* lattice run resume --run .lattice/runs/<run-id>
|
|
88
|
+
* lattice run close --run .lattice/runs/<run-id>
|
|
89
|
+
* lattice run abandon --run .lattice/runs/<run-id> --reason <reason>
|
|
90
|
+
* lattice event verify --run .lattice/runs/<run-id>
|
|
34
91
|
*
|
|
35
92
|
* - stdout: versioned JSON 1行のみ。診断はstderr。
|
|
36
93
|
* - exit 0: 成功(artifact refとdigestを含むversioned JSON)。
|
|
37
|
-
* - exit 1: typed契約失敗。stderrへ`lattice.cli_error.
|
|
94
|
+
* - exit 1: typed契約失敗。stderrへ`lattice.cli_error.v2` JSON 1行。
|
|
38
95
|
* - exit 2: usage違反(未知command、引数の欠落・重複・余剰・順序不正)。
|
|
39
96
|
* - `--executor`等の暗黙fallbackを持たず、未実装surfaceはusage違反として拒否する。
|
|
40
97
|
*
|
|
41
|
-
* plan verifyは保存planを信用せず、requestとfresh
|
|
98
|
+
* plan verifyは保存planを信用せず、requestとfresh LatticeSensor観測からplanを
|
|
42
99
|
* producerで再コンパイルしてdigest完全一致を要求し、さらにschedule minimumを
|
|
43
100
|
* producer非依存の`verifySchedulabilityPlanV2`で再計算する(成功条件5)。
|
|
44
101
|
*/
|
|
@@ -46,16 +103,19 @@ import { verifySchedulabilityPlanV2 } from './schedulability-verifier-v2.mjs';
|
|
|
46
103
|
const MAX_INPUT_BYTES = 8_388_608;
|
|
47
104
|
const COMPILE_RESULT_SCHEMA = 'lattice.plan_compile_result.v1';
|
|
48
105
|
const VERIFY_RESULT_SCHEMA = 'lattice.plan_verify_result.v1';
|
|
49
|
-
const CLI_ERROR_SCHEMA = 'lattice.cli_error.
|
|
50
|
-
// run
|
|
51
|
-
const RUN_STORE_ROOT = ['
|
|
106
|
+
const CLI_ERROR_SCHEMA = 'lattice.cli_error.v2';
|
|
107
|
+
// 現役run storeは対象Git repo内のLattice-owned・ignored rootへ限定する。
|
|
108
|
+
const RUN_STORE_ROOT = ['.lattice', 'runs'];
|
|
109
|
+
const RUN_REF = /^\.lattice\/runs\/([0-9A-Za-z](?:[0-9A-Za-z._-]{0,127}))$/u;
|
|
110
|
+
const ABANDON_REASON = /^[0-9A-Za-z](?:[0-9A-Za-z._:-]{0,127})$/u;
|
|
52
111
|
const KNOWN_ADAPTERS = Object.freeze(['scripted', 'isolated-worktree', 'actual-agent']);
|
|
53
112
|
|
|
54
113
|
class CliContractError extends Error {
|
|
55
|
-
constructor(code, message) {
|
|
114
|
+
constructor(code, message, detail) {
|
|
56
115
|
super(message);
|
|
57
116
|
this.name = 'CliContractError';
|
|
58
117
|
this.code = code;
|
|
118
|
+
this.detail = detail;
|
|
59
119
|
}
|
|
60
120
|
}
|
|
61
121
|
|
|
@@ -67,7 +127,10 @@ function usageFailure(stderr, args) {
|
|
|
67
127
|
|
|
68
128
|
function typedFailure(stderr, code, message, detail) {
|
|
69
129
|
const payload = { schema: CLI_ERROR_SCHEMA, code, message };
|
|
70
|
-
|
|
130
|
+
// ADR 0052 Decision 1: detailは非空plain objectの場合だけ出す(null・非object・空objectは省略)。
|
|
131
|
+
if (detail !== null && typeof detail === 'object' && !Array.isArray(detail) && Object.keys(detail).length > 0) {
|
|
132
|
+
payload.detail = detail;
|
|
133
|
+
}
|
|
71
134
|
stderr.write(`${JSON.stringify(payload)}\n`);
|
|
72
135
|
return 1;
|
|
73
136
|
}
|
|
@@ -133,6 +196,57 @@ async function resolveRepoBinding(cwd, request) {
|
|
|
133
196
|
}
|
|
134
197
|
}
|
|
135
198
|
|
|
199
|
+
async function resolveRepoRoot(cwd) {
|
|
200
|
+
const root = await runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
201
|
+
if (root.code !== 0 || root.stdout.trim().length === 0) {
|
|
202
|
+
throw new CliContractError('REPO_UNRESOLVED', `cwdのgit rootを解決できない: ${root.stderr.trim()}`);
|
|
203
|
+
}
|
|
204
|
+
return path.resolve(root.stdout.trim());
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function requireIgnoredRunStore(repoRoot) {
|
|
208
|
+
const probe = '.lattice/runs/.lattice-ignore-probe';
|
|
209
|
+
const ignored = await runGit(['check-ignore', '-q', '--', probe], repoRoot);
|
|
210
|
+
if (ignored.code !== 0) {
|
|
211
|
+
throw new CliContractError(
|
|
212
|
+
'RUN_STORE_NOT_IGNORED',
|
|
213
|
+
'.lattice/runs/ がgit ignore対象ではない',
|
|
214
|
+
{ guidance: '.gitignoreへ `.lattice/runs/` を追加してから再実行する' },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async function requireSafeRunAncestors(repoRoot) {
|
|
220
|
+
for (const relative of ['.lattice', '.lattice/runs']) {
|
|
221
|
+
try {
|
|
222
|
+
const state = await lstat(path.join(repoRoot, relative));
|
|
223
|
+
if (state.isSymbolicLink() || !state.isDirectory()) {
|
|
224
|
+
throw new CliContractError('INVALID_RUN_STORE', `${relative}が安全なdirectoryではない`);
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function resolveRunStore(cwd, runRef) {
|
|
233
|
+
const normalized = runRef.replaceAll('\\', '/');
|
|
234
|
+
const match = RUN_REF.exec(normalized);
|
|
235
|
+
if (match === null || path.isAbsolute(runRef)) {
|
|
236
|
+
throw new CliContractError(
|
|
237
|
+
'INVALID_RUN_REF',
|
|
238
|
+
'run refは `.lattice/runs/<run-id>` のrepo相対形式でなければならない',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
242
|
+
await requireSafeRunAncestors(repoRoot);
|
|
243
|
+
return { repoRoot, runDir: path.join(repoRoot, ...RUN_STORE_ROOT, match[1]), runRef: normalized };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function canonicalNow() {
|
|
247
|
+
return new Date().toISOString();
|
|
248
|
+
}
|
|
249
|
+
|
|
136
250
|
async function loadRequest(requestPath) {
|
|
137
251
|
const request = await readBoundedJson(requestPath, 'run request');
|
|
138
252
|
if (!validateRunRequest(request)) {
|
|
@@ -142,20 +256,20 @@ async function loadRequest(requestPath) {
|
|
|
142
256
|
}
|
|
143
257
|
|
|
144
258
|
async function compileFromRepo({ request, cwd, planRef, planEpoch, predecessorRefs }) {
|
|
145
|
-
const collected = await
|
|
259
|
+
const collected = await collectSensorEvidence({
|
|
146
260
|
cwd,
|
|
147
|
-
querySet: request.
|
|
261
|
+
querySet: request.sensor_query_set,
|
|
148
262
|
});
|
|
149
263
|
// 観測中にHEADが動いた場合、観測とbase束縛が別snapshotになるためreject
|
|
150
264
|
// (TOCTOU窓の閉鎖。base再確認は観測の後に行う)。
|
|
151
265
|
await resolveRepoBinding(cwd, request);
|
|
152
|
-
const
|
|
153
|
-
querySet: request.
|
|
266
|
+
const sensorEvidence = evidenceFromCollectedOutcomes({
|
|
267
|
+
querySet: request.sensor_query_set,
|
|
154
268
|
collected,
|
|
155
269
|
});
|
|
156
270
|
return compileRuntimePlanV1({
|
|
157
271
|
request,
|
|
158
|
-
|
|
272
|
+
sensorEvidence,
|
|
159
273
|
planRef,
|
|
160
274
|
planEpoch,
|
|
161
275
|
predecessorRefs,
|
|
@@ -213,7 +327,7 @@ async function planVerify({ requestPath, planPath, cwd, stdout }) {
|
|
|
213
327
|
}
|
|
214
328
|
const planNodeIds = plan.nodes.map((node) => node.todo_id);
|
|
215
329
|
if (artifact.manifests === null || typeof artifact.manifests !== 'object' || Array.isArray(artifact.manifests)
|
|
216
|
-
|| Object.keys(artifact.manifests).sort().join('
|
|
330
|
+
|| Object.keys(artifact.manifests).sort().join('\0') !== [...planNodeIds].sort().join('\0')) {
|
|
217
331
|
throw new CliContractError('INVALID_PLAN_ARTIFACT', 'manifestsのkey集合がplan nodesと一致しない');
|
|
218
332
|
}
|
|
219
333
|
for (const todoId of planNodeIds) {
|
|
@@ -274,6 +388,15 @@ function runStorePath(cwd, runId) {
|
|
|
274
388
|
}
|
|
275
389
|
|
|
276
390
|
async function readRunStore(runDir) {
|
|
391
|
+
let runState;
|
|
392
|
+
try {
|
|
393
|
+
runState = await lstat(runDir);
|
|
394
|
+
} catch {
|
|
395
|
+
throw new CliContractError('INVALID_RUN_STORE', 'run store directoryを読めない');
|
|
396
|
+
}
|
|
397
|
+
if (runState.isSymbolicLink() || !runState.isDirectory()) {
|
|
398
|
+
throw new CliContractError('INVALID_RUN_STORE', 'run storeが安全なdirectoryではない');
|
|
399
|
+
}
|
|
277
400
|
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
278
401
|
if (!Array.isArray(events)) {
|
|
279
402
|
throw new CliContractError('INVALID_RUN_STORE', 'events.jsonがarrayではない');
|
|
@@ -282,7 +405,196 @@ async function readRunStore(runDir) {
|
|
|
282
405
|
const compileArtifact = await readBoundedJson(
|
|
283
406
|
path.join(runDir, 'plan-compile-result.json'), 'plan compile result',
|
|
284
407
|
);
|
|
285
|
-
|
|
408
|
+
const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
|
|
409
|
+
const compileKeys = ['schema', 'request_digest', 'plan', 'manifests', 'schedule', 'graph_digest', 'result_digest'];
|
|
410
|
+
const metaKeys = ['schema', 'run_id', 'executor_adapter', 'plan_digest'];
|
|
411
|
+
const { result_digest: claimedCompileDigest, ...compileBody } = compileArtifact ?? {};
|
|
412
|
+
const legacyMetaValid = meta !== null && typeof meta === 'object' && !Array.isArray(meta)
|
|
413
|
+
&& Object.keys(meta).sort().join('\0') === metaKeys.sort().join('\0')
|
|
414
|
+
&& meta.schema === 'lattice.run_meta.v1'
|
|
415
|
+
&& meta.run_id === request.request_id
|
|
416
|
+
&& KNOWN_ADAPTERS.includes(meta.executor_adapter)
|
|
417
|
+
&& compileArtifact?.plan?.plan_digest === meta.plan_digest;
|
|
418
|
+
const managed = meta?.schema === 'lattice.run_meta.v2'
|
|
419
|
+
? await readCommittedEpochStore(runDir)
|
|
420
|
+
: null;
|
|
421
|
+
const managedMetaValid = managed !== null
|
|
422
|
+
&& managed.meta.run_id === request.request_id
|
|
423
|
+
&& KNOWN_ADAPTERS.includes(managed.meta.executor_adapter)
|
|
424
|
+
&& managed.meta.created_plan_digest === compileArtifact.plan.plan_digest;
|
|
425
|
+
if (!validateRunRequest(request)
|
|
426
|
+
|| (!legacyMetaValid && !managedMetaValid)
|
|
427
|
+
|| compileArtifact === null || typeof compileArtifact !== 'object' || Array.isArray(compileArtifact)
|
|
428
|
+
|| Object.keys(compileArtifact).sort().join('\0') !== compileKeys.sort().join('\0')
|
|
429
|
+
|| compileArtifact.schema !== COMPILE_RESULT_SCHEMA
|
|
430
|
+
|| claimedCompileDigest !== digestArtifact(compileBody)
|
|
431
|
+
|| compileArtifact.request_digest !== request.request_digest
|
|
432
|
+
|| !validateRuntimePlan(compileArtifact?.plan)
|
|
433
|
+
|| !verifyRuntimePlanBinding({ plan: compileArtifact.plan, request })
|
|
434
|
+
) {
|
|
435
|
+
throw new CliContractError('INVALID_RUN_STORE', 'run storeのartifact bindingが不正');
|
|
436
|
+
}
|
|
437
|
+
if (managed === null) return { events, meta, compileArtifact, request, managed: null };
|
|
438
|
+
return {
|
|
439
|
+
events,
|
|
440
|
+
meta: {
|
|
441
|
+
schema: 'lattice.run_meta.v1',
|
|
442
|
+
run_id: managed.meta.run_id,
|
|
443
|
+
executor_adapter: managed.meta.executor_adapter,
|
|
444
|
+
plan_digest: managed.bundle.plan.plan_digest,
|
|
445
|
+
},
|
|
446
|
+
compileArtifact: {
|
|
447
|
+
...compileArtifact,
|
|
448
|
+
request_digest: managed.bundle.request.request_digest,
|
|
449
|
+
plan: managed.bundle.plan,
|
|
450
|
+
manifests: managed.bundle.manifests,
|
|
451
|
+
},
|
|
452
|
+
request: managed.bundle.request,
|
|
453
|
+
managed,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function writeJsonFile(filePath, value) {
|
|
458
|
+
await writeFile(filePath, `${JSON.stringify(value, null, 1)}\n`, { mode: 0o600, flag: 'wx' });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
async function writeCanonicalJsonFile(filePath, value) {
|
|
462
|
+
await writeFile(filePath, `${canonicalizeArtifact(value)}\n`, { mode: 0o600, flag: 'wx' });
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function replaceEventsAtomically(runDir, events) {
|
|
466
|
+
const temporaryPath = path.join(runDir, `.events-${process.pid}-${Date.now()}.tmp`);
|
|
467
|
+
await writeJsonFile(temporaryPath, events);
|
|
468
|
+
await rename(temporaryPath, path.join(runDir, 'events.json'));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function replaceNamedJsonAtomically(runDir, name, value) {
|
|
472
|
+
await durableReplaceBytes(runDir, name, Buffer.from(`${JSON.stringify(value, null, 1)}\n`));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function replaceCanonicalJsonAtomically(directory, name, value) {
|
|
476
|
+
await durableReplaceBytes(directory, name, Buffer.from(`${canonicalizeArtifact(value)}\n`));
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function runtimeEventBatchReceiptName(transactionId, phase) {
|
|
480
|
+
return `run-event-batch-${digestArtifact({ transaction_id: transactionId, phase })}.json`;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function sameDigestPrefix(left, right) {
|
|
484
|
+
return left.length <= right.length && left.every((digest, index) => digest === right[index]);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Recompileのrun event副作用をtransaction単位でexact-once publishする。
|
|
489
|
+
* receiptをevents.jsonより先にdurable化するため、receipt直後のcrashは同じ
|
|
490
|
+
* event object(recorded_atを含む)をroll-forwardできる。events.jsonはatomic
|
|
491
|
+
* replaceだが、検証はpartial prefixも受け、別branchや別batchは拒否する。
|
|
492
|
+
*/
|
|
493
|
+
async function publishRuntimeEventBatch({ runDir, transactionId, phase, planEpoch,
|
|
494
|
+
bindingDigest, currentEvents, proposedBatch, crashInjector }) {
|
|
495
|
+
if (!verifyRunEventChain({ events: currentEvents }).valid) {
|
|
496
|
+
throw new ManagedRuntimeError('EVENT_CHAIN_INVALID', 'event batch publish前のrun event chainが不正');
|
|
497
|
+
}
|
|
498
|
+
const receiptName = runtimeEventBatchReceiptName(transactionId, phase);
|
|
499
|
+
const receiptPath = path.join(runDir, receiptName);
|
|
500
|
+
let receipt = await readBoundedJson(receiptPath, 'runtime event batch receipt').catch((error) => {
|
|
501
|
+
if (error?.code === 'INPUT_UNREADABLE') return null;
|
|
502
|
+
throw error;
|
|
503
|
+
});
|
|
504
|
+
if (receipt === null) {
|
|
505
|
+
const combined = [...currentEvents, ...proposedBatch];
|
|
506
|
+
const chain = verifyRunEventChain({ events: combined });
|
|
507
|
+
if (!chain.valid) {
|
|
508
|
+
throw new ManagedRuntimeError('EVENT_CHAIN_INVALID', 'recompile event batchのchainが不正');
|
|
509
|
+
}
|
|
510
|
+
receipt = {
|
|
511
|
+
schema: 'lattice.runtime_run_event_batch_receipt.v1',
|
|
512
|
+
transaction_id: transactionId,
|
|
513
|
+
phase,
|
|
514
|
+
plan_epoch: planEpoch,
|
|
515
|
+
binding_digest: bindingDigest,
|
|
516
|
+
base_event_digests: currentEvents.map((event) => event.event_digest),
|
|
517
|
+
batch_events: structuredClone(proposedBatch),
|
|
518
|
+
ordered_event_digests: proposedBatch.map((event) => event.event_digest),
|
|
519
|
+
batch_digest: digestArtifact(proposedBatch.map((event) => event.event_digest)),
|
|
520
|
+
receipt_digest: '',
|
|
521
|
+
};
|
|
522
|
+
receipt.receipt_digest = selfDigest(receipt, 'receipt_digest');
|
|
523
|
+
await replaceCanonicalJsonAtomically(runDir, receiptName, receipt);
|
|
524
|
+
if (typeof crashInjector === 'function') await crashInjector('after_run_event_batch_receipt', {
|
|
525
|
+
transaction_id: transactionId, phase, receipt_digest: receipt.receipt_digest,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const receiptValid = receipt?.schema === 'lattice.runtime_run_event_batch_receipt.v1'
|
|
529
|
+
&& receipt.transaction_id === transactionId
|
|
530
|
+
&& receipt.phase === phase
|
|
531
|
+
&& receipt.plan_epoch === planEpoch
|
|
532
|
+
&& receipt.binding_digest === bindingDigest
|
|
533
|
+
&& receipt.receipt_digest === selfDigest(receipt, 'receipt_digest')
|
|
534
|
+
&& Array.isArray(receipt.base_event_digests)
|
|
535
|
+
&& Array.isArray(receipt.batch_events)
|
|
536
|
+
&& Array.isArray(receipt.ordered_event_digests)
|
|
537
|
+
&& canonicalizeArtifact(receipt.batch_events.map((event) => event.event_digest))
|
|
538
|
+
=== canonicalizeArtifact(receipt.ordered_event_digests)
|
|
539
|
+
&& receipt.batch_digest === digestArtifact(receipt.ordered_event_digests);
|
|
540
|
+
if (!receiptValid) {
|
|
541
|
+
throw new ManagedRuntimeError('RECOMPILE_EVENT_BATCH_CONFLICT', 'event batch receiptのbindingが不正');
|
|
542
|
+
}
|
|
543
|
+
const currentDigests = currentEvents.map((event) => event.event_digest);
|
|
544
|
+
const fullDigests = [...receipt.base_event_digests, ...receipt.ordered_event_digests];
|
|
545
|
+
if (!sameDigestPrefix(currentDigests, fullDigests) && !sameDigestPrefix(fullDigests, currentDigests)) {
|
|
546
|
+
throw new ManagedRuntimeError('RECOMPILE_EVENT_BATCH_CONFLICT', 'events.jsonがreceiptと異なるbranchにある');
|
|
547
|
+
}
|
|
548
|
+
if (currentDigests.length >= fullDigests.length) return currentEvents;
|
|
549
|
+
if (currentDigests.length < receipt.base_event_digests.length) {
|
|
550
|
+
throw new ManagedRuntimeError('RECOMPILE_EVENT_BATCH_CONFLICT', 'receiptのbase event chainが欠落している');
|
|
551
|
+
}
|
|
552
|
+
const baseEvents = currentEvents.slice(0, receipt.base_event_digests.length);
|
|
553
|
+
const completed = [...baseEvents, ...receipt.batch_events];
|
|
554
|
+
const chain = verifyRunEventChain({ events: completed });
|
|
555
|
+
if (!chain.valid) {
|
|
556
|
+
throw new ManagedRuntimeError('EVENT_CHAIN_INVALID', 'receiptから復元したevent batchのchainが不正');
|
|
557
|
+
}
|
|
558
|
+
await replaceEventsAtomically(runDir, completed);
|
|
559
|
+
return completed;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function durableReplaceBytes(directory, name, bytes) {
|
|
563
|
+
const temporaryPath = path.join(directory, `.${name}-${process.pid}-${randomUUID()}.tmp`);
|
|
564
|
+
let handle;
|
|
565
|
+
try {
|
|
566
|
+
handle = await open(temporaryPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
|
|
567
|
+
await handle.writeFile(bytes);
|
|
568
|
+
await handle.sync();
|
|
569
|
+
await handle.close();
|
|
570
|
+
handle = null;
|
|
571
|
+
await rename(temporaryPath, path.join(directory, name));
|
|
572
|
+
const directoryHandle = await open(directory, fsConstants.O_RDONLY);
|
|
573
|
+
try { await directoryHandle.sync(); } finally { await directoryHandle.close(); }
|
|
574
|
+
} finally {
|
|
575
|
+
if (handle) await handle.close().catch(() => {});
|
|
576
|
+
await rm(temporaryPath, { force: true }).catch(() => {});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
async function withLifecycleLock(runDir, action) {
|
|
581
|
+
// managed mutationの正本lockはdurable supervisorが所有する。これはunmanaged/launch CLI競合だけを塞ぐ。
|
|
582
|
+
const lockPath = path.join(runDir, '.cli-lifecycle.lock');
|
|
583
|
+
let handle;
|
|
584
|
+
try {
|
|
585
|
+
handle = await open(lockPath, 'wx', 0o600);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
if (error?.code === 'EEXIST') {
|
|
588
|
+
throw new CliContractError('RUN_BUSY', 'run lifecycle操作が既に進行中');
|
|
589
|
+
}
|
|
590
|
+
throw error;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
return await action();
|
|
594
|
+
} finally {
|
|
595
|
+
await handle.close();
|
|
596
|
+
await unlink(lockPath).catch(() => {});
|
|
597
|
+
}
|
|
286
598
|
}
|
|
287
599
|
|
|
288
600
|
async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
@@ -295,10 +607,13 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
|
295
607
|
|| !request.executor_capability.adapters.includes(executorAdapter)) {
|
|
296
608
|
throw new CliContractError('UNKNOWN_ADAPTER', 'requestのexecutor_capabilityに含まれないadapter');
|
|
297
609
|
}
|
|
298
|
-
await
|
|
610
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
611
|
+
await requireSafeRunAncestors(repoRoot);
|
|
612
|
+
await requireIgnoredRunStore(repoRoot);
|
|
613
|
+
await resolveRepoBinding(repoRoot, request);
|
|
299
614
|
const result = await compileFromRepo({
|
|
300
615
|
request,
|
|
301
|
-
cwd,
|
|
616
|
+
cwd: repoRoot,
|
|
302
617
|
planRef: `plan-${request.request_id}-e1`,
|
|
303
618
|
planEpoch: 1,
|
|
304
619
|
predecessorRefs: [],
|
|
@@ -322,13 +637,16 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
|
322
637
|
manifests: result.manifests,
|
|
323
638
|
recordedAt: new Date().toISOString().replace(/\.\d+Z$/u, '.000Z'),
|
|
324
639
|
});
|
|
325
|
-
const runDir = runStorePath(
|
|
326
|
-
const { mkdir, writeFile } = await import('node:fs/promises');
|
|
640
|
+
const runDir = runStorePath(repoRoot, request.request_id);
|
|
327
641
|
await mkdir(path.dirname(runDir), { recursive: true });
|
|
642
|
+
const temporaryDir = await mkdtemp(path.join(path.dirname(runDir), `.${request.request_id}.tmp-`));
|
|
328
643
|
try {
|
|
329
|
-
await
|
|
644
|
+
await lstat(runDir);
|
|
645
|
+
await rm(temporaryDir, { recursive: true, force: true });
|
|
646
|
+
throw new CliContractError('RUN_EXISTS', 'run storeが既に存在する');
|
|
330
647
|
} catch (error) {
|
|
331
|
-
|
|
648
|
+
if (error instanceof CliContractError) throw error;
|
|
649
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
332
650
|
}
|
|
333
651
|
const meta = {
|
|
334
652
|
schema: 'lattice.run_meta.v1',
|
|
@@ -336,14 +654,23 @@ async function runStart({ requestPath, executorAdapter, cwd, stdout }) {
|
|
|
336
654
|
executor_adapter: executorAdapter,
|
|
337
655
|
plan_digest: result.plan.plan_digest,
|
|
338
656
|
};
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
657
|
+
try {
|
|
658
|
+
await writeJsonFile(path.join(temporaryDir, 'request.json'), request);
|
|
659
|
+
await writeJsonFile(path.join(temporaryDir, 'plan-compile-result.json'), compileArtifact);
|
|
660
|
+
await writeJsonFile(path.join(temporaryDir, 'events.json'), events);
|
|
661
|
+
await writeJsonFile(path.join(temporaryDir, 'run-meta.json'), meta);
|
|
662
|
+
await rename(temporaryDir, runDir);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
await rm(temporaryDir, { recursive: true, force: true });
|
|
665
|
+
if (error?.code === 'EEXIST' || error?.code === 'ENOTEMPTY') {
|
|
666
|
+
throw new CliContractError('RUN_EXISTS', 'run storeが既に存在する');
|
|
667
|
+
}
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
343
670
|
const output = {
|
|
344
671
|
schema: 'lattice.run_start_result.v1',
|
|
345
672
|
run_id: request.request_id,
|
|
346
|
-
run_dir: path.relative(
|
|
673
|
+
run_dir: path.relative(repoRoot, runDir),
|
|
347
674
|
executor_adapter: executorAdapter,
|
|
348
675
|
plan_digest: result.plan.plan_digest,
|
|
349
676
|
events_digest: digestArtifact(events.map(({ event_digest: digest }) => digest)),
|
|
@@ -357,7 +684,7 @@ async function runObserve({ runDir, stdout }) {
|
|
|
357
684
|
const { events } = await readRunStore(runDir);
|
|
358
685
|
const chain = verifyRunEventChain({ events });
|
|
359
686
|
if (!chain.valid) {
|
|
360
|
-
throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', chain.failed_conditions);
|
|
687
|
+
throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', { failed_conditions: chain.failed_conditions });
|
|
361
688
|
}
|
|
362
689
|
const state = projectRuntimeState({ events });
|
|
363
690
|
const output = {
|
|
@@ -378,13 +705,15 @@ async function runObserve({ runDir, stdout }) {
|
|
|
378
705
|
}
|
|
379
706
|
|
|
380
707
|
async function runStatus({ runDir, stdout }) {
|
|
381
|
-
const { events, meta, compileArtifact } = await readRunStore(runDir);
|
|
708
|
+
const { events, meta, compileArtifact, managed } = await readRunStore(runDir);
|
|
382
709
|
const chain = verifyRunEventChain({ events });
|
|
383
710
|
if (!chain.valid) {
|
|
384
|
-
throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', chain.failed_conditions);
|
|
711
|
+
throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', { failed_conditions: chain.failed_conditions });
|
|
385
712
|
}
|
|
386
713
|
const state = projectRuntimeState({ events });
|
|
387
|
-
const
|
|
714
|
+
const computedFrontier = computeReadyFrontier({ plan: compileArtifact.plan, events });
|
|
715
|
+
const managedFrozen = managed === null ? false : await isManagedRunFrozen(runDir, events);
|
|
716
|
+
const frontier = managedFrozen ? { dispatchable: [] } : computedFrontier;
|
|
388
717
|
const output = {
|
|
389
718
|
schema: 'lattice.run_status.v1',
|
|
390
719
|
run_id: meta.run_id,
|
|
@@ -397,11 +726,1961 @@ async function runStatus({ runDir, stdout }) {
|
|
|
397
726
|
closed: state.closed,
|
|
398
727
|
event_count: events.length,
|
|
399
728
|
};
|
|
729
|
+
if (managed !== null) {
|
|
730
|
+
output.schema = 'lattice.managed_run_status.v1';
|
|
731
|
+
const runtimeProjection = {
|
|
732
|
+
schema: 'lattice.runtime_status_projection.v1',
|
|
733
|
+
...projectRuntimeStatusOverlays({ events }),
|
|
734
|
+
runtime_frozen: managedFrozen,
|
|
735
|
+
};
|
|
736
|
+
runtimeProjection.projection_digest = digestArtifact(runtimeProjection);
|
|
737
|
+
output.runtime_projection = runtimeProjection;
|
|
738
|
+
}
|
|
739
|
+
output.result_digest = digestArtifact(output);
|
|
740
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
741
|
+
return 0;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function runList({ cwd, stdout }) {
|
|
745
|
+
const repoRoot = await resolveRepoRoot(cwd);
|
|
746
|
+
await requireSafeRunAncestors(repoRoot);
|
|
747
|
+
const root = path.join(repoRoot, ...RUN_STORE_ROOT);
|
|
748
|
+
let entries;
|
|
749
|
+
try {
|
|
750
|
+
const rootState = await lstat(root);
|
|
751
|
+
if (rootState.isSymbolicLink() || !rootState.isDirectory()) {
|
|
752
|
+
throw new CliContractError('INVALID_RUN_STORE', '.lattice/runsが安全なdirectoryではない');
|
|
753
|
+
}
|
|
754
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (error?.code === 'ENOENT') entries = [];
|
|
757
|
+
else throw error;
|
|
758
|
+
}
|
|
759
|
+
const activeRuns = [];
|
|
760
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
761
|
+
if (!entry.isDirectory() || !RUN_REF.test(`.lattice/runs/${entry.name}`)) {
|
|
762
|
+
throw new CliContractError('INVALID_RUN_STORE', `不正なrun store entry: ${entry.name}`);
|
|
763
|
+
}
|
|
764
|
+
const runDir = path.join(root, entry.name);
|
|
765
|
+
const { events, meta, request } = await readRunStore(runDir);
|
|
766
|
+
requireValidEventChain(events);
|
|
767
|
+
if (!projectRuntimeState({ events }).closed) {
|
|
768
|
+
activeRuns.push({
|
|
769
|
+
run_id: meta.run_id,
|
|
770
|
+
run_ref: `.lattice/runs/${entry.name}`,
|
|
771
|
+
base_sha: request.repo.base_sha,
|
|
772
|
+
executor_adapter: meta.executor_adapter,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
const output = { schema: 'lattice.run_list.v1', active_runs: activeRuns };
|
|
777
|
+
output.result_digest = digestArtifact(output);
|
|
778
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function requireValidEventChain(events) {
|
|
783
|
+
const chain = verifyRunEventChain({ events });
|
|
784
|
+
if (!chain.valid) {
|
|
785
|
+
throw new CliContractError('EVENT_CHAIN_INVALID', 'event chainが不正', { failed_conditions: chain.failed_conditions });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function runResume({ runDir, repoRoot, stdout }) {
|
|
790
|
+
const { events, meta, compileArtifact, request, managed } = await readRunStore(runDir);
|
|
791
|
+
requireValidEventChain(events);
|
|
792
|
+
const state = projectRuntimeState({ events });
|
|
793
|
+
if (state.closed) {
|
|
794
|
+
throw new CliContractError('RUN_CLOSED', 'closed runはresumeできない');
|
|
795
|
+
}
|
|
796
|
+
await resolveRepoBinding(repoRoot, request);
|
|
797
|
+
// managed write gateの完全検証とdispatchはsupervisorだけが所有する。read-only
|
|
798
|
+
// resumeがCLIからleaseを再認可しないよう、managed runでは常に空frontierを返す。
|
|
799
|
+
const managedFrozen = managed === null ? false : await isManagedRunFrozen(runDir, events);
|
|
800
|
+
const frontier = managedFrozen
|
|
801
|
+
? { dispatchable: [] }
|
|
802
|
+
: computeReadyFrontier({ plan: compileArtifact.plan, events });
|
|
803
|
+
const output = {
|
|
804
|
+
schema: 'lattice.run_resume_result.v1',
|
|
805
|
+
outcome: 'resumable',
|
|
806
|
+
run_id: meta.run_id,
|
|
807
|
+
executor_adapter: meta.executor_adapter,
|
|
808
|
+
dispatchable: frontier.dispatchable,
|
|
809
|
+
running: state.running,
|
|
810
|
+
accepted: state.accepted,
|
|
811
|
+
event_count: events.length,
|
|
812
|
+
};
|
|
400
813
|
output.result_digest = digestArtifact(output);
|
|
401
814
|
stdout.write(`${JSON.stringify(output)}\n`);
|
|
402
815
|
return 0;
|
|
403
816
|
}
|
|
404
817
|
|
|
818
|
+
async function runClose({ runDir, repoRoot, stdout, requestId = null }) {
|
|
819
|
+
return withLifecycleLock(runDir, async () => {
|
|
820
|
+
const { events, meta, compileArtifact, request, managed } = await readRunStore(runDir);
|
|
821
|
+
requireValidEventChain(events);
|
|
822
|
+
const initialState = projectRuntimeState({ events });
|
|
823
|
+
let next = events;
|
|
824
|
+
let alreadyClosed = initialState.closed;
|
|
825
|
+
const existingClose = events.findLast((event) => event.kind === 'run_closed');
|
|
826
|
+
if (alreadyClosed && existingClose?.payload?.outcome === 'abandoned') {
|
|
827
|
+
throw new CliContractError('RUN_ABANDONED', 'abandoned runは正常closeへ変更できない');
|
|
828
|
+
}
|
|
829
|
+
await resolveRepoBinding(repoRoot, request);
|
|
830
|
+
if (!alreadyClosed) {
|
|
831
|
+
const closed = closeRunIfComplete({
|
|
832
|
+
runId: meta.run_id,
|
|
833
|
+
plan: compileArtifact.plan,
|
|
834
|
+
events,
|
|
835
|
+
recordedAt: canonicalNow(),
|
|
836
|
+
});
|
|
837
|
+
if (!closed.closed) {
|
|
838
|
+
throw new CliContractError(
|
|
839
|
+
'RUN_NOT_COMPLETE',
|
|
840
|
+
'全TODOのreceipt accepted前はrunを正常closeできない',
|
|
841
|
+
{ guidance: 'run statusで未完了TODOを確認するか、run abandonを明示実行する' },
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
if (managed !== null) {
|
|
845
|
+
await runManagedControl({ runDir, runRef: path.relative(repoRoot, runDir),
|
|
846
|
+
operation: 'close', artifactDigest: null, stdout, shutdownReason: 'normal-close',
|
|
847
|
+
emit: false, requestId });
|
|
848
|
+
next = (await readRunStore(runDir)).events;
|
|
849
|
+
} else {
|
|
850
|
+
next = closed.events;
|
|
851
|
+
await replaceEventsAtomically(runDir, next);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const output = {
|
|
855
|
+
schema: 'lattice.run_close_result.v1',
|
|
856
|
+
outcome: 'closed',
|
|
857
|
+
run_id: meta.run_id,
|
|
858
|
+
already_closed: alreadyClosed,
|
|
859
|
+
event_count: next.length,
|
|
860
|
+
events_digest: digestArtifact(next.map(({ event_digest: digest }) => digest)),
|
|
861
|
+
};
|
|
862
|
+
output.result_digest = digestArtifact(output);
|
|
863
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
864
|
+
return 0;
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
async function runAbandon({ runDir, runRef, reason, stdout, requestId = null }) {
|
|
869
|
+
if (!ABANDON_REASON.test(reason)) {
|
|
870
|
+
throw new CliContractError('INVALID_ABANDON_REASON', 'reasonは128文字以下の識別子でなければならない');
|
|
871
|
+
}
|
|
872
|
+
return withLifecycleLock(runDir, async () => {
|
|
873
|
+
const current = await readRunStore(runDir);
|
|
874
|
+
const { events, meta, compileArtifact } = current;
|
|
875
|
+
requireValidEventChain(events);
|
|
876
|
+
const state = projectRuntimeState({ events });
|
|
877
|
+
if (state.closed) {
|
|
878
|
+
const existing = events.findLast((event) => event.kind === 'run_closed');
|
|
879
|
+
if (current.managed !== null && existing?.payload?.outcome === 'abandoned'
|
|
880
|
+
&& existing.payload.reason === reason) {
|
|
881
|
+
const output = { schema: 'lattice.run_abandon_result.v1', outcome: 'abandoned', reason,
|
|
882
|
+
run_id: meta.run_id, event_count: events.length,
|
|
883
|
+
events_digest: digestArtifact(events.map(({ event_digest: value }) => value)) };
|
|
884
|
+
output.result_digest = digestArtifact(output); stdout.write(`${JSON.stringify(output)}\n`); return 0;
|
|
885
|
+
}
|
|
886
|
+
throw new CliContractError('RUN_CLOSED', 'closed runはabandonできない');
|
|
887
|
+
}
|
|
888
|
+
let next = [...events];
|
|
889
|
+
if (current.managed !== null) {
|
|
890
|
+
await runManagedControl({ runDir, runRef, operation: 'abandon', artifactDigest: null,
|
|
891
|
+
shutdownReason: reason, stdout, emit: false, requestId });
|
|
892
|
+
next = (await readRunStore(runDir)).events;
|
|
893
|
+
} else next.push(buildNextRunEvent({
|
|
894
|
+
events: next,
|
|
895
|
+
runId: meta.run_id,
|
|
896
|
+
kind: 'run_closed',
|
|
897
|
+
planEpoch: compileArtifact.plan.plan_epoch,
|
|
898
|
+
subject: { kind: 'runtime_plan', ref: compileArtifact.plan.plan_ref },
|
|
899
|
+
payload: { outcome: 'abandoned', reason, accepted: state.accepted },
|
|
900
|
+
recordedAt: canonicalNow(),
|
|
901
|
+
}));
|
|
902
|
+
if (current.managed === null) await replaceEventsAtomically(runDir, next);
|
|
903
|
+
const output = {
|
|
904
|
+
schema: 'lattice.run_abandon_result.v1',
|
|
905
|
+
outcome: 'abandoned',
|
|
906
|
+
reason,
|
|
907
|
+
run_id: meta.run_id,
|
|
908
|
+
event_count: next.length,
|
|
909
|
+
events_digest: digestArtifact(next.map(({ event_digest: digest }) => digest)),
|
|
910
|
+
};
|
|
911
|
+
output.result_digest = digestArtifact(output);
|
|
912
|
+
stdout.write(`${JSON.stringify(output)}\n`);
|
|
913
|
+
return 0;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function readManagedSession(runDir) {
|
|
918
|
+
const supervisorDir = path.join(runDir, 'supervisor');
|
|
919
|
+
const activePaths = await resolveActiveRuntimePaths({ runDir });
|
|
920
|
+
const descriptorPath = activePaths.descriptorPath;
|
|
921
|
+
const sessionPath = activePaths.sessionPath;
|
|
922
|
+
const descriptor = await readBoundedJson(descriptorPath, 'runtime supervisor descriptor');
|
|
923
|
+
const sessionInfo = await lstat(sessionPath).catch(() => null);
|
|
924
|
+
const descriptorKeys = ['schema', 'run_id', 'pid', 'process_start_identity', 'socket_ref',
|
|
925
|
+
'session_nonce_digest', 'protocol_version', 'activated_at', 'descriptor_digest'];
|
|
926
|
+
const identityKeys = ['schema', 'platform', 'pid', 'started_identity', 'identity_digest'];
|
|
927
|
+
const descriptorBody = { ...descriptor };
|
|
928
|
+
delete descriptorBody.descriptor_digest;
|
|
929
|
+
const identity = descriptor?.process_start_identity;
|
|
930
|
+
const identityBody = { ...identity };
|
|
931
|
+
delete identityBody.identity_digest;
|
|
932
|
+
if (descriptor?.schema !== 'lattice.runtime_supervisor_descriptor.v1'
|
|
933
|
+
|| Object.keys(descriptor).sort().join('\0') !== descriptorKeys.sort().join('\0')
|
|
934
|
+
|| typeof descriptor.run_id !== 'string'
|
|
935
|
+
|| !Number.isSafeInteger(descriptor.pid) || descriptor.pid < 1
|
|
936
|
+
|| identity?.schema !== 'lattice.process_start_identity.v1'
|
|
937
|
+
|| Object.keys(identity).sort().join('\0') !== identityKeys.sort().join('\0')
|
|
938
|
+
|| identity.pid !== descriptor.pid
|
|
939
|
+
|| identity.identity_digest !== digestArtifact(identityBody)
|
|
940
|
+
|| descriptor.descriptor_digest !== digestArtifact(descriptorBody)
|
|
941
|
+
|| descriptor.socket_ref !== 'supervisor/control.sock'
|
|
942
|
+
|| sessionInfo === null || !sessionInfo.isFile() || sessionInfo.isSymbolicLink()
|
|
943
|
+
|| (sessionInfo.mode & 0o077) !== 0) {
|
|
944
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'live supervisor bindingが不正');
|
|
945
|
+
}
|
|
946
|
+
const sessionNonce = (await readFile(sessionPath, 'utf8')).trim();
|
|
947
|
+
const nonceDigest = digestArtifact(sessionNonce);
|
|
948
|
+
if (sessionNonce.length < 32 || nonceDigest !== descriptor.session_nonce_digest) {
|
|
949
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'supervisor sessionがdescriptorと一致しない');
|
|
950
|
+
}
|
|
951
|
+
if (activePaths.pointer !== null
|
|
952
|
+
&& (activePaths.pointer.descriptor_digest !== descriptor.descriptor_digest
|
|
953
|
+
|| activePaths.pointer.session_nonce_digest !== nonceDigest)) {
|
|
954
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'active runtime pointer bindingが不正');
|
|
955
|
+
}
|
|
956
|
+
const socketPath = path.join(supervisorDir, 'control.sock');
|
|
957
|
+
const socketInfo = await lstat(socketPath).catch(() => null);
|
|
958
|
+
if (socketInfo === null || !socketInfo.isSocket() || socketInfo.isSymbolicLink()) {
|
|
959
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'supervisor control socketがliveではない');
|
|
960
|
+
}
|
|
961
|
+
return { descriptor, sessionNonce, socketPath };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function controlOperationPayload({ operation, runRef, artifact = null, artifactDigest,
|
|
965
|
+
checkpointDigest = null, expectedEpoch, expectedQueueDigest, shutdownReason = null }) {
|
|
966
|
+
const value = {
|
|
967
|
+
schema: 'lattice.runtime_control_operation.v1',
|
|
968
|
+
operation,
|
|
969
|
+
run_ref: runRef,
|
|
970
|
+
artifact,
|
|
971
|
+
artifact_digest: artifactDigest,
|
|
972
|
+
checkpoint_digest: checkpointDigest,
|
|
973
|
+
expected_epoch: expectedEpoch,
|
|
974
|
+
expected_queue_digest: expectedQueueDigest,
|
|
975
|
+
shutdown_reason: shutdownReason,
|
|
976
|
+
};
|
|
977
|
+
value.operation_digest = digestArtifact(value);
|
|
978
|
+
return value;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
async function isActiveSupervisorGateCommitted(runDir, events, expectedEpoch) {
|
|
982
|
+
if (await isManagedRunFrozen(runDir, events)) return false;
|
|
983
|
+
const active = await resolveActiveRuntimePaths({ runDir });
|
|
984
|
+
const sessionNonceDigest = active.pointer?.session_nonce_digest
|
|
985
|
+
?? digestArtifact((await readFile(active.sessionPath, 'utf8')).trim());
|
|
986
|
+
const gateDocument = await readBoundedJson(path.join(runDir, 'supervisor', 'write-gate.json'),
|
|
987
|
+
'supervisor write gate');
|
|
988
|
+
const committedGate = await createRuntimeGateStore({ runDir, runId: gateDocument.run_id,
|
|
989
|
+
sessionNonceDigest, controlEventsPath: active.controlEventsPath }).read();
|
|
990
|
+
if (committedGate === null || committedGate.gate.plan_epoch !== expectedEpoch) return false;
|
|
991
|
+
const gate = committedGate.gate;
|
|
992
|
+
const controlEvents = await readBoundedJson(active.controlEventsPath,
|
|
993
|
+
'runtime control events');
|
|
994
|
+
const resumed = controlEvents.findLast((event) => event.kind === 'intake_resumed'
|
|
995
|
+
&& event.payload?.plan_epoch === expectedEpoch
|
|
996
|
+
&& event.payload?.gate_digest === gate.gate_digest);
|
|
997
|
+
return resumed !== undefined && resumed.session_nonce_digest === sessionNonceDigest;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async function runManagedControl({ runDir, runRef, operation, artifact = null, artifactDigest, stdout,
|
|
1001
|
+
checkpointDigest = null, shutdownReason = null, emit = true, requestId = null }) {
|
|
1002
|
+
const committed = await readCommittedEpochStore(runDir);
|
|
1003
|
+
if (committed === null) throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
|
|
1004
|
+
const { descriptor, sessionNonce, socketPath } = await readManagedSession(runDir);
|
|
1005
|
+
if (descriptor.run_id !== committed.meta.run_id) {
|
|
1006
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'supervisor descriptorが別runに属する');
|
|
1007
|
+
}
|
|
1008
|
+
let expectedQueueDigest = null;
|
|
1009
|
+
try {
|
|
1010
|
+
const queue = await readBoundedJson(path.join(runDir, 'queued-events.json'), 'runtime queue');
|
|
1011
|
+
expectedQueueDigest = queue.queue_digest;
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (!(error instanceof CliContractError) || error.code !== 'INPUT_UNREADABLE') throw error;
|
|
1014
|
+
}
|
|
1015
|
+
let expectedEpoch = operation === 'activate' ? 1 : operation === 'recompile'
|
|
1016
|
+
&& Number.isSafeInteger(artifact?.predecessor_epoch)
|
|
1017
|
+
? artifact.predecessor_epoch : committed.pointer.plan_epoch;
|
|
1018
|
+
if (operation === 'recompile' && requestId !== null) {
|
|
1019
|
+
const transaction = await readBoundedJson(path.join(runDir,
|
|
1020
|
+
'epoch-activation-transaction.json'), 'epoch activation transaction').catch(() => null);
|
|
1021
|
+
if (transaction?.request_id === requestId
|
|
1022
|
+
&& transaction.recompile_request_digest === artifact?.request_digest) {
|
|
1023
|
+
expectedEpoch = transaction.successor_epoch - 1;
|
|
1024
|
+
expectedQueueDigest = transaction.queue_head_digest;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
if (operation === 'reprocess' && requestId !== null) {
|
|
1028
|
+
const pending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
|
|
1029
|
+
'pending recompile').catch(() => null);
|
|
1030
|
+
if (pending?.reprocess_request_id === requestId
|
|
1031
|
+
&& pending.pending_digest === selfDigest(pending, 'pending_digest')) {
|
|
1032
|
+
expectedEpoch = pending.predecessor_epoch;
|
|
1033
|
+
expectedQueueDigest = pending.reprocess_queue_digest;
|
|
1034
|
+
} else {
|
|
1035
|
+
const transaction = await readBoundedJson(path.join(runDir,
|
|
1036
|
+
'epoch-activation-transaction.json'), 'epoch activation transaction').catch(() => null);
|
|
1037
|
+
if (transaction?.schema === 'lattice.runtime_epoch_activation_transaction.v1'
|
|
1038
|
+
&& transaction.transaction_digest === selfDigest(transaction, 'transaction_digest')) {
|
|
1039
|
+
expectedEpoch = transaction.successor_epoch - 1;
|
|
1040
|
+
expectedQueueDigest = transaction.queue_head_digest;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
if (operation === 'activate') expectedQueueDigest = null;
|
|
1045
|
+
const payload = controlOperationPayload({
|
|
1046
|
+
operation, runRef, artifact, artifactDigest, checkpointDigest,
|
|
1047
|
+
expectedEpoch,
|
|
1048
|
+
expectedQueueDigest, shutdownReason,
|
|
1049
|
+
});
|
|
1050
|
+
const request = createRuntimeControlRequest({
|
|
1051
|
+
requestId: requestId ?? randomUUID(), runId: committed.meta.run_id, operation,
|
|
1052
|
+
payload, sessionNonce,
|
|
1053
|
+
});
|
|
1054
|
+
if (operation === 'reprocess' && requestId !== null
|
|
1055
|
+
&& committed.pointer.plan_epoch === expectedEpoch + 1) {
|
|
1056
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1057
|
+
if (await isActiveSupervisorGateCommitted(runDir, events, committed.pointer.plan_epoch)) {
|
|
1058
|
+
const activeRuntime = await resolveActiveRuntimePaths({ runDir });
|
|
1059
|
+
const journal = await readBoundedJson(activeRuntime.controlEventsPath,
|
|
1060
|
+
'runtime control events');
|
|
1061
|
+
const result = buildControlResult({ operation: 'reprocess', outcome: 'reprocessed',
|
|
1062
|
+
eventHeadDigest: events.at(-1)?.event_digest ?? null,
|
|
1063
|
+
controlHeadDigest: journal.at(-1)?.event_digest ?? null,
|
|
1064
|
+
activeEpoch: committed.pointer.plan_epoch, stagedEpoch: null });
|
|
1065
|
+
let response = buildControlResponse(request, 'completed', result,
|
|
1066
|
+
journal.at(-1)?.event_digest ?? null);
|
|
1067
|
+
const store = createRuntimeControlStore({ runDir, runId: committed.meta.run_id,
|
|
1068
|
+
clock: canonicalNow });
|
|
1069
|
+
const known = await store.readRequest(request);
|
|
1070
|
+
if (known?.state === 'completed' && known.response.outcome === 'completed') {
|
|
1071
|
+
response = known.response;
|
|
1072
|
+
} else if (known?.state === 'completed' && known.response.outcome === 'rejected') {
|
|
1073
|
+
response = await store.recoverCompletedRequest(request, response);
|
|
1074
|
+
} else {
|
|
1075
|
+
response = await store.completeRequest(request, response);
|
|
1076
|
+
}
|
|
1077
|
+
if (emit) stdout.write(`${JSON.stringify(response.result)}\n`);
|
|
1078
|
+
return emit ? 0 : response.result;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
let response;
|
|
1082
|
+
let ambiguous = false;
|
|
1083
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
1084
|
+
try {
|
|
1085
|
+
response = await sendRuntimeControlRequest({ socketPath, request,
|
|
1086
|
+
timeoutMs: ['recompile', 'reprocess'].includes(operation) ? 60_000 : 5_000 });
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (error?.code === 'RUN_OUTCOME_UNKNOWN') ambiguous = true;
|
|
1089
|
+
else if (!(ambiguous && error?.code === 'RUN_NOT_MANAGED')) throw error;
|
|
1090
|
+
if (operation === 'reprocess' && requestId !== null) {
|
|
1091
|
+
const latest = await readCommittedEpochStore(runDir);
|
|
1092
|
+
const latestEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1093
|
+
if (latest?.pointer.plan_epoch === expectedEpoch + 1
|
|
1094
|
+
&& await isActiveSupervisorGateCommitted(runDir, latestEvents,
|
|
1095
|
+
latest.pointer.plan_epoch)) {
|
|
1096
|
+
return runManagedControl({ runDir, runRef, operation, artifact, artifactDigest,
|
|
1097
|
+
stdout, checkpointDigest, shutdownReason, emit, requestId });
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
|
|
1101
|
+
`managed ${operation}の結果が確定しない。同一request_id=${request.request_id}`);
|
|
1102
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
if (response.outcome !== 'unknown') break;
|
|
1106
|
+
if (operation === 'reprocess' && requestId !== null) {
|
|
1107
|
+
const latest = await readCommittedEpochStore(runDir);
|
|
1108
|
+
const latestEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1109
|
+
if (latest?.pointer.plan_epoch === expectedEpoch + 1
|
|
1110
|
+
&& await isActiveSupervisorGateCommitted(runDir, latestEvents,
|
|
1111
|
+
latest.pointer.plan_epoch)) {
|
|
1112
|
+
return runManagedControl({ runDir, runRef, operation, artifact, artifactDigest,
|
|
1113
|
+
stdout, checkpointDigest, shutdownReason, emit, requestId });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
|
|
1117
|
+
`managed ${operation}の結果が確定しない。同一request_id=${request.request_id}`);
|
|
1118
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1119
|
+
}
|
|
1120
|
+
if (response.outcome !== 'completed') {
|
|
1121
|
+
const code = response.result?.unmet?.[0] ?? 'RUN_FROZEN';
|
|
1122
|
+
throw new CliContractError(code, `managed ${operation}が${response.outcome}で終了した: ${response.result?.unmet?.[1] ?? code}`);
|
|
1123
|
+
}
|
|
1124
|
+
const resultKeys = ['schema', 'operation', 'outcome', 'event_head_digest', 'control_head_digest',
|
|
1125
|
+
'active_epoch', 'staged_epoch', 'unmet', 'result_digest'];
|
|
1126
|
+
if (operation === 'finding_record') resultKeys.push('finding_digest');
|
|
1127
|
+
const resultBody = { ...response.result };
|
|
1128
|
+
delete resultBody.result_digest;
|
|
1129
|
+
if (response.result?.schema !== 'lattice.runtime_control_result.v1'
|
|
1130
|
+
|| Object.keys(response.result).sort().join('\0') !== resultKeys.sort().join('\0')
|
|
1131
|
+
|| response.result.operation !== operation
|
|
1132
|
+
|| response.result.result_digest !== digestArtifact(resultBody)) {
|
|
1133
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'managed control result bindingが不正');
|
|
1134
|
+
}
|
|
1135
|
+
if (emit) stdout.write(`${JSON.stringify(response.result)}\n`);
|
|
1136
|
+
return emit ? 0 : response.result;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function buildControlResult({ operation, outcome, eventHeadDigest, controlHeadDigest, activeEpoch, stagedEpoch = null, unmet = [] }) {
|
|
1140
|
+
const result = {
|
|
1141
|
+
schema: 'lattice.runtime_control_result.v1', operation, outcome,
|
|
1142
|
+
event_head_digest: eventHeadDigest, control_head_digest: controlHeadDigest,
|
|
1143
|
+
active_epoch: activeEpoch, staged_epoch: stagedEpoch, unmet,
|
|
1144
|
+
};
|
|
1145
|
+
result.result_digest = digestArtifact(result);
|
|
1146
|
+
return result;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function buildControlResponse(request, outcome, result, controlHeadDigest) {
|
|
1150
|
+
const response = {
|
|
1151
|
+
schema: 'lattice.runtime_control_response.v1', request_id: request.request_id,
|
|
1152
|
+
run_id: request.run_id, outcome, result, control_head_digest: controlHeadDigest,
|
|
1153
|
+
};
|
|
1154
|
+
response.response_digest = digestArtifact(response);
|
|
1155
|
+
return response;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function validateRuntimeQueue(queue, runId, frozenEpoch) {
|
|
1159
|
+
return queue !== null && typeof queue === 'object' && !Array.isArray(queue)
|
|
1160
|
+
&& Object.keys(queue).sort().join('\0') === ['schema', 'run_id', 'frozen_epoch', 'entries', 'queue_digest'].sort().join('\0')
|
|
1161
|
+
&& queue.schema === 'lattice.runtime_queue.v1' && queue.run_id === runId
|
|
1162
|
+
&& queue.frozen_epoch === frozenEpoch && Array.isArray(queue.entries)
|
|
1163
|
+
&& queue.entries.every((entry, index) => entry !== null && typeof entry === 'object'
|
|
1164
|
+
&& !Array.isArray(entry)
|
|
1165
|
+
&& Object.keys(entry).sort().join('\0') === ['sequence', 'kind', 'subject_digest', 'artifact_digest'].sort().join('\0')
|
|
1166
|
+
&& Number.isSafeInteger(entry.sequence) && entry.sequence === index + 1
|
|
1167
|
+
&& typeof entry.kind === 'string' && entry.kind.length > 0
|
|
1168
|
+
&& /^[0-9a-f]{64}$/u.test(entry.subject_digest)
|
|
1169
|
+
&& /^[0-9a-f]{64}$/u.test(entry.artifact_digest))
|
|
1170
|
+
&& queue.queue_digest === selfDigest(queue, 'queue_digest');
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function controlIntentDigest(request) {
|
|
1174
|
+
return selfDigest({ request_id: request.request_id, run_id: request.run_id,
|
|
1175
|
+
operation: request.operation, payload: request.payload, intent_digest: '' }, 'intent_digest');
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
export function reconstructHoldResultFromJournal({ journal, runId, requestId, intentDigest }) {
|
|
1179
|
+
if (!Array.isArray(journal)) return null;
|
|
1180
|
+
const preparedIndex = journal.findIndex((event) => event.kind === 'hold_prepared'
|
|
1181
|
+
&& event.payload?.request_id === requestId
|
|
1182
|
+
&& event.payload?.logical_intent_digest === intentDigest);
|
|
1183
|
+
const prepared = journal[preparedIndex];
|
|
1184
|
+
const barrierIndex = prepared === undefined ? -1 : journal.findIndex((event, index) => (
|
|
1185
|
+
index > preparedIndex && event.kind === 'barrier_requested'
|
|
1186
|
+
&& event.payload?.barrier_id === prepared.payload.barrier_id));
|
|
1187
|
+
const barrier = journal[barrierIndex];
|
|
1188
|
+
if (prepared === undefined || barrier === undefined) return null;
|
|
1189
|
+
const acknowledgements = journal.slice(barrierIndex + 1)
|
|
1190
|
+
.filter((event) => event.kind === 'executor_quiesced'
|
|
1191
|
+
&& event.payload?.barrier_id === prepared.payload.barrier_id
|
|
1192
|
+
&& event.payload?.barrier_control_digest === barrier.event_digest);
|
|
1193
|
+
const acknowledgedTodos = acknowledgements.map((event) => event.payload.todo_id).sort();
|
|
1194
|
+
if (acknowledgements.length !== barrier.payload.running_count
|
|
1195
|
+
|| acknowledgements.some((event) => typeof event.payload?.ack?.ack_digest !== 'string')
|
|
1196
|
+
|| new Set(acknowledgedTodos).size !== acknowledgedTodos.length
|
|
1197
|
+
|| JSON.stringify(acknowledgedTodos) !== JSON.stringify(barrier.payload.running_todo_ids)) return null;
|
|
1198
|
+
const result = {
|
|
1199
|
+
schema: 'lattice.runtime_hold_result.v1', run_id: runId,
|
|
1200
|
+
finding_digest: prepared.payload.finding_digest, barrier_id: prepared.payload.barrier_id,
|
|
1201
|
+
quiescence_ack_digests: acknowledgements.map((event) => event.payload.ack.ack_digest).sort(),
|
|
1202
|
+
outcome: 'held', recorded_at: prepared.payload.recorded_at, result_digest: '',
|
|
1203
|
+
};
|
|
1204
|
+
result.result_digest = selfDigest(result, 'result_digest');
|
|
1205
|
+
return result;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
async function runActivate({ runDir, runRef, repoRoot, stdout, requestId = null }) {
|
|
1209
|
+
return withLifecycleLock(runDir, async () => {
|
|
1210
|
+
const { events, meta, managed } = await readRunStore(runDir);
|
|
1211
|
+
if (!['lattice.run_meta.v1', 'lattice.run_meta.v2'].includes(meta.schema)) throw new CliContractError('RUN_NOT_MANAGED', 'run metaがactivate対象でない');
|
|
1212
|
+
if (projectRuntimeState({ events }).closed) throw new CliContractError('RUN_CLOSED', 'closed runは再activateできない');
|
|
1213
|
+
if (managed !== null && requestId !== null) {
|
|
1214
|
+
try {
|
|
1215
|
+
return await runManagedControl({ runDir, runRef, operation: 'activate', artifactDigest: null,
|
|
1216
|
+
stdout, requestId });
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
if (error?.code !== 'RUN_NOT_MANAGED') throw error;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (managed !== null) await prepareManagedSupervisorRestart({ runDir });
|
|
1222
|
+
const launched = await launchDurableSupervisor({ runDir });
|
|
1223
|
+
const payload = controlOperationPayload({
|
|
1224
|
+
operation: 'activate', runRef, artifactDigest: null, expectedEpoch: 1, expectedQueueDigest: null,
|
|
1225
|
+
});
|
|
1226
|
+
const request = createRuntimeControlRequest({ requestId: requestId ?? randomUUID(), runId: meta.run_id,
|
|
1227
|
+
operation: 'activate', payload, sessionNonce: launched.sessionNonce });
|
|
1228
|
+
let response;
|
|
1229
|
+
let ambiguous = false;
|
|
1230
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
1231
|
+
try {
|
|
1232
|
+
response = await sendRuntimeActivationRequest({
|
|
1233
|
+
socketPath: launched.socketPath, request, expectedPid: launched.pid,
|
|
1234
|
+
expectedProcessStartIdentity: launched.processStartIdentity,
|
|
1235
|
+
});
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (error?.code === 'RUN_OUTCOME_UNKNOWN') ambiguous = true;
|
|
1238
|
+
else if (!(ambiguous && error?.code === 'RUN_NOT_MANAGED')) throw error;
|
|
1239
|
+
if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
|
|
1240
|
+
`managed activateの結果が確定しない。同一request_id=${request.request_id}`);
|
|
1241
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (response.outcome !== 'unknown') break;
|
|
1245
|
+
if (attempt === 99) throw new CliContractError('RUN_OUTCOME_UNKNOWN',
|
|
1246
|
+
`managed activateの結果が確定しない。同一request_id=${request.request_id}`);
|
|
1247
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1248
|
+
}
|
|
1249
|
+
if (response.outcome !== 'completed') {
|
|
1250
|
+
const code = response.result?.unmet?.[0] ?? 'ADAPTER_CONTROLLER_UNAVAILABLE';
|
|
1251
|
+
const deadline = Date.now() + 2_000;
|
|
1252
|
+
while (Date.now() < deadline) {
|
|
1253
|
+
try { await lstat(path.join(runDir, 'supervisor')); } catch (error) {
|
|
1254
|
+
if (error?.code === 'ENOENT') break;
|
|
1255
|
+
}
|
|
1256
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1257
|
+
}
|
|
1258
|
+
throw new CliContractError(code, `managed activateが${response.outcome}で終了した: ${response.result?.unmet?.[1] ?? code}`);
|
|
1259
|
+
}
|
|
1260
|
+
// daemonがpointerまでcommitしたことを再読してから成功を返す。
|
|
1261
|
+
const committed = await readCommittedEpochStore(runDir);
|
|
1262
|
+
if (committed === null
|
|
1263
|
+
|| !events.some((event) => event.event_digest === committed.pointer.activation_run_event_digest)) {
|
|
1264
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'activate commitを再検証できない');
|
|
1265
|
+
}
|
|
1266
|
+
stdout.write(`${JSON.stringify(response.result)}\n`);
|
|
1267
|
+
return 0;
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
export async function runManagedSupervisorDaemon({
|
|
1272
|
+
runDir, sessionNonce, serveRuntimeControlSocket, activateController, onReady,
|
|
1273
|
+
registerDaemonCleanup = () => {}, crashInjector = null,
|
|
1274
|
+
validatePhaseRevision = null, commitPhaseRevision = null,
|
|
1275
|
+
}) {
|
|
1276
|
+
const repoRoot = await realpath(path.resolve(runDir, '..', '..', '..'));
|
|
1277
|
+
const request = await readBoundedJson(path.join(runDir, 'request.json'), 'run request');
|
|
1278
|
+
const compileArtifact = await readBoundedJson(path.join(runDir, 'plan-compile-result.json'), 'plan compile result');
|
|
1279
|
+
const legacyMeta = await readBoundedJson(path.join(runDir, 'run-meta.json'), 'run meta');
|
|
1280
|
+
const supervisorDir = path.join(runDir, 'supervisor');
|
|
1281
|
+
await mkdir(supervisorDir, { recursive: true, mode: 0o700 });
|
|
1282
|
+
const socketPath = path.join(supervisorDir, 'control.sock');
|
|
1283
|
+
let managedSupervisor = null;
|
|
1284
|
+
let activation = null;
|
|
1285
|
+
let additionalActivations = [];
|
|
1286
|
+
let activationCommitted = false;
|
|
1287
|
+
let controlEvents = [];
|
|
1288
|
+
const restarting = legacyMeta.schema === 'lattice.run_meta.v2';
|
|
1289
|
+
let candidateDir = null;
|
|
1290
|
+
let candidateControllerDir = null;
|
|
1291
|
+
let recoveryRegistrationDigest = null;
|
|
1292
|
+
const requestStore = createRuntimeControlStore({ runDir, runId: request.request_id,
|
|
1293
|
+
clock: canonicalNow });
|
|
1294
|
+
let eventStore = requestStore;
|
|
1295
|
+
let server;
|
|
1296
|
+
|
|
1297
|
+
const appendControl = async ({ run_id: runId, kind, session_nonce_digest: sessionDigest, payload }) => {
|
|
1298
|
+
const eventDigest = await eventStore.append({ run_id: runId, kind,
|
|
1299
|
+
session_nonce_digest: sessionDigest, payload });
|
|
1300
|
+
controlEvents = await eventStore.readEvents();
|
|
1301
|
+
return eventDigest;
|
|
1302
|
+
};
|
|
1303
|
+
let gateWriter = null;
|
|
1304
|
+
const resolveObservationBinding = async ({ binding }) => {
|
|
1305
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1306
|
+
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1307
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === binding?.todo_id
|
|
1308
|
+
&& event.payload?.executor_handle === binding?.executor_handle);
|
|
1309
|
+
const observation = dispatch?.payload?.direct_os_observation_binding;
|
|
1310
|
+
if (observation === null || typeof observation !== 'object' || Array.isArray(observation)) {
|
|
1311
|
+
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable Direct OS binding不足: ${binding?.todo_id ?? 'unknown'}`);
|
|
1312
|
+
}
|
|
1313
|
+
return structuredClone(observation);
|
|
1314
|
+
};
|
|
1315
|
+
const resolveRunningBindings = async ({ runId }) => {
|
|
1316
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1317
|
+
const state = projectRuntimeState({ events });
|
|
1318
|
+
return state.running.map((todoId) => {
|
|
1319
|
+
const dispatch = events.findLast((event) => event.kind === 'executor_dispatched'
|
|
1320
|
+
&& event.subject?.kind === 'todo' && event.subject.ref === todoId);
|
|
1321
|
+
const payload = dispatch?.payload ?? {};
|
|
1322
|
+
const binding = {
|
|
1323
|
+
todo_id: todoId, executor_handle: payload.executor_handle,
|
|
1324
|
+
worktree_id: payload.worktree_id, plan_epoch: dispatch?.plan_epoch,
|
|
1325
|
+
packet_digest: payload.packet_digest, write_lease_id: payload.write_lease_id,
|
|
1326
|
+
controller_registration_digest: recoveryRegistrationDigest
|
|
1327
|
+
?? payload.controller_registration_digest,
|
|
1328
|
+
};
|
|
1329
|
+
if (Object.values(binding).some((value) => value === undefined)) {
|
|
1330
|
+
throw new ManagedRuntimeError('HOLD_ACKS_INCOMPLETE', `durable running binding不足: ${todoId}`);
|
|
1331
|
+
}
|
|
1332
|
+
return binding;
|
|
1333
|
+
});
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
const executeControl = async (controlRequest) => {
|
|
1337
|
+
if (controlRequest.run_id !== request.request_id || controlRequest.session_nonce !== sessionNonce) {
|
|
1338
|
+
throw new ManagedRuntimeError('RUN_NOT_MANAGED', 'daemon session binding不一致');
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
if (controlRequest.operation === 'activate') {
|
|
1342
|
+
if (activation !== null) throw new ManagedRuntimeError('RUN_BUSY', '既にactivate済み');
|
|
1343
|
+
if (restarting) {
|
|
1344
|
+
candidateDir = path.join(supervisorDir, 'restart-candidates', digestArtifact(sessionNonce));
|
|
1345
|
+
await mkdir(candidateDir, { recursive: true, mode: 0o700 });
|
|
1346
|
+
const priorActive = await resolveActiveRuntimePaths({ runDir });
|
|
1347
|
+
const priorControl = await readFile(priorActive.controlEventsPath).catch((error) => {
|
|
1348
|
+
if (error?.code === 'ENOENT') return Buffer.from('[]\n');
|
|
1349
|
+
throw error;
|
|
1350
|
+
});
|
|
1351
|
+
await durableReplaceBytes(candidateDir, 'control-events.json', priorControl);
|
|
1352
|
+
eventStore = createRuntimeControlStore({ runDir: candidateDir,
|
|
1353
|
+
runId: request.request_id, clock: canonicalNow });
|
|
1354
|
+
}
|
|
1355
|
+
activation = await activateController({ repoRoot, runId: request.request_id,
|
|
1356
|
+
adapterKind: legacyMeta.executor_adapter });
|
|
1357
|
+
if (process.env.NODE_ENV === 'test'
|
|
1358
|
+
&& process.env.LATTICE_INTERNAL_TEST_CONTROLLER_COUNT === '2') {
|
|
1359
|
+
additionalActivations = [await activateController({ repoRoot, runId: request.request_id,
|
|
1360
|
+
adapterKind: legacyMeta.executor_adapter })];
|
|
1361
|
+
}
|
|
1362
|
+
if (restarting) recoveryRegistrationDigest = activation.registration.registration_digest;
|
|
1363
|
+
const activationControlDigest = await appendControl({ run_id: request.request_id,
|
|
1364
|
+
kind: 'supervisor_activated', session_nonce_digest: digestArtifact(sessionNonce),
|
|
1365
|
+
payload: activation.activationControlEvent.payload });
|
|
1366
|
+
if (restarting) {
|
|
1367
|
+
const recoveryEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1368
|
+
const runningTodos = projectRuntimeState({ events: recoveryEvents }).running;
|
|
1369
|
+
const oldRegistrations = [...new Set(runningTodos.map((todoId) => recoveryEvents.findLast((event) => (
|
|
1370
|
+
event.kind === 'executor_dispatched' && event.subject?.ref === todoId
|
|
1371
|
+
))?.payload?.controller_registration_digest).filter(Boolean))].sort();
|
|
1372
|
+
await appendControl({ run_id: request.request_id, kind: 'controller_recovery_rebound',
|
|
1373
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
1374
|
+
old_registration_digests: oldRegistrations,
|
|
1375
|
+
new_registration_digest: activation.registration.registration_digest,
|
|
1376
|
+
running_todo_ids: [...runningTodos].sort(),
|
|
1377
|
+
} });
|
|
1378
|
+
}
|
|
1379
|
+
const controllerDir = path.join(runDir, 'controllers', activation.controllerDescriptor.controller_id);
|
|
1380
|
+
candidateControllerDir = controllerDir;
|
|
1381
|
+
await mkdir(controllerDir, { recursive: true, mode: 0o700 });
|
|
1382
|
+
await writeCanonicalJsonFile(path.join(controllerDir, 'descriptor.json'), activation.controllerDescriptor);
|
|
1383
|
+
await writeCanonicalJsonFile(path.join(controllerDir, 'registration.json'), activation.registration);
|
|
1384
|
+
for (const extra of additionalActivations) {
|
|
1385
|
+
const extraDir = path.join(runDir, 'controllers', extra.controllerDescriptor.controller_id);
|
|
1386
|
+
await mkdir(extraDir, { recursive: true, mode: 0o700 });
|
|
1387
|
+
await writeCanonicalJsonFile(path.join(extraDir, 'descriptor.json'), extra.controllerDescriptor);
|
|
1388
|
+
await writeCanonicalJsonFile(path.join(extraDir, 'registration.json'), extra.registration);
|
|
1389
|
+
}
|
|
1390
|
+
if (restarting) {
|
|
1391
|
+
await replaceCanonicalJsonAtomically(candidateDir, 'descriptor.json', activation.supervisorDescriptor);
|
|
1392
|
+
await durableReplaceBytes(candidateDir, 'session', Buffer.from(`${sessionNonce}\n`));
|
|
1393
|
+
} else {
|
|
1394
|
+
await writeCanonicalJsonFile(path.join(supervisorDir, 'descriptor.json'), activation.supervisorDescriptor);
|
|
1395
|
+
await durableReplaceBytes(supervisorDir, 'session', Buffer.from(`${sessionNonce}\n`));
|
|
1396
|
+
}
|
|
1397
|
+
gateWriter = createRuntimeGateStore({ runDir, runId: request.request_id,
|
|
1398
|
+
sessionNonceDigest: digestArtifact(sessionNonce),
|
|
1399
|
+
controlEventsPath: restarting ? path.join(candidateDir, 'control-events.json')
|
|
1400
|
+
: path.join(runDir, 'control-events.json') });
|
|
1401
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1402
|
+
const committed = restarting
|
|
1403
|
+
? await readCommittedEpochStore(runDir)
|
|
1404
|
+
: await activateEpochOneStore({ runDir, request, compileArtifact, legacyMeta,
|
|
1405
|
+
activationRunEventDigest: events.at(-1).event_digest,
|
|
1406
|
+
activationControlEventDigest: activationControlDigest });
|
|
1407
|
+
managedSupervisor = await activation.createManagedSupervisor({
|
|
1408
|
+
resolveObservationBinding, resolveRunningBindings,
|
|
1409
|
+
journal: { append: appendControl }, gateWriter,
|
|
1410
|
+
});
|
|
1411
|
+
for (const extra of additionalActivations) {
|
|
1412
|
+
await extra.registerWithManagedSupervisor(managedSupervisor);
|
|
1413
|
+
}
|
|
1414
|
+
if (restarting) {
|
|
1415
|
+
await managedSupervisor.recoveryBarrier({ barrierId: `recovery-${randomUUID()}`,
|
|
1416
|
+
frozenEventDigest: events.at(-1).event_digest });
|
|
1417
|
+
// recovery barrierのdurable receipt後、単一pointer renameをcommit pointにする。
|
|
1418
|
+
const candidateEvents = await eventStore.readEvents();
|
|
1419
|
+
const restartResult = buildControlResult({ operation: 'activate', outcome: 'activated',
|
|
1420
|
+
eventHeadDigest: events.at(-1).event_digest,
|
|
1421
|
+
controlHeadDigest: candidateEvents.at(-1).event_digest,
|
|
1422
|
+
activeEpoch: committed.pointer.plan_epoch });
|
|
1423
|
+
const restartResponse = buildControlResponse(controlRequest, 'completed', restartResult,
|
|
1424
|
+
candidateEvents.at(-1).event_digest);
|
|
1425
|
+
await replaceCanonicalJsonAtomically(candidateDir, 'activation-response.json', restartResponse);
|
|
1426
|
+
const activePointer = {
|
|
1427
|
+
schema: 'lattice.runtime_active_pointer.v1', run_id: request.request_id,
|
|
1428
|
+
candidate_ref: path.relative(runDir, candidateDir),
|
|
1429
|
+
activation_request_id: controlRequest.request_id,
|
|
1430
|
+
activation_request_digest: controlRequest.request_digest,
|
|
1431
|
+
activation_intent_digest: controlIntentDigest(controlRequest),
|
|
1432
|
+
activation_response_digest: restartResponse.response_digest,
|
|
1433
|
+
descriptor_digest: activation.supervisorDescriptor.descriptor_digest,
|
|
1434
|
+
session_nonce_digest: digestArtifact(sessionNonce),
|
|
1435
|
+
control_head_digest: candidateEvents.at(-1).event_digest,
|
|
1436
|
+
control_head_sequence: candidateEvents.at(-1).sequence,
|
|
1437
|
+
committed_at: canonicalNow(), pointer_digest: '',
|
|
1438
|
+
};
|
|
1439
|
+
activePointer.pointer_digest = selfDigest(activePointer, 'pointer_digest');
|
|
1440
|
+
await replaceCanonicalJsonAtomically(supervisorDir, 'active-runtime.json', activePointer);
|
|
1441
|
+
activationCommitted = true;
|
|
1442
|
+
if (typeof crashInjector === 'function') await crashInjector(
|
|
1443
|
+
'after_active_runtime_pointer_commit', { request_id: controlRequest.request_id,
|
|
1444
|
+
pointer_digest: activePointer.pointer_digest });
|
|
1445
|
+
return restartResponse;
|
|
1446
|
+
}
|
|
1447
|
+
if (!restarting) activationCommitted = true;
|
|
1448
|
+
const result = buildControlResult({ operation: 'activate', outcome: 'activated',
|
|
1449
|
+
eventHeadDigest: events.at(-1).event_digest, controlHeadDigest: controlEvents.at(-1).event_digest,
|
|
1450
|
+
activeEpoch: committed.pointer.plan_epoch });
|
|
1451
|
+
return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
|
|
1452
|
+
}
|
|
1453
|
+
if (managedSupervisor === null) throw new ManagedRuntimeError('RUN_NOT_MANAGED', 'activate未完了');
|
|
1454
|
+
if (controlRequest.operation === 'finding_record') {
|
|
1455
|
+
const candidate = controlRequest.payload.artifact;
|
|
1456
|
+
if (!validateRuntimeFindingCandidate(candidate)
|
|
1457
|
+
|| digestArtifact(candidate) !== controlRequest.payload.artifact_digest) {
|
|
1458
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'finding candidate/digest binding不正');
|
|
1459
|
+
}
|
|
1460
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1461
|
+
const active = await readCommittedEpochStore(runDir);
|
|
1462
|
+
const checkpointDigest = controlRequest.payload.checkpoint_digest;
|
|
1463
|
+
const observed = events.findLast((event) => event.plan_epoch === controlRequest.payload.expected_epoch
|
|
1464
|
+
&& event.payload?.checkpoint_digest === checkpointDigest);
|
|
1465
|
+
if (observed === undefined) {
|
|
1466
|
+
if (events.some((event) => event.payload?.checkpoint_digest === checkpointDigest)) {
|
|
1467
|
+
throw new ManagedRuntimeError('STALE_FINDING', 'checkpointは旧epochに属する');
|
|
1468
|
+
}
|
|
1469
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED', '保存checkpointをactive event prefixから解決できない');
|
|
1470
|
+
}
|
|
1471
|
+
const activeRequestV1 = active.bundle.request.schema === 'lattice.run_request.v1'
|
|
1472
|
+
? structuredClone(active.bundle.request) : {
|
|
1473
|
+
schema: 'lattice.run_request.v1', request_id: active.bundle.request.request_id,
|
|
1474
|
+
repo: active.bundle.request.repo, capacity: active.bundle.request.capacity,
|
|
1475
|
+
todos: active.bundle.request.todos, manual_witness: active.bundle.request.manual_witness,
|
|
1476
|
+
sensor_query_set: active.bundle.request.sensor_query_set,
|
|
1477
|
+
executor_capability: active.bundle.request.executor_capability,
|
|
1478
|
+
claim_mode: active.bundle.request.claim_mode, request_digest: '',
|
|
1479
|
+
};
|
|
1480
|
+
activeRequestV1.request_digest = selfDigest(activeRequestV1, 'request_digest');
|
|
1481
|
+
const fresh = await compileFromRepo({ request: activeRequestV1, cwd: repoRoot,
|
|
1482
|
+
planRef: active.bundle.plan.plan_ref, planEpoch: active.pointer.plan_epoch,
|
|
1483
|
+
predecessorRefs: active.bundle.plan.predecessor_refs });
|
|
1484
|
+
if (fresh.outcome !== 'dispatchable'
|
|
1485
|
+
|| canonicalizeArtifact(fresh.manifests) !== canonicalizeArtifact(active.bundle.manifests)
|
|
1486
|
+
|| canonicalizeArtifact(fresh.plan.nodes.map(({ todo_id: id }) => id))
|
|
1487
|
+
!== canonicalizeArtifact(active.bundle.plan.nodes.map(({ todo_id: id }) => id))) {
|
|
1488
|
+
throw new ManagedRuntimeError('STALE_FINDING', 'current compile/sensorがactive epochから変化した');
|
|
1489
|
+
}
|
|
1490
|
+
const checkpointEntries = Array.isArray(observed.payload?.diff?.entries)
|
|
1491
|
+
? observed.payload.diff.entries : [];
|
|
1492
|
+
const durableEvidence = new Set([checkpointDigest, observed.event_digest]);
|
|
1493
|
+
for (const entry of checkpointEntries) {
|
|
1494
|
+
if (/^[0-9a-f]{64}$/u.test(entry?.content_digest ?? '')) durableEvidence.add(entry.content_digest);
|
|
1495
|
+
}
|
|
1496
|
+
for (const manifest of Object.values(fresh.manifests)) {
|
|
1497
|
+
for (const graph of manifest.graph_evidence) {
|
|
1498
|
+
if (/^[0-9a-f]{64}$/u.test(graph?.result_digest ?? '')) durableEvidence.add(graph.result_digest);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
let derivedTodoIds;
|
|
1502
|
+
let derivedKind;
|
|
1503
|
+
const observationSet = events.filter((event) => event.plan_epoch === active.pointer.plan_epoch
|
|
1504
|
+
&& event.kind === 'checkpoint_observed' && Array.isArray(event.payload?.diff?.entries)
|
|
1505
|
+
&& event.subject?.kind === 'todo').map((event) => ({ todo_id: event.subject.ref,
|
|
1506
|
+
paths: event.payload.diff.entries.map((entry) => entry.path) }));
|
|
1507
|
+
const independentlyClassified = classifyObservedDiff({ plan: active.bundle.plan,
|
|
1508
|
+
manifests: fresh.manifests, observations: observationSet }).findings;
|
|
1509
|
+
if (candidate.path !== null) {
|
|
1510
|
+
const producer = detectCheckpointFindings({ todoId: observed.subject.ref,
|
|
1511
|
+
checkpoint: observed.payload, packets: active.bundle.executor_packets,
|
|
1512
|
+
runningTodoIds: projectRuntimeState({ events }).running }).findings;
|
|
1513
|
+
const match = producer.find((findingValue) => findingValue.kind === candidate.proposed_kind
|
|
1514
|
+
&& findingValue.path === candidate.path
|
|
1515
|
+
&& canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
|
|
1516
|
+
const verified = independentlyClassified.find((findingValue) => findingValue.kind === candidate.proposed_kind
|
|
1517
|
+
&& findingValue.path === candidate.path
|
|
1518
|
+
&& canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
|
|
1519
|
+
if (match === undefined || verified === undefined) throw new ManagedRuntimeError(
|
|
1520
|
+
'FINDING_UNRESOLVED', 'path findingのproducer/verifier再導出が一致しない');
|
|
1521
|
+
derivedTodoIds = [...match.todo_ids];
|
|
1522
|
+
derivedKind = match.kind;
|
|
1523
|
+
} else {
|
|
1524
|
+
const match = independentlyClassified.find((findingValue) => findingValue.kind === candidate.proposed_kind
|
|
1525
|
+
&& findingValue.resource_id === candidate.resource_id
|
|
1526
|
+
&& canonicalizeArtifact(findingValue.todo_ids) === canonicalizeArtifact(candidate.todo_ids));
|
|
1527
|
+
if (match === undefined) throw new ManagedRuntimeError('FINDING_UNRESOLVED',
|
|
1528
|
+
'resource findingをindependent checkpoint classifierから再導出できない');
|
|
1529
|
+
derivedTodoIds = [...match.todo_ids];
|
|
1530
|
+
derivedKind = match.kind;
|
|
1531
|
+
}
|
|
1532
|
+
if (derivedTodoIds.length === 0
|
|
1533
|
+
|| derivedKind !== candidate.proposed_kind
|
|
1534
|
+
|| canonicalizeArtifact(derivedTodoIds) !== canonicalizeArtifact(candidate.todo_ids)) {
|
|
1535
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'candidate todo集合をcheckpoint/current manifestsから再導出できない');
|
|
1536
|
+
}
|
|
1537
|
+
if (!candidate.evidence_digests.every((digest) => durableEvidence.has(digest))) {
|
|
1538
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'candidate evidenceをdurable checkpoint/sensor artifactへ解決できない');
|
|
1539
|
+
}
|
|
1540
|
+
const observer = { schema: 'lattice.runtime_observer_identity.v1', kind: 'supervisor',
|
|
1541
|
+
controller_registration_digest: activation.registration.registration_digest,
|
|
1542
|
+
executor_handle: null, identity_digest: '' };
|
|
1543
|
+
observer.identity_digest = selfDigest(observer, 'identity_digest');
|
|
1544
|
+
const finding = await recordRuntimeFinding({ runDir, candidate, checkpointDigest,
|
|
1545
|
+
observedEventDigest: observed.event_digest, recordedBy: observer,
|
|
1546
|
+
// producerとは別のcallbackでも同一active bundle/checkpoint bindingを再検査する。
|
|
1547
|
+
deriveFinding: (_untrusted, evidence) => {
|
|
1548
|
+
return { schema: 'lattice.runtime_conflict_finding.v1',
|
|
1549
|
+
kind: derivedKind, todo_ids: derivedTodoIds,
|
|
1550
|
+
path: candidate.path, resource_id: candidate.resource_id,
|
|
1551
|
+
evidence_digests: [...new Set([checkpointDigest,
|
|
1552
|
+
...candidate.evidence_digests])].sort(), finding_digest: '' };
|
|
1553
|
+
},
|
|
1554
|
+
verifyFinding: (derived, evidence) => derived.kind === candidate.proposed_kind
|
|
1555
|
+
&& derived.finding_digest === selfDigest(derived, 'finding_digest')
|
|
1556
|
+
&& evidence.checkpoint.event_digest === observed.event_digest
|
|
1557
|
+
&& canonicalizeArtifact(evidence.bundle.manifests)
|
|
1558
|
+
=== canonicalizeArtifact(fresh.manifests)
|
|
1559
|
+
&& derived.todo_ids.every((id) => Object.hasOwn(fresh.manifests, id)) });
|
|
1560
|
+
if (projectRuntimeState({ events }).freeze !== null) {
|
|
1561
|
+
let queue = await readBoundedJson(path.join(runDir, 'queued-events.json'),
|
|
1562
|
+
'runtime queue').catch((error) => {
|
|
1563
|
+
if (error instanceof CliContractError && error.code === 'INPUT_UNREADABLE') return null;
|
|
1564
|
+
throw error;
|
|
1565
|
+
});
|
|
1566
|
+
if (queue === null) queue = { schema: 'lattice.runtime_queue.v1',
|
|
1567
|
+
run_id: request.request_id, frozen_epoch: controlRequest.payload.expected_epoch,
|
|
1568
|
+
entries: [], queue_digest: '' };
|
|
1569
|
+
else if (!validateRuntimeQueue(queue, request.request_id,
|
|
1570
|
+
controlRequest.payload.expected_epoch)) {
|
|
1571
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'finding queue binding不正');
|
|
1572
|
+
}
|
|
1573
|
+
queue.entries.push({ sequence: queue.entries.length + 1, kind: 'finding',
|
|
1574
|
+
subject_digest: candidate.candidate_digest,
|
|
1575
|
+
artifact_digest: finding.finding_digest });
|
|
1576
|
+
queue.queue_digest = selfDigest(queue, 'queue_digest');
|
|
1577
|
+
await replaceCanonicalJsonAtomically(runDir, 'queued-events.json', queue);
|
|
1578
|
+
}
|
|
1579
|
+
const result = buildControlResult({ operation: 'finding_record', outcome: 'recorded',
|
|
1580
|
+
eventHeadDigest: events.at(-1).event_digest,
|
|
1581
|
+
controlHeadDigest: controlEvents.at(-1).event_digest,
|
|
1582
|
+
activeEpoch: controlRequest.payload.expected_epoch,
|
|
1583
|
+
stagedEpoch: null });
|
|
1584
|
+
result.finding_digest = finding.finding_digest;
|
|
1585
|
+
const unsigned = { ...result }; delete unsigned.result_digest;
|
|
1586
|
+
result.result_digest = digestArtifact(unsigned);
|
|
1587
|
+
return buildControlResponse(controlRequest, 'completed', result,
|
|
1588
|
+
controlEvents.at(-1).event_digest);
|
|
1589
|
+
}
|
|
1590
|
+
if (controlRequest.operation === 'conflict') {
|
|
1591
|
+
const findingDigest = controlRequest.payload.artifact_digest;
|
|
1592
|
+
const finding = await readBoundedJson(path.join(runDir, 'findings', `${findingDigest}.json`), 'runtime finding');
|
|
1593
|
+
const active = await readCommittedEpochStore(runDir);
|
|
1594
|
+
if (!validateRuntimeFindingRecord(finding)
|
|
1595
|
+
|| finding.finding_digest !== findingDigest
|
|
1596
|
+
|| finding.run_id !== request.request_id) {
|
|
1597
|
+
throw new ManagedRuntimeError('FINDING_UNRESOLVED', 'finding binding不正');
|
|
1598
|
+
}
|
|
1599
|
+
if (finding.plan_epoch !== active.pointer.plan_epoch) {
|
|
1600
|
+
throw new ManagedRuntimeError('STALE_FINDING', 'findingはactive epochに属さない');
|
|
1601
|
+
}
|
|
1602
|
+
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1603
|
+
const todoId = finding.finding?.todo_ids?.[0] ?? active.bundle.plan.nodes[0].todo_id;
|
|
1604
|
+
events.push(buildNextRunEvent({ events, runId: request.request_id, kind: 'conflict_found', planEpoch: active.pointer.plan_epoch,
|
|
1605
|
+
subject: { kind: 'todo', ref: todoId }, payload: {
|
|
1606
|
+
...finding.finding, finding_digest: findingDigest, reported_by: 'lattice-supervisor',
|
|
1607
|
+
}, recordedAt: canonicalNow() }));
|
|
1608
|
+
events.push(buildNextRunEvent({ events, runId: request.request_id, kind: 'intake_frozen', planEpoch: active.pointer.plan_epoch,
|
|
1609
|
+
subject: { kind: 'runtime_plan', ref: active.bundle.plan.plan_ref },
|
|
1610
|
+
payload: { frozen_prefix_digest: digestArtifact(events.map(({ event_digest: value }) => value)), reason_kind: finding.finding.kind }, recordedAt: canonicalNow() }));
|
|
1611
|
+
await replaceEventsAtomically(runDir, events);
|
|
1612
|
+
const result = buildControlResult({ operation: 'conflict', outcome: 'frozen',
|
|
1613
|
+
eventHeadDigest: events.at(-1).event_digest, controlHeadDigest: controlEvents.at(-1).event_digest,
|
|
1614
|
+
activeEpoch: active.pointer.plan_epoch });
|
|
1615
|
+
return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
|
|
1616
|
+
}
|
|
1617
|
+
if (controlRequest.operation === 'hold') {
|
|
1618
|
+
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1619
|
+
const conflict = events.findLast((event) => event.kind === 'conflict_found');
|
|
1620
|
+
if (!conflict) throw new ManagedRuntimeError('FINDING_UNRESOLVED', '保存済みconflictなし');
|
|
1621
|
+
const holdBarrierId = `barrier-${randomUUID()}`;
|
|
1622
|
+
const holdRecordedAt = canonicalNow();
|
|
1623
|
+
await appendControl({ run_id: request.request_id, kind: 'hold_prepared',
|
|
1624
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: {
|
|
1625
|
+
request_id: controlRequest.request_id,
|
|
1626
|
+
logical_intent_digest: controlIntentDigest(controlRequest),
|
|
1627
|
+
finding_digest: conflict.payload.finding_digest,
|
|
1628
|
+
barrier_id: holdBarrierId, recorded_at: holdRecordedAt,
|
|
1629
|
+
} });
|
|
1630
|
+
const held = await managedSupervisor.holdConflict({
|
|
1631
|
+
findingDigest: conflict.payload.finding_digest, frozenEventDigest: events.at(-1).event_digest,
|
|
1632
|
+
barrierId: holdBarrierId, reason: conflict.payload.kind,
|
|
1633
|
+
recordedAt: holdRecordedAt,
|
|
1634
|
+
});
|
|
1635
|
+
controlEvents = await eventStore.readEvents();
|
|
1636
|
+
const quiesced = controlEvents.filter((event) => event.kind === 'executor_quiesced'
|
|
1637
|
+
&& held.quiescence_ack_digests.includes(event.payload?.ack?.ack_digest));
|
|
1638
|
+
for (const evidence of quiesced) {
|
|
1639
|
+
const checkpoint = evidence.payload.direct_observation.checkpoint;
|
|
1640
|
+
events.push(buildNextRunEvent({ events, runId: request.request_id,
|
|
1641
|
+
kind: 'checkpoint_observed', planEpoch: controlRequest.payload.expected_epoch,
|
|
1642
|
+
subject: { kind: 'todo', ref: evidence.payload.todo_id },
|
|
1643
|
+
payload: { ...structuredClone(checkpoint), barrier_final: true,
|
|
1644
|
+
barrier_evidence_digest: evidence.payload.evidence_digest },
|
|
1645
|
+
recordedAt: holdRecordedAt }));
|
|
1646
|
+
}
|
|
1647
|
+
const active = await readCommittedEpochStore(runDir);
|
|
1648
|
+
const decided = decideHoldAndCarryOver({ runId: request.request_id,
|
|
1649
|
+
request: active.bundle.request, plan: active.bundle.plan,
|
|
1650
|
+
manifests: active.bundle.manifests, packets: active.bundle.executor_packets,
|
|
1651
|
+
events, recordedAt: holdRecordedAt });
|
|
1652
|
+
events = decided.events;
|
|
1653
|
+
await replaceEventsAtomically(runDir, events);
|
|
1654
|
+
if (typeof crashInjector === 'function') await crashInjector('after_hold_effect', {
|
|
1655
|
+
request_id: controlRequest.request_id, hold_result_digest: held.result_digest,
|
|
1656
|
+
});
|
|
1657
|
+
await replaceNamedJsonAtomically(runDir, 'hold-result.json', held);
|
|
1658
|
+
const operationReceipt = {
|
|
1659
|
+
schema: 'lattice.runtime_control_operation_receipt.v1',
|
|
1660
|
+
request_id: controlRequest.request_id,
|
|
1661
|
+
logical_intent_digest: controlIntentDigest(controlRequest),
|
|
1662
|
+
operation: 'hold', outcome: 'held', effect_digest: held.result_digest,
|
|
1663
|
+
receipt_digest: '',
|
|
1664
|
+
};
|
|
1665
|
+
operationReceipt.receipt_digest = selfDigest(operationReceipt, 'receipt_digest');
|
|
1666
|
+
await replaceCanonicalJsonAtomically(runDir, 'hold-operation-receipt.json', operationReceipt);
|
|
1667
|
+
const result = buildControlResult({ operation: 'hold', outcome: 'held',
|
|
1668
|
+
eventHeadDigest: events.at(-1).event_digest, controlHeadDigest: controlEvents.at(-1).event_digest,
|
|
1669
|
+
activeEpoch: 1 });
|
|
1670
|
+
return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
|
|
1671
|
+
}
|
|
1672
|
+
if (controlRequest.operation === 'recompile') {
|
|
1673
|
+
const recompileRequest = controlRequest.payload.artifact;
|
|
1674
|
+
if (digestArtifact(recompileRequest) !== controlRequest.payload.artifact_digest) {
|
|
1675
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'recompile artifact digest不一致');
|
|
1676
|
+
}
|
|
1677
|
+
const active = await readCommittedEpochStore(runDir);
|
|
1678
|
+
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
1679
|
+
const freeze = events.findLast((event) => event.kind === 'intake_frozen');
|
|
1680
|
+
const holdEvent = events.findLast((event) => event.kind === 'hold_decided'
|
|
1681
|
+
&& event.plan_epoch === active.pointer.plan_epoch);
|
|
1682
|
+
if (freeze === undefined || holdEvent === undefined
|
|
1683
|
+
|| !validateRuntimeRecompileRequest(recompileRequest, {
|
|
1684
|
+
predecessorBundle: active.bundle, frozenEventDigest: freeze.event_digest,
|
|
1685
|
+
holdDecisionDigest: holdEvent.payload?.decision_digest, validatePhaseRevision,
|
|
1686
|
+
})) throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'frozen/hold/successor binding不正');
|
|
1687
|
+
|
|
1688
|
+
// front-endはv1入力を再利用するが、successor identityはv2 digestへ再封印する。
|
|
1689
|
+
const successorV1 = { schema: 'lattice.run_request.v1',
|
|
1690
|
+
request_id: recompileRequest.successor_request.request_id,
|
|
1691
|
+
repo: recompileRequest.successor_request.repo,
|
|
1692
|
+
capacity: recompileRequest.successor_request.capacity,
|
|
1693
|
+
todos: recompileRequest.successor_request.todos,
|
|
1694
|
+
manual_witness: recompileRequest.successor_request.manual_witness,
|
|
1695
|
+
sensor_query_set: recompileRequest.successor_request.sensor_query_set,
|
|
1696
|
+
executor_capability: recompileRequest.successor_request.executor_capability,
|
|
1697
|
+
claim_mode: recompileRequest.successor_request.claim_mode, request_digest: '' };
|
|
1698
|
+
successorV1.request_digest = selfDigest(successorV1, 'request_digest');
|
|
1699
|
+
const nextEpoch = active.pointer.plan_epoch + 1;
|
|
1700
|
+
const compiled = await compileFromRepo({ request: successorV1, cwd: repoRoot,
|
|
1701
|
+
planRef: `plan-${recompileRequest.run_id}-e${nextEpoch}`, planEpoch: nextEpoch,
|
|
1702
|
+
predecessorRefs: [active.bundle.plan.plan_ref] });
|
|
1703
|
+
if (compiled.outcome !== 'dispatchable') {
|
|
1704
|
+
throw new ManagedRuntimeError(compiled.code ?? 'SEAM_SPLIT_UNPROVEN', 'successor compileがdispatchableでない');
|
|
1705
|
+
}
|
|
1706
|
+
const newPlan = structuredClone(compiled.plan);
|
|
1707
|
+
newPlan.request_digest = recompileRequest.successor_request.request_digest;
|
|
1708
|
+
if (recompileRequest.mode === 'intentional_serial') {
|
|
1709
|
+
const serial = recompileRequest.intentional_serial;
|
|
1710
|
+
const key = `${serial.todo_ids.join('\0')}\0${serial.resource_id}`;
|
|
1711
|
+
const exists = newPlan.conflicts.some((entry) => `${entry.todo_ids.join('\0')}\0${entry.resource_id}` === key);
|
|
1712
|
+
if (!exists) newPlan.conflicts.push({ todo_ids: [...serial.todo_ids], resource_id: serial.resource_id });
|
|
1713
|
+
newPlan.conflicts.sort((left, right) => `${left.todo_ids.join('\0')}\0${left.resource_id}`
|
|
1714
|
+
.localeCompare(`${right.todo_ids.join('\0')}\0${right.resource_id}`));
|
|
1715
|
+
}
|
|
1716
|
+
newPlan.plan_digest = selfDigest(newPlan, 'plan_digest');
|
|
1717
|
+
const executorPackets = buildExecutorPackets({ plan: newPlan, manifests: compiled.manifests });
|
|
1718
|
+
|
|
1719
|
+
// 既存pure coreからcontext invalidationとcarry rebind packetだけを再利用する。
|
|
1720
|
+
const predecessorRequest = active.bundle.request.schema === 'lattice.run_request.v1'
|
|
1721
|
+
? active.bundle.request : { ...active.bundle.request, schema: 'lattice.run_request.v1' };
|
|
1722
|
+
delete predecessorRequest.predecessor_request_digest;
|
|
1723
|
+
delete predecessorRequest.task_migration_digest;
|
|
1724
|
+
predecessorRequest.request_digest = selfDigest(predecessorRequest, 'request_digest');
|
|
1725
|
+
const corePlan = active.bundle.request.schema === 'lattice.run_request.v1'
|
|
1726
|
+
? active.bundle.plan : { ...active.bundle.plan, request_digest: predecessorRequest.request_digest };
|
|
1727
|
+
corePlan.plan_digest = selfDigest(corePlan, 'plan_digest');
|
|
1728
|
+
const predecessorEvents = events;
|
|
1729
|
+
// event batch receiptより先に、outer reprocessが同じrecompile transactionを
|
|
1730
|
+
// 再構成できるbindingをdurable化する。
|
|
1731
|
+
const priorPending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
|
|
1732
|
+
'pending recompile').catch(() => null);
|
|
1733
|
+
const pendingRecompile = {
|
|
1734
|
+
schema: 'lattice.runtime_pending_recompile.v1',
|
|
1735
|
+
control_request_id: controlRequest.request_id,
|
|
1736
|
+
reprocess_request_id: priorPending?.reprocess_request_id ?? null,
|
|
1737
|
+
reprocess_queue_digest: priorPending?.reprocess_queue_digest ?? null,
|
|
1738
|
+
recovery_response_outcome: priorPending?.recovery_response_outcome ?? null,
|
|
1739
|
+
recompile_request: structuredClone(recompileRequest),
|
|
1740
|
+
predecessor_epoch: active.pointer.plan_epoch,
|
|
1741
|
+
frozen_event_digest: freeze.event_digest,
|
|
1742
|
+
hold_decision_digest: holdEvent.payload.decision_digest,
|
|
1743
|
+
pending_digest: '',
|
|
1744
|
+
};
|
|
1745
|
+
pendingRecompile.pending_digest = selfDigest(pendingRecompile, 'pending_digest');
|
|
1746
|
+
await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pendingRecompile);
|
|
1747
|
+
const core = recompileNextEpochPlan({ runId: active.meta.run_id,
|
|
1748
|
+
request: predecessorRequest, plan: corePlan, manifests: active.bundle.manifests,
|
|
1749
|
+
packets: active.bundle.executor_packets, events, holdDecision: holdEvent.payload,
|
|
1750
|
+
additionalConflicts: recompileRequest.mode === 'intentional_serial'
|
|
1751
|
+
? [{ todo_ids: recompileRequest.intentional_serial.todo_ids,
|
|
1752
|
+
resource_id: recompileRequest.intentional_serial.resource_id }] : [],
|
|
1753
|
+
recordedAt: canonicalNow() });
|
|
1754
|
+
const proposedCoreBatch = [];
|
|
1755
|
+
let proposedCoreEvents = [...predecessorEvents];
|
|
1756
|
+
for (const proposed of core.events.slice(predecessorEvents.length)) {
|
|
1757
|
+
const payload = proposed.kind === 'plan_recompiled'
|
|
1758
|
+
? { ...proposed.payload, new_plan_digest: newPlan.plan_digest }
|
|
1759
|
+
: proposed.payload;
|
|
1760
|
+
const event = buildNextRunEvent({ events: proposedCoreEvents, runId: active.meta.run_id,
|
|
1761
|
+
kind: proposed.kind, planEpoch: proposed.plan_epoch,
|
|
1762
|
+
subject: proposed.kind === 'plan_recompiled'
|
|
1763
|
+
? { kind: 'runtime_plan', ref: newPlan.plan_ref } : proposed.subject,
|
|
1764
|
+
payload, recordedAt: proposed.recorded_at });
|
|
1765
|
+
proposedCoreEvents.push(event);
|
|
1766
|
+
proposedCoreBatch.push(event);
|
|
1767
|
+
}
|
|
1768
|
+
events = [...predecessorEvents];
|
|
1769
|
+
const planDiff = { ...core.planDiff, new_plan_ref: newPlan.plan_ref };
|
|
1770
|
+
planDiff.diff_digest = selfDigest(planDiff, 'diff_digest');
|
|
1771
|
+
const rebindPackets = Object.fromEntries(Object.entries(core.rebindPackets)
|
|
1772
|
+
.filter(([todoId]) => Object.hasOwn(executorPackets, todoId)));
|
|
1773
|
+
const bundleBody = { schema: 'lattice.runtime_epoch_bundle.v1', run_id: active.meta.run_id,
|
|
1774
|
+
plan_epoch: nextEpoch, request: recompileRequest.successor_request, plan: newPlan,
|
|
1775
|
+
manifests: compiled.manifests, executor_packets: executorPackets,
|
|
1776
|
+
rebind_packets: rebindPackets, plan_diff: planDiff,
|
|
1777
|
+
task_migration: recompileRequest.task_migration,
|
|
1778
|
+
treatment: recompileRequest.mode === 'seam_split'
|
|
1779
|
+
? recompileRequest.seam_split : recompileRequest.intentional_serial,
|
|
1780
|
+
phase_revision_digest: null, phase_revision_commit_receipt: null,
|
|
1781
|
+
predecessor_bundle_digest: active.bundle.bundle_digest };
|
|
1782
|
+
bundleBody.bundle_digest = digestArtifact(bundleBody);
|
|
1783
|
+
const treatment = recompileRequest.mode === 'seam_split'
|
|
1784
|
+
? recompileRequest.seam_split : recompileRequest.intentional_serial;
|
|
1785
|
+
const treatmentFinding = await readBoundedJson(path.join(runDir, 'findings',
|
|
1786
|
+
`${treatment.finding_digest}.json`), 'runtime finding');
|
|
1787
|
+
if (!validateRuntimeFindingRecord(treatmentFinding)
|
|
1788
|
+
|| treatmentFinding.run_id !== active.meta.run_id
|
|
1789
|
+
|| treatmentFinding.plan_epoch !== active.pointer.plan_epoch
|
|
1790
|
+
|| treatmentFinding.finding_digest !== treatment.finding_digest) {
|
|
1791
|
+
throw new ManagedRuntimeError(treatmentFinding?.plan_epoch !== active.pointer.plan_epoch
|
|
1792
|
+
? 'STALE_FINDING' : 'FINDING_UNRESOLVED', 'treatment findingがactive epochへbindしない');
|
|
1793
|
+
}
|
|
1794
|
+
if (treatment.finding_digest !== holdEvent.payload?.finding?.finding_digest
|
|
1795
|
+
|| canonicalizeArtifact(treatmentFinding.finding.todo_ids)
|
|
1796
|
+
!== canonicalizeArtifact(treatment.todo_ids ?? treatment.predecessor_task_ids)) {
|
|
1797
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'treatmentがhold finding/todo集合と一致しない');
|
|
1798
|
+
}
|
|
1799
|
+
if (recompileRequest.mode === 'intentional_serial'
|
|
1800
|
+
&& treatmentFinding.finding.resource_id !== treatment.resource_id) {
|
|
1801
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceがfinding resourceと一致しない');
|
|
1802
|
+
}
|
|
1803
|
+
if (recompileRequest.mode === 'intentional_serial'
|
|
1804
|
+
&& !treatment.todo_ids.every((todoId) => {
|
|
1805
|
+
const manifest = active.bundle.manifests[todoId];
|
|
1806
|
+
return manifest?.resources?.includes(treatment.resource_id)
|
|
1807
|
+
|| manifest?.state_effects?.some((effect) => effect.resource_id === treatment.resource_id);
|
|
1808
|
+
})) {
|
|
1809
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST', 'serial resourceをfresh ownershipから再導出できない');
|
|
1810
|
+
}
|
|
1811
|
+
if (recompileRequest.mode === 'seam_split') {
|
|
1812
|
+
const ownership = (manifests) => Object.entries(manifests).flatMap(([todoId, manifest]) => [
|
|
1813
|
+
...manifest.resources.map((resourceId) => ({ resource_id: resourceId,
|
|
1814
|
+
owner_todo_id: todoId, access_kind: 'own' })),
|
|
1815
|
+
...manifest.state_effects.map((effect) => ({ resource_id: effect.resource_id,
|
|
1816
|
+
owner_todo_id: todoId, access_kind: 'write' })),
|
|
1817
|
+
]).sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
|
|
1818
|
+
const edges = (planValue) => [
|
|
1819
|
+
...planValue.precedence.map((edge) => ({ from_todo_id: edge.from_todo_id,
|
|
1820
|
+
to_todo_id: edge.to_todo_id, kind: 'hard_dependency' })),
|
|
1821
|
+
...planValue.conflicts.map((edge) => ({ from_todo_id: edge.todo_ids[0],
|
|
1822
|
+
to_todo_id: edge.todo_ids[1], kind: 'conflict' })),
|
|
1823
|
+
].sort((left, right) => canonicalizeArtifact(left).localeCompare(canonicalizeArtifact(right)));
|
|
1824
|
+
const difference = (left, right) => left.filter((entry) => !right.some((other) =>
|
|
1825
|
+
canonicalizeArtifact(entry) === canonicalizeArtifact(other)));
|
|
1826
|
+
const beforeOwnership = ownership(active.bundle.manifests);
|
|
1827
|
+
const afterOwnership = ownership(compiled.manifests);
|
|
1828
|
+
const beforeEdges = edges(active.bundle.plan);
|
|
1829
|
+
const afterEdges = edges(newPlan);
|
|
1830
|
+
const derivedOwnership = { added: difference(afterOwnership, beforeOwnership),
|
|
1831
|
+
removed: difference(beforeOwnership, afterOwnership) };
|
|
1832
|
+
const derivedEdges = { added: difference(afterEdges, beforeEdges),
|
|
1833
|
+
removed: difference(beforeEdges, afterEdges) };
|
|
1834
|
+
if (canonicalizeArtifact(derivedOwnership.added)
|
|
1835
|
+
!== canonicalizeArtifact(treatment.ownership_diff.added)
|
|
1836
|
+
|| canonicalizeArtifact(derivedOwnership.removed)
|
|
1837
|
+
!== canonicalizeArtifact(treatment.ownership_diff.removed)
|
|
1838
|
+
|| canonicalizeArtifact(derivedEdges.added)
|
|
1839
|
+
!== canonicalizeArtifact(treatment.edge_diff.added)
|
|
1840
|
+
|| canonicalizeArtifact(derivedEdges.removed)
|
|
1841
|
+
!== canonicalizeArtifact(treatment.edge_diff.removed)) {
|
|
1842
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
|
|
1843
|
+
'seam ownership/edge diffをfresh predecessor/successorから再導出できない');
|
|
1844
|
+
}
|
|
1845
|
+
const findingPath = treatmentFinding.finding.path;
|
|
1846
|
+
if (findingPath !== null) {
|
|
1847
|
+
const coversPath = (manifest) => manifest.writes.some((declared) =>
|
|
1848
|
+
declared === findingPath || (declared.endsWith('/') && findingPath.startsWith(declared)));
|
|
1849
|
+
const overlappingBefore = Object.values(active.bundle.manifests)
|
|
1850
|
+
.filter(coversPath).length;
|
|
1851
|
+
const overlappingAfter = Object.values(compiled.manifests)
|
|
1852
|
+
.filter(coversPath).length;
|
|
1853
|
+
if (overlappingAfter > 1) {
|
|
1854
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
|
|
1855
|
+
'seam successorでfinding pathがsingle ownerへ収束していない');
|
|
1856
|
+
}
|
|
1857
|
+
} else {
|
|
1858
|
+
const findingResource = treatmentFinding.finding.resource_id;
|
|
1859
|
+
const resourceOwners = (manifests) => Object.values(manifests).filter((manifest) =>
|
|
1860
|
+
manifest.resources.includes(findingResource)
|
|
1861
|
+
|| manifest.state_effects.some((effect) => effect.resource_id === findingResource)).length;
|
|
1862
|
+
if (resourceOwners(compiled.manifests) > 1) {
|
|
1863
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
|
|
1864
|
+
'seam successorでresourceがsingle ownerへ収束していない');
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
const findingConflictResource = treatmentFinding.finding.resource_id ?? treatmentFinding.finding.path;
|
|
1868
|
+
if (newPlan.conflicts.some((conflict) => conflict.resource_id === findingConflictResource
|
|
1869
|
+
&& treatmentFinding.finding.todo_ids.every((todoId) => conflict.todo_ids.includes(todoId)))) {
|
|
1870
|
+
throw new ManagedRuntimeError('INVALID_RECOMPILE_REQUEST',
|
|
1871
|
+
'seam successorにfinding conflict edgeが残存している');
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
const validateCompiledSuccessor = (candidate, context) => validateRuntimeEpochBundle(candidate)
|
|
1875
|
+
&& candidate.predecessor_bundle_digest === context.predecessor.bundle_digest
|
|
1876
|
+
&& candidate.plan_epoch === context.pointer.plan_epoch + 1
|
|
1877
|
+
&& candidate.request.request_digest === recompileRequest.successor_request.request_digest
|
|
1878
|
+
&& candidate.task_migration.migration_digest === recompileRequest.task_migration.migration_digest
|
|
1879
|
+
&& canonicalizeArtifact(candidate.treatment) === canonicalizeArtifact(treatment)
|
|
1880
|
+
&& canonicalizeArtifact(candidate.plan) === canonicalizeArtifact(newPlan)
|
|
1881
|
+
&& canonicalizeArtifact(candidate.manifests) === canonicalizeArtifact(compiled.manifests)
|
|
1882
|
+
&& canonicalizeArtifact(candidate.executor_packets) === canonicalizeArtifact(executorPackets)
|
|
1883
|
+
&& (recompileRequest.mode !== 'intentional_serial'
|
|
1884
|
+
|| candidate.plan.conflicts.some((entry) => canonicalizeArtifact(entry.todo_ids)
|
|
1885
|
+
=== canonicalizeArtifact(recompileRequest.intentional_serial.todo_ids)
|
|
1886
|
+
&& entry.resource_id === recompileRequest.intentional_serial.resource_id));
|
|
1887
|
+
const staged = await stageSuccessorEpoch({ runDir,
|
|
1888
|
+
transactionId: recompileRequest.request_id, bundle: bundleBody, recompileRequest,
|
|
1889
|
+
validateSuccessor: validateCompiledSuccessor, validatePhaseRevision, commitPhaseRevision });
|
|
1890
|
+
if (typeof crashInjector === 'function') await crashInjector('after_successor_stage', {
|
|
1891
|
+
request_id: controlRequest.request_id, bundle_digest: staged.bundle_digest,
|
|
1892
|
+
});
|
|
1893
|
+
const queue = await readBoundedJson(path.join(runDir, 'queued-events.json'),
|
|
1894
|
+
'runtime queue').catch((error) => {
|
|
1895
|
+
if (error instanceof CliContractError && error.code === 'INPUT_UNREADABLE') return null;
|
|
1896
|
+
throw error;
|
|
1897
|
+
});
|
|
1898
|
+
let queuedReplayCommit = null;
|
|
1899
|
+
if (queue !== null) {
|
|
1900
|
+
if (!validateRuntimeQueue(queue, active.meta.run_id, active.pointer.plan_epoch)
|
|
1901
|
+
|| queue.queue_digest !== controlRequest.payload.expected_queue_digest) {
|
|
1902
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'queued event prefix binding不正');
|
|
1903
|
+
}
|
|
1904
|
+
const head = queue.entries[0];
|
|
1905
|
+
if (head !== undefined && (!['finding', 'conflict_found'].includes(head.kind)
|
|
1906
|
+
|| head.artifact_digest !== treatment.finding_digest)) {
|
|
1907
|
+
throw new ManagedRuntimeError('QUEUED_REPLAY_REQUIRED', 'queue headを同一stagingへ順序通りreplayする必要がある');
|
|
1908
|
+
}
|
|
1909
|
+
const remaining = head === undefined ? [] : queue.entries.slice(1);
|
|
1910
|
+
if (remaining.length > 0) throw new ManagedRuntimeError('QUEUED_REPLAY_REQUIRED',
|
|
1911
|
+
'successor activation前にqueue全entryの順序replayが必要');
|
|
1912
|
+
const cleared = { schema: 'lattice.runtime_queue.v1', run_id: active.meta.run_id,
|
|
1913
|
+
frozen_epoch: active.pointer.plan_epoch, entries: remaining, queue_digest: '' };
|
|
1914
|
+
cleared.queue_digest = selfDigest(cleared, 'queue_digest');
|
|
1915
|
+
queuedReplayCommit = cleared;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
// treatment/finding、fresh successor、stage、queue headを全検証した後にだけ
|
|
1919
|
+
// public run event batchをpublishする。
|
|
1920
|
+
events = await publishRuntimeEventBatch({ runDir,
|
|
1921
|
+
transactionId: recompileRequest.request_id, phase: 'recompile', planEpoch: nextEpoch,
|
|
1922
|
+
bindingDigest: digestArtifact({ successor_epoch: nextEpoch,
|
|
1923
|
+
successor_plan_digest: newPlan.plan_digest,
|
|
1924
|
+
recompile_request_digest: recompileRequest.request_digest }),
|
|
1925
|
+
currentEvents: predecessorEvents, proposedBatch: proposedCoreBatch, crashInjector });
|
|
1926
|
+
|
|
1927
|
+
const controllerActivations = [activation, ...additionalActivations];
|
|
1928
|
+
const sortedSuccessorIds = newPlan.nodes.map((node) => node.todo_id).sort();
|
|
1929
|
+
const controllerForTodo = (todoId) => controllerActivations[
|
|
1930
|
+
Math.max(0, sortedSuccessorIds.indexOf(todoId)) % controllerActivations.length];
|
|
1931
|
+
const nonceDigest = digestArtifact(sessionNonce);
|
|
1932
|
+
const issuedControlDigest = controlEvents.at(-1).event_digest;
|
|
1933
|
+
const activationTransaction = {
|
|
1934
|
+
schema: 'lattice.runtime_epoch_activation_transaction.v1',
|
|
1935
|
+
request_id: controlRequest.request_id,
|
|
1936
|
+
request_digest: controlRequest.request_digest,
|
|
1937
|
+
logical_intent_digest: controlIntentDigest(controlRequest),
|
|
1938
|
+
recompile_request_digest: recompileRequest.request_digest,
|
|
1939
|
+
predecessor_pointer_digest: active.pointer.pointer_digest,
|
|
1940
|
+
predecessor_bundle_digest: active.bundle.bundle_digest,
|
|
1941
|
+
successor_bundle_digest: staged.bundle_digest,
|
|
1942
|
+
successor_epoch: nextEpoch,
|
|
1943
|
+
queue_head_digest: controlRequest.payload.expected_queue_digest,
|
|
1944
|
+
queue_replay_commit: queuedReplayCommit,
|
|
1945
|
+
state: 'prepared', committed_pointer_digest: null, gate_digest: null,
|
|
1946
|
+
recovered_todo_ids: [],
|
|
1947
|
+
recovery_error: null,
|
|
1948
|
+
transaction_digest: '',
|
|
1949
|
+
};
|
|
1950
|
+
activationTransaction.transaction_digest = selfDigest(activationTransaction, 'transaction_digest');
|
|
1951
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', activationTransaction);
|
|
1952
|
+
const stagedLease = (todoId, packetDigest, owner) => {
|
|
1953
|
+
const lease = { schema: 'lattice.runtime_write_lease.v1',
|
|
1954
|
+
lease_id: `lease-${recompileRequest.request_id}-${todoId}`, run_id: active.meta.run_id,
|
|
1955
|
+
todo_id: todoId, plan_epoch: nextEpoch, packet_digest: packetDigest,
|
|
1956
|
+
controller_registration_digest: owner.registration.registration_digest,
|
|
1957
|
+
supervisor_session_nonce_digest: nonceDigest, state: 'staged',
|
|
1958
|
+
ttl_ms: owner.controllerDescriptor.heartbeat.ttl_ms,
|
|
1959
|
+
issued_control_digest: issuedControlDigest, lease_digest: '' };
|
|
1960
|
+
lease.lease_digest = selfDigest(lease, 'lease_digest');
|
|
1961
|
+
return lease;
|
|
1962
|
+
};
|
|
1963
|
+
const rebound = new Set(Object.keys(rebindPackets));
|
|
1964
|
+
const rebindEvidence = new Map();
|
|
1965
|
+
const preparedSuccessors = new Set();
|
|
1966
|
+
for (const [todoId, packet] of Object.entries(rebindPackets)) {
|
|
1967
|
+
const owner = controllerForTodo(todoId);
|
|
1968
|
+
const dispatch = projectRuntimeState({ events }).dispatches[todoId];
|
|
1969
|
+
const evidence = await managedSupervisor.rebindController({ controllerId: owner.controllerDescriptor.controller_id, rebindPacket: packet,
|
|
1970
|
+
stagedLease: stagedLease(todoId, packet.packet_digest, owner), expected: { todo_id: todoId,
|
|
1971
|
+
executor_handle: dispatch.payload.executor_handle, worktree_id: dispatch.payload.worktree_id,
|
|
1972
|
+
predecessor_packet_digest: active.bundle.executor_packets[todoId].packet_digest,
|
|
1973
|
+
rebind_packet_digest: packet.packet_digest } });
|
|
1974
|
+
rebindEvidence.set(todoId, { ...evidence,
|
|
1975
|
+
controller_registration_digest: owner.registration.registration_digest });
|
|
1976
|
+
}
|
|
1977
|
+
for (const todoId of core.planDiff.redispatched) {
|
|
1978
|
+
const successors = recompileRequest.task_migration.entries
|
|
1979
|
+
.find((entry) => entry.predecessor_task_id === todoId)?.successor_task_ids ?? [];
|
|
1980
|
+
for (const successorId of successors) {
|
|
1981
|
+
if (rebound.has(successorId)) continue;
|
|
1982
|
+
const packet = executorPackets[successorId];
|
|
1983
|
+
const owner = controllerForTodo(successorId);
|
|
1984
|
+
if (packet !== undefined) await managedSupervisor.prepareController({
|
|
1985
|
+
controllerId: owner.controllerDescriptor.controller_id,
|
|
1986
|
+
executorPacket: packet, stagedLease: stagedLease(successorId, packet.packet_digest, owner) });
|
|
1987
|
+
if (packet !== undefined) preparedSuccessors.add(successorId);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
const acceptedCheckpointDigests = new Set(planDiff.accepted_checkpoints);
|
|
1991
|
+
const acceptedTodoIds = new Set(projectRuntimeState({ events }).receipts
|
|
1992
|
+
.filter((receipt) => receipt.accepted_sequence !== null
|
|
1993
|
+
&& acceptedCheckpointDigests.has(receipt.payload?.checkpoint_digest))
|
|
1994
|
+
.map((receipt) => receipt.todo_id));
|
|
1995
|
+
const expectedLiveTodoIds = newPlan.nodes.map((node) => node.todo_id)
|
|
1996
|
+
.filter((todoId) => !acceptedTodoIds.has(todoId)).sort();
|
|
1997
|
+
const activatedTodoIds = [...new Set([...rebound, ...preparedSuccessors])].sort();
|
|
1998
|
+
if (canonicalizeArtifact(expectedLiveTodoIds) !== canonicalizeArtifact(activatedTodoIds)) {
|
|
1999
|
+
throw new ManagedRuntimeError('EPOCH_REBIND_INCOMPLETE', 'successor live taskのrebind/prepare集合が完全でない');
|
|
2000
|
+
}
|
|
2001
|
+
// 全direct ack後だけepoch_reboundをexact-once batchで保存する。
|
|
2002
|
+
const reboundRecordedAt = canonicalNow();
|
|
2003
|
+
const proposedReboundBatch = [];
|
|
2004
|
+
let proposedReboundEvents = [...events];
|
|
2005
|
+
for (const [todoId, packet] of Object.entries(rebindPackets)
|
|
2006
|
+
.sort(([left], [right]) => left.localeCompare(right))) {
|
|
2007
|
+
const event = buildNextRunEvent({ events: proposedReboundEvents,
|
|
2008
|
+
runId: active.meta.run_id, kind: 'epoch_rebound', planEpoch: nextEpoch,
|
|
2009
|
+
subject: { kind: 'todo', ref: todoId }, payload: { ...packet,
|
|
2010
|
+
rebind_ack_digest: rebindEvidence.get(todoId).ack.ack_digest,
|
|
2011
|
+
control_event_digest: rebindEvidence.get(todoId).control_event_digest,
|
|
2012
|
+
controller_registration_digest: rebindEvidence.get(todoId).controller_registration_digest },
|
|
2013
|
+
recordedAt: reboundRecordedAt });
|
|
2014
|
+
proposedReboundEvents.push(event);
|
|
2015
|
+
proposedReboundBatch.push(event);
|
|
2016
|
+
}
|
|
2017
|
+
events = await publishRuntimeEventBatch({ runDir,
|
|
2018
|
+
transactionId: recompileRequest.request_id, phase: 'epoch_rebound', planEpoch: nextEpoch,
|
|
2019
|
+
bindingDigest: digestArtifact({ successor_epoch: nextEpoch,
|
|
2020
|
+
successor_bundle_digest: staged.bundle_digest,
|
|
2021
|
+
ordered_rebind_packet_digests: Object.entries(rebindPackets)
|
|
2022
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
2023
|
+
.map(([, packet]) => packet.packet_digest) }),
|
|
2024
|
+
currentEvents: events, proposedBatch: proposedReboundBatch, crashInjector });
|
|
2025
|
+
controlEvents = await eventStore.readEvents();
|
|
2026
|
+
const committed = await commitStagedSuccessorEpoch({ runDir,
|
|
2027
|
+
transactionId: staged.transaction_id,
|
|
2028
|
+
activationRunEventDigest: events.at(-1).event_digest,
|
|
2029
|
+
activationControlEventDigest: controlEvents.at(-1).event_digest,
|
|
2030
|
+
readActivationControlEvents: () => eventStore.readEvents() });
|
|
2031
|
+
if (typeof crashInjector === 'function') await crashInjector('after_successor_pointer_rename', {
|
|
2032
|
+
request_id: controlRequest.request_id, pointer_digest: committed.pointer.pointer_digest,
|
|
2033
|
+
});
|
|
2034
|
+
activationTransaction.state = 'pointer_committed';
|
|
2035
|
+
activationTransaction.committed_pointer_digest = committed.pointer.pointer_digest;
|
|
2036
|
+
activationTransaction.transaction_digest = selfDigest(activationTransaction, 'transaction_digest');
|
|
2037
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', activationTransaction);
|
|
2038
|
+
if (typeof crashInjector === 'function') await crashInjector('after_successor_pointer_commit', {
|
|
2039
|
+
request_id: controlRequest.request_id, pointer_digest: committed.pointer.pointer_digest,
|
|
2040
|
+
});
|
|
2041
|
+
const activationDigest = digestArtifact({ pointer_digest: committed.pointer.pointer_digest,
|
|
2042
|
+
staged_bundle_digest: staged.bundle_digest });
|
|
2043
|
+
const activated = await managedSupervisor.commitWriteGate({ planEpoch: nextEpoch,
|
|
2044
|
+
committedEpochDigest: committed.pointer.pointer_digest, activationDigest,
|
|
2045
|
+
commitReleaseBarrier: (barrier) => commitReleaseEpochBarrier({ runDir, barrier }),
|
|
2046
|
+
afterControllerRelease: async (released) => {
|
|
2047
|
+
if (released.release_index === 1 && typeof crashInjector === 'function') {
|
|
2048
|
+
await crashInjector('after_first_controller_release', released);
|
|
2049
|
+
}
|
|
2050
|
+
},
|
|
2051
|
+
committedAt: canonicalNow() });
|
|
2052
|
+
if (queuedReplayCommit !== null) {
|
|
2053
|
+
await replaceCanonicalJsonAtomically(runDir, 'queued-events.json', queuedReplayCommit);
|
|
2054
|
+
}
|
|
2055
|
+
activationTransaction.state = 'gate_committed';
|
|
2056
|
+
activationTransaction.gate_digest = activated.gate.gate_digest;
|
|
2057
|
+
activationTransaction.transaction_digest = selfDigest(activationTransaction, 'transaction_digest');
|
|
2058
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', activationTransaction);
|
|
2059
|
+
events.push(buildNextRunEvent({ events, runId: active.meta.run_id, kind: 'intake_resumed',
|
|
2060
|
+
planEpoch: nextEpoch, subject: { kind: 'runtime_plan', ref: newPlan.plan_ref },
|
|
2061
|
+
payload: { plan_diff_digest: planDiff.diff_digest,
|
|
2062
|
+
write_gate_digest: activated.gate.gate_digest }, recordedAt: canonicalNow() }));
|
|
2063
|
+
await replaceEventsAtomically(runDir, events);
|
|
2064
|
+
if (typeof crashInjector === 'function') await crashInjector('after_successor_intake_resume', {
|
|
2065
|
+
request_id: controlRequest.request_id, gate_digest: activated.gate.gate_digest,
|
|
2066
|
+
event_digest: events.at(-1).event_digest,
|
|
2067
|
+
});
|
|
2068
|
+
const result = buildControlResult({ operation: 'recompile', outcome: 'recompiled',
|
|
2069
|
+
eventHeadDigest: events.at(-1).event_digest,
|
|
2070
|
+
controlHeadDigest: (await eventStore.readEvents()).at(-1).event_digest,
|
|
2071
|
+
activeEpoch: nextEpoch, stagedEpoch: null });
|
|
2072
|
+
return buildControlResponse(controlRequest, 'completed', result, result.control_head_digest);
|
|
2073
|
+
}
|
|
2074
|
+
if (controlRequest.operation === 'reprocess') {
|
|
2075
|
+
const active = await readCommittedEpochStore(runDir);
|
|
2076
|
+
const queue = await readBoundedJson(path.join(runDir, 'queued-events.json'), 'runtime queue');
|
|
2077
|
+
if (!validateRuntimeQueue(queue, active.meta.run_id, active.pointer.plan_epoch)
|
|
2078
|
+
|| queue.queue_digest !== controlRequest.payload.expected_queue_digest) {
|
|
2079
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'reprocess queue binding不正');
|
|
2080
|
+
}
|
|
2081
|
+
const pending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
|
|
2082
|
+
'pending recompile').catch(() => null);
|
|
2083
|
+
if (pending?.schema !== 'lattice.runtime_pending_recompile.v1'
|
|
2084
|
+
|| pending.pending_digest !== selfDigest(pending, 'pending_digest')
|
|
2085
|
+
|| pending.predecessor_epoch !== active.pointer.plan_epoch) {
|
|
2086
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'same staging recompile binding不足');
|
|
2087
|
+
}
|
|
2088
|
+
const recompileRequest = pending.recompile_request;
|
|
2089
|
+
const treatment = recompileRequest.mode === 'seam_split'
|
|
2090
|
+
? recompileRequest.seam_split : recompileRequest.intentional_serial;
|
|
2091
|
+
if (queue.entries.some((entry, index) => entry.sequence !== index + 1
|
|
2092
|
+
|| !['finding', 'conflict_found'].includes(entry.kind)
|
|
2093
|
+
|| entry.artifact_digest !== treatment.finding_digest)) {
|
|
2094
|
+
throw new ManagedRuntimeError('QUEUED_CONFLICT_REMAINS', 'queue headをpending treatmentで解決できない');
|
|
2095
|
+
}
|
|
2096
|
+
pending.reprocess_request_id = controlRequest.request_id;
|
|
2097
|
+
pending.reprocess_queue_digest = queue.queue_digest;
|
|
2098
|
+
pending.pending_digest = selfDigest(pending, 'pending_digest');
|
|
2099
|
+
await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pending);
|
|
2100
|
+
const resumeRequest = createRuntimeControlRequest({
|
|
2101
|
+
requestId: pending.control_request_id, runId: active.meta.run_id,
|
|
2102
|
+
operation: 'recompile', sessionNonce, payload: controlOperationPayload({
|
|
2103
|
+
operation: 'recompile', runRef: controlRequest.payload.run_ref,
|
|
2104
|
+
artifact: recompileRequest, artifactDigest: digestArtifact(recompileRequest),
|
|
2105
|
+
checkpointDigest: null, expectedEpoch: active.pointer.plan_epoch,
|
|
2106
|
+
expectedQueueDigest: queue.queue_digest, shutdownReason: null,
|
|
2107
|
+
}),
|
|
2108
|
+
});
|
|
2109
|
+
const resumed = await executeControl(resumeRequest);
|
|
2110
|
+
if (resumed.outcome !== 'completed') {
|
|
2111
|
+
throw new ManagedRuntimeError(resumed.result?.unmet?.[0] ?? 'RUN_RECOVERY_REQUIRED',
|
|
2112
|
+
resumed.result?.unmet?.[1] ?? 'same staging recompile再開失敗');
|
|
2113
|
+
}
|
|
2114
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2115
|
+
const nowActive = await readCommittedEpochStore(runDir);
|
|
2116
|
+
const resumedControlHead = (await eventStore.readEvents()).at(-1).event_digest;
|
|
2117
|
+
const result = buildControlResult({ operation: 'reprocess', outcome: 'reprocessed',
|
|
2118
|
+
eventHeadDigest: events.at(-1).event_digest,
|
|
2119
|
+
controlHeadDigest: resumedControlHead,
|
|
2120
|
+
activeEpoch: nowActive.pointer.plan_epoch, stagedEpoch: null });
|
|
2121
|
+
return buildControlResponse(controlRequest, 'completed', result,
|
|
2122
|
+
resumedControlHead);
|
|
2123
|
+
}
|
|
2124
|
+
if (['close', 'abandon'].includes(controlRequest.operation)) {
|
|
2125
|
+
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2126
|
+
const active = await readCommittedEpochStore(runDir);
|
|
2127
|
+
const state = projectRuntimeState({ events });
|
|
2128
|
+
const shutdownReason = controlRequest.payload.shutdown_reason;
|
|
2129
|
+
if (typeof shutdownReason !== 'string' || shutdownReason.length === 0) {
|
|
2130
|
+
throw new ManagedRuntimeError('MANAGED_SHUTDOWN_INCOMPLETE', 'shutdown reason不足');
|
|
2131
|
+
}
|
|
2132
|
+
let proposed;
|
|
2133
|
+
if (controlRequest.operation === 'close') {
|
|
2134
|
+
proposed = closeRunIfComplete({ runId: request.request_id, plan: active.bundle.plan,
|
|
2135
|
+
events, recordedAt: canonicalNow() });
|
|
2136
|
+
if (!proposed.closed) throw new ManagedRuntimeError('RUN_NOT_COMPLETE', '全TODO完了前はcloseできない');
|
|
2137
|
+
}
|
|
2138
|
+
const shutdown = await managedSupervisor.shutdownManaged({ mode: controlRequest.operation,
|
|
2139
|
+
reason: shutdownReason, barrierId: `shutdown-${randomUUID()}`,
|
|
2140
|
+
frozenEventDigest: events.at(-1).event_digest, recordedAt: canonicalNow() });
|
|
2141
|
+
if (controlRequest.operation === 'close') events = proposed.events;
|
|
2142
|
+
else {
|
|
2143
|
+
events = [...events, buildNextRunEvent({ events, runId: request.request_id, kind: 'run_closed',
|
|
2144
|
+
planEpoch: active.pointer.plan_epoch,
|
|
2145
|
+
subject: { kind: 'runtime_plan', ref: active.bundle.plan.plan_ref },
|
|
2146
|
+
payload: { outcome: 'abandoned', reason: shutdownReason, accepted: state.accepted },
|
|
2147
|
+
recordedAt: canonicalNow() })];
|
|
2148
|
+
}
|
|
2149
|
+
await replaceEventsAtomically(runDir, events);
|
|
2150
|
+
await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
|
|
2151
|
+
session_nonce_digest: digestArtifact(sessionNonce),
|
|
2152
|
+
payload: { shutdown_result_digest: shutdown.result_digest } });
|
|
2153
|
+
const result = buildControlResult({ operation: controlRequest.operation,
|
|
2154
|
+
outcome: controlRequest.operation === 'close' ? 'closed' : 'abandoned',
|
|
2155
|
+
eventHeadDigest: events.at(-1).event_digest,
|
|
2156
|
+
controlHeadDigest: controlEvents.at(-1).event_digest,
|
|
2157
|
+
activeEpoch: active.pointer.plan_epoch });
|
|
2158
|
+
setTimeout(() => {
|
|
2159
|
+
server?.close(() => {
|
|
2160
|
+
resolveActiveRuntimePaths({ runDir }).then((active) => Promise.all([
|
|
2161
|
+
rm(socketPath, { force: true }), rm(active.sessionPath, { force: true }),
|
|
2162
|
+
rm(path.join(supervisorDir, 'session'), { force: true }),
|
|
2163
|
+
])).catch(() => {});
|
|
2164
|
+
});
|
|
2165
|
+
}, 200);
|
|
2166
|
+
return buildControlResponse(controlRequest, 'completed', result, controlEvents.at(-1).event_digest);
|
|
2167
|
+
}
|
|
2168
|
+
throw new ManagedRuntimeError('RUN_NOT_MANAGED', `未知のmanaged operation: ${controlRequest.operation}`);
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
const code = error?.code ?? 'ADAPTER_CONTROLLER_UNAVAILABLE';
|
|
2171
|
+
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
|
|
2172
|
+
const result = buildControlResult({ operation: controlRequest.operation, outcome: 'rejected',
|
|
2173
|
+
eventHeadDigest: events.at(-1)?.event_digest ?? null,
|
|
2174
|
+
controlHeadDigest: controlEvents.at(-1)?.event_digest ?? null, activeEpoch: 1,
|
|
2175
|
+
unmet: [code, String(error?.message ?? error)] });
|
|
2176
|
+
if (controlRequest.operation === 'activate' && !activationCommitted) {
|
|
2177
|
+
setTimeout(() => {
|
|
2178
|
+
server?.close(() => {
|
|
2179
|
+
Promise.all([
|
|
2180
|
+
activation?.disposeController?.(),
|
|
2181
|
+
candidateDir ? rm(candidateDir, { recursive: true, force: true }) : Promise.resolve(),
|
|
2182
|
+
candidateControllerDir ? rm(candidateControllerDir, { recursive: true, force: true }) : Promise.resolve(),
|
|
2183
|
+
rm(socketPath, { force: true }),
|
|
2184
|
+
restarting ? Promise.resolve() : rm(path.join(runDir, 'control-request-ledger.json'), { force: true }),
|
|
2185
|
+
restarting ? Promise.resolve() : rm(supervisorDir, { recursive: true, force: true }),
|
|
2186
|
+
]).catch(() => {});
|
|
2187
|
+
});
|
|
2188
|
+
}, 200);
|
|
2189
|
+
}
|
|
2190
|
+
return buildControlResponse(controlRequest, 'rejected', result, controlEvents.at(-1)?.event_digest ?? null);
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
const handler = async (controlRequest) => {
|
|
2194
|
+
let known = await requestStore.readRequest(controlRequest);
|
|
2195
|
+
let sessionRecoveryResponse = null;
|
|
2196
|
+
if (known?.state === 'completed'
|
|
2197
|
+
&& ['recompile', 'reprocess'].includes(controlRequest.operation)) {
|
|
2198
|
+
const recoveryEvents = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2199
|
+
const recoveryCommitted = await readCommittedEpochStore(runDir);
|
|
2200
|
+
const expectedSuccessor = controlRequest.payload.expected_epoch + 1;
|
|
2201
|
+
if (recoveryCommitted?.pointer.plan_epoch === expectedSuccessor
|
|
2202
|
+
&& !(await isActiveSupervisorGateCommitted(runDir, recoveryEvents, expectedSuccessor))) {
|
|
2203
|
+
sessionRecoveryResponse = known.response;
|
|
2204
|
+
known = { ...known, state: 'in_progress', response: null };
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (known !== null && controlRequest.operation === 'activate') {
|
|
2208
|
+
const active = await resolveActiveRuntimePaths({ runDir }).catch(() => null);
|
|
2209
|
+
if (active?.pointer?.activation_request_id === controlRequest.request_id
|
|
2210
|
+
&& active.pointer.activation_intent_digest === controlIntentDigest(controlRequest)) {
|
|
2211
|
+
const priorResponse = await readBoundedJson(path.join(path.dirname(active.descriptorPath),
|
|
2212
|
+
'activation-response.json'), 'activation response');
|
|
2213
|
+
if (!validateRuntimeControlResponse(priorResponse, controlRequest.operation)
|
|
2214
|
+
|| priorResponse.response_digest !== active.pointer.activation_response_digest) {
|
|
2215
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'committed activation response binding不正');
|
|
2216
|
+
}
|
|
2217
|
+
const currentActivation = active.pointer.activation_request_digest
|
|
2218
|
+
=== controlRequest.request_digest
|
|
2219
|
+
&& active.pointer.session_nonce_digest === digestArtifact(sessionNonce)
|
|
2220
|
+
&& known.request_digest === controlRequest.request_digest
|
|
2221
|
+
&& managedSupervisor !== null;
|
|
2222
|
+
if (currentActivation) {
|
|
2223
|
+
if (known.state === 'completed') {
|
|
2224
|
+
if (known.response.response_digest !== priorResponse.response_digest) {
|
|
2225
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED',
|
|
2226
|
+
'current activation ledger/pointer response binding不正');
|
|
2227
|
+
}
|
|
2228
|
+
return known.response;
|
|
2229
|
+
}
|
|
2230
|
+
return requestStore.replaceCompletedActivationRequest(controlRequest, priorResponse);
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
let activationLock;
|
|
2234
|
+
try {
|
|
2235
|
+
activationLock = await acquireRuntimeLifecycleLock({ runDir,
|
|
2236
|
+
sessionNonceDigest: digestArtifact(sessionNonce), operation: controlRequest.operation,
|
|
2237
|
+
requestId: controlRequest.request_id, timeoutMs: 0 });
|
|
2238
|
+
const freshResponse = await executeControl(controlRequest);
|
|
2239
|
+
if (freshResponse.outcome !== 'completed') return freshResponse;
|
|
2240
|
+
return requestStore.replaceCompletedActivationRequest(controlRequest, freshResponse);
|
|
2241
|
+
} finally {
|
|
2242
|
+
await activationLock?.release();
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (known?.state === 'completed') return known.response;
|
|
2246
|
+
if (known?.state === 'in_progress' && controlRequest.operation === 'reprocess'
|
|
2247
|
+
&& sessionRecoveryResponse === null) {
|
|
2248
|
+
const recovered = await readBoundedJson(path.join(runDir,
|
|
2249
|
+
`reprocess-operation-response-${controlRequest.request_id}.json`),
|
|
2250
|
+
'reprocess operation response').catch(() => null);
|
|
2251
|
+
if (validateRuntimeControlResponse(recovered, 'reprocess')
|
|
2252
|
+
&& recovered.request_id === controlRequest.request_id) {
|
|
2253
|
+
return requestStore.completeRequest(controlRequest, recovered);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
const staleInProgress = known?.state === 'in_progress'
|
|
2257
|
+
&& known.request_digest !== controlRequest.request_digest;
|
|
2258
|
+
if (controlRequest.run_id !== request.request_id
|
|
2259
|
+
|| controlRequest.session_nonce !== sessionNonce || staleInProgress
|
|
2260
|
+
|| sessionRecoveryResponse !== null) {
|
|
2261
|
+
let events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
|
|
2262
|
+
const active = await resolveActiveRuntimePaths({ runDir }).catch(() => null);
|
|
2263
|
+
const journal = active === null ? []
|
|
2264
|
+
: await readBoundedJson(active.controlEventsPath, 'runtime control events').catch(() => []);
|
|
2265
|
+
let outcome = 'rejected';
|
|
2266
|
+
let operationOutcome = 'rejected';
|
|
2267
|
+
let unmet = ['RUN_RECOVERY_REQUIRED', '旧session requestはdurable side effectを導出できない'];
|
|
2268
|
+
if (known?.state === 'in_progress') {
|
|
2269
|
+
let holdReceipt = controlRequest.operation === 'hold'
|
|
2270
|
+
? await readBoundedJson(path.join(runDir, 'hold-operation-receipt.json'),
|
|
2271
|
+
'hold operation receipt').catch(() => null)
|
|
2272
|
+
: null;
|
|
2273
|
+
if (controlRequest.operation === 'hold' && holdReceipt === null) {
|
|
2274
|
+
const reconstructed = reconstructHoldResultFromJournal({ journal,
|
|
2275
|
+
runId: request.request_id, requestId: controlRequest.request_id,
|
|
2276
|
+
intentDigest: controlIntentDigest(controlRequest) });
|
|
2277
|
+
if (reconstructed !== null) {
|
|
2278
|
+
await replaceCanonicalJsonAtomically(runDir, 'hold-result.json', reconstructed);
|
|
2279
|
+
holdReceipt = {
|
|
2280
|
+
schema: 'lattice.runtime_control_operation_receipt.v1',
|
|
2281
|
+
request_id: controlRequest.request_id,
|
|
2282
|
+
logical_intent_digest: controlIntentDigest(controlRequest),
|
|
2283
|
+
operation: 'hold', outcome: 'held', effect_digest: reconstructed.result_digest,
|
|
2284
|
+
receipt_digest: '',
|
|
2285
|
+
};
|
|
2286
|
+
holdReceipt.receipt_digest = selfDigest(holdReceipt, 'receipt_digest');
|
|
2287
|
+
await replaceCanonicalJsonAtomically(runDir, 'hold-operation-receipt.json', holdReceipt);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
const held = holdReceipt?.schema === 'lattice.runtime_control_operation_receipt.v1'
|
|
2291
|
+
&& holdReceipt.request_id === controlRequest.request_id
|
|
2292
|
+
&& holdReceipt.logical_intent_digest === controlIntentDigest(controlRequest)
|
|
2293
|
+
&& holdReceipt.operation === 'hold' && holdReceipt.outcome === 'held'
|
|
2294
|
+
&& holdReceipt.receipt_digest === selfDigest(holdReceipt, 'receipt_digest');
|
|
2295
|
+
if (held && !events.some((event) => event.kind === 'hold_decided'
|
|
2296
|
+
&& event.payload?.finding?.finding_digest
|
|
2297
|
+
=== journal.find((event) => event.kind === 'hold_prepared'
|
|
2298
|
+
&& event.payload.request_id === controlRequest.request_id)?.payload.finding_digest)) {
|
|
2299
|
+
const heldResult = await readBoundedJson(path.join(runDir, 'hold-result.json'),
|
|
2300
|
+
'hold result');
|
|
2301
|
+
const evidenceEvents = journal.filter((event) => event.kind === 'executor_quiesced'
|
|
2302
|
+
&& heldResult.quiescence_ack_digests.includes(event.payload?.ack?.ack_digest));
|
|
2303
|
+
for (const evidence of evidenceEvents) {
|
|
2304
|
+
if (events.some((event) => event.kind === 'checkpoint_observed'
|
|
2305
|
+
&& event.payload?.barrier_evidence_digest === evidence.payload.evidence_digest)) continue;
|
|
2306
|
+
events.push(buildNextRunEvent({ events, runId: request.request_id,
|
|
2307
|
+
kind: 'checkpoint_observed', planEpoch: controlRequest.payload.expected_epoch,
|
|
2308
|
+
subject: { kind: 'todo', ref: evidence.payload.todo_id },
|
|
2309
|
+
payload: { ...structuredClone(evidence.payload.direct_observation.checkpoint),
|
|
2310
|
+
barrier_final: true, barrier_evidence_digest: evidence.payload.evidence_digest },
|
|
2311
|
+
recordedAt: canonicalNow() }));
|
|
2312
|
+
}
|
|
2313
|
+
const activeEpoch = await readCommittedEpochStore(runDir);
|
|
2314
|
+
const decided = decideHoldAndCarryOver({ runId: request.request_id,
|
|
2315
|
+
request: activeEpoch.bundle.request, plan: activeEpoch.bundle.plan,
|
|
2316
|
+
manifests: activeEpoch.bundle.manifests, packets: activeEpoch.bundle.executor_packets,
|
|
2317
|
+
events, recordedAt: canonicalNow() });
|
|
2318
|
+
events = decided.events;
|
|
2319
|
+
await replaceEventsAtomically(runDir, events);
|
|
2320
|
+
}
|
|
2321
|
+
const found = controlRequest.operation === 'conflict'
|
|
2322
|
+
&& events.some((event) => event.kind === 'conflict_found'
|
|
2323
|
+
&& event.payload?.finding_digest === controlRequest.payload.artifact_digest);
|
|
2324
|
+
let recordedFinding = null;
|
|
2325
|
+
if (controlRequest.operation === 'finding_record') {
|
|
2326
|
+
const names = await readdir(path.join(runDir, 'findings')).catch(() => []);
|
|
2327
|
+
for (const name of names.filter((entry) => /^[0-9a-f]{64}\.json$/u.test(entry))) {
|
|
2328
|
+
const candidate = await readBoundedJson(path.join(runDir, 'findings', name),
|
|
2329
|
+
'runtime finding').catch(() => null);
|
|
2330
|
+
const input = controlRequest.payload.artifact;
|
|
2331
|
+
if (validateRuntimeFindingRecord(candidate)
|
|
2332
|
+
&& candidate.source_checkpoint_digest === controlRequest.payload.checkpoint_digest
|
|
2333
|
+
&& candidate.finding.kind === input.proposed_kind
|
|
2334
|
+
&& canonicalizeArtifact(candidate.finding.todo_ids) === canonicalizeArtifact(input.todo_ids)
|
|
2335
|
+
&& candidate.finding.path === input.path
|
|
2336
|
+
&& candidate.finding.resource_id === input.resource_id
|
|
2337
|
+
&& input.evidence_digests.every((digest) => candidate.finding.evidence_digests.includes(digest))
|
|
2338
|
+
&& candidate.finding.evidence_digests.includes(controlRequest.payload.checkpoint_digest)) {
|
|
2339
|
+
recordedFinding = candidate; break;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
let committed = await readCommittedEpochStore(runDir).catch(() => null);
|
|
2344
|
+
let prePointerRecovered = false;
|
|
2345
|
+
if (controlRequest.operation === 'recompile'
|
|
2346
|
+
&& committed?.pointer.plan_epoch === controlRequest.payload.expected_epoch
|
|
2347
|
+
&& controlRequest.payload.artifact?.predecessor_epoch === committed.pointer.plan_epoch) {
|
|
2348
|
+
const recoveredResponse = await executeControl(controlRequest);
|
|
2349
|
+
if (recoveredResponse.outcome === 'completed') {
|
|
2350
|
+
events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2351
|
+
prePointerRecovered = true;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
let recompiled = controlRequest.operation === 'recompile'
|
|
2355
|
+
&& (prePointerRecovered || (committed?.pointer.plan_epoch === controlRequest.payload.expected_epoch + 1
|
|
2356
|
+
&& committed.bundle.request.request_digest
|
|
2357
|
+
=== controlRequest.payload.artifact?.successor_request?.request_digest
|
|
2358
|
+
&& !staleInProgress && managedSupervisor.frozen === false
|
|
2359
|
+
&& !(await isManagedRunFrozen(runDir, events))));
|
|
2360
|
+
if (controlRequest.operation === 'recompile' && committed !== null
|
|
2361
|
+
&& committed.pointer.plan_epoch === controlRequest.payload.expected_epoch + 1
|
|
2362
|
+
&& committed.bundle.request.request_digest
|
|
2363
|
+
=== controlRequest.payload.artifact?.successor_request?.request_digest
|
|
2364
|
+
&& (staleInProgress || managedSupervisor.frozen
|
|
2365
|
+
|| await isManagedRunFrozen(runDir, events))) {
|
|
2366
|
+
const transaction = await readBoundedJson(path.join(runDir,
|
|
2367
|
+
'epoch-activation-transaction.json'), 'epoch activation transaction').catch(() => null);
|
|
2368
|
+
if (transaction?.schema !== 'lattice.runtime_epoch_activation_transaction.v1'
|
|
2369
|
+
|| transaction.request_id !== controlRequest.request_id
|
|
2370
|
+
|| transaction.logical_intent_digest !== controlIntentDigest(controlRequest)
|
|
2371
|
+
|| transaction.successor_bundle_digest !== committed.bundle.bundle_digest
|
|
2372
|
+
|| !([null, committed.pointer.pointer_digest].includes(transaction.committed_pointer_digest))
|
|
2373
|
+
|| (transaction.committed_pointer_digest === null
|
|
2374
|
+
&& (transaction.state !== 'prepared'
|
|
2375
|
+
|| transaction.predecessor_bundle_digest !== committed.bundle.predecessor_bundle_digest))
|
|
2376
|
+
|| transaction.transaction_digest !== selfDigest(transaction, 'transaction_digest')) {
|
|
2377
|
+
throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED', 'activation transaction binding不正');
|
|
2378
|
+
}
|
|
2379
|
+
if (transaction.committed_pointer_digest === null) {
|
|
2380
|
+
transaction.state = 'pointer_committed';
|
|
2381
|
+
transaction.committed_pointer_digest = committed.pointer.pointer_digest;
|
|
2382
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2383
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2384
|
+
}
|
|
2385
|
+
const recoveryControllers = [activation, ...additionalActivations];
|
|
2386
|
+
const recoveryTodoIds = committed.bundle.plan.nodes.map((node) => node.todo_id).sort();
|
|
2387
|
+
const recoveryControllerForTodo = (todoId) => recoveryControllers[
|
|
2388
|
+
Math.max(0, recoveryTodoIds.indexOf(todoId)) % recoveryControllers.length];
|
|
2389
|
+
const nonceDigest = digestArtifact(sessionNonce);
|
|
2390
|
+
const issuedControlDigest = journal.at(-1)?.event_digest;
|
|
2391
|
+
transaction.state = 'recovery_preparing';
|
|
2392
|
+
transaction.recovered_todo_ids = [];
|
|
2393
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2394
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2395
|
+
const recoveryLease = (todoId, packetDigest, owner) => {
|
|
2396
|
+
const lease = { schema: 'lattice.runtime_write_lease.v1',
|
|
2397
|
+
lease_id: `lease-recovery-${controlRequest.request_id}-${todoId}`,
|
|
2398
|
+
run_id: committed.meta.run_id, todo_id: todoId,
|
|
2399
|
+
plan_epoch: committed.pointer.plan_epoch, packet_digest: packetDigest,
|
|
2400
|
+
controller_registration_digest: owner.registration.registration_digest,
|
|
2401
|
+
supervisor_session_nonce_digest: nonceDigest, state: 'staged',
|
|
2402
|
+
ttl_ms: owner.controllerDescriptor.heartbeat.ttl_ms,
|
|
2403
|
+
issued_control_digest: issuedControlDigest, lease_digest: '' };
|
|
2404
|
+
lease.lease_digest = selfDigest(lease, 'lease_digest');
|
|
2405
|
+
return lease;
|
|
2406
|
+
};
|
|
2407
|
+
const predecessorBundle = await readBoundedJson(path.join(runDir, 'epochs',
|
|
2408
|
+
String(committed.pointer.plan_epoch - 1).padStart(8, '0'), 'epoch-bundle.json'),
|
|
2409
|
+
'predecessor epoch bundle');
|
|
2410
|
+
const recoveryRebindEvidence = new Map();
|
|
2411
|
+
for (const [todoId, rebindPacket] of Object.entries(committed.bundle.rebind_packets)) {
|
|
2412
|
+
const owner = recoveryControllerForTodo(todoId);
|
|
2413
|
+
const dispatch = projectRuntimeState({ events }).dispatches[todoId];
|
|
2414
|
+
if (dispatch === undefined) throw new ManagedRuntimeError('RUN_RECOVERY_REQUIRED',
|
|
2415
|
+
`carry rebind predecessor dispatch不足: ${todoId}`);
|
|
2416
|
+
const evidence = await managedSupervisor.rebindController({ controllerId: owner.controllerDescriptor.controller_id, rebindPacket,
|
|
2417
|
+
stagedLease: recoveryLease(todoId, rebindPacket.packet_digest, owner), expected: {
|
|
2418
|
+
todo_id: todoId, executor_handle: dispatch.payload.executor_handle,
|
|
2419
|
+
worktree_id: dispatch.payload.worktree_id,
|
|
2420
|
+
predecessor_packet_digest: predecessorBundle.executor_packets[todoId]?.packet_digest
|
|
2421
|
+
?? rebindPacket.packet_digest,
|
|
2422
|
+
rebind_packet_digest: rebindPacket.packet_digest,
|
|
2423
|
+
} });
|
|
2424
|
+
recoveryRebindEvidence.set(todoId, { ...evidence,
|
|
2425
|
+
controller_registration_digest: owner.registration.registration_digest });
|
|
2426
|
+
transaction.recovered_todo_ids.push(todoId);
|
|
2427
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2428
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2429
|
+
}
|
|
2430
|
+
for (const predecessorId of committed.bundle.plan_diff.redispatched) {
|
|
2431
|
+
const successors = committed.bundle.task_migration.entries
|
|
2432
|
+
.find((entry) => entry.predecessor_task_id === predecessorId)?.successor_task_ids ?? [];
|
|
2433
|
+
for (const todoId of successors) {
|
|
2434
|
+
if (Object.hasOwn(committed.bundle.rebind_packets, todoId)) continue;
|
|
2435
|
+
const packet = committed.bundle.executor_packets[todoId];
|
|
2436
|
+
if (packet === undefined) continue;
|
|
2437
|
+
const owner = recoveryControllerForTodo(todoId);
|
|
2438
|
+
try {
|
|
2439
|
+
await managedSupervisor.prepareController({ controllerId: owner.controllerDescriptor.controller_id,
|
|
2440
|
+
executorPacket: packet, stagedLease: recoveryLease(todoId, packet.packet_digest, owner) });
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
transaction.recovery_error = `${error?.code ?? 'ERROR'}:${error?.message ?? error}`;
|
|
2443
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2444
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2445
|
+
throw error;
|
|
2446
|
+
}
|
|
2447
|
+
transaction.recovered_todo_ids.push(todoId);
|
|
2448
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2449
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
const recoveryReboundAt = canonicalNow();
|
|
2453
|
+
const recoveryReboundBatch = [];
|
|
2454
|
+
let recoveryReboundEvents = [...events];
|
|
2455
|
+
for (const [todoId, rebindPacket] of Object.entries(committed.bundle.rebind_packets)
|
|
2456
|
+
.sort(([left], [right]) => left.localeCompare(right))) {
|
|
2457
|
+
const evidence = recoveryRebindEvidence.get(todoId);
|
|
2458
|
+
const event = buildNextRunEvent({ events: recoveryReboundEvents,
|
|
2459
|
+
runId: committed.meta.run_id, kind: 'epoch_rebound',
|
|
2460
|
+
planEpoch: committed.pointer.plan_epoch,
|
|
2461
|
+
subject: { kind: 'todo', ref: todoId }, payload: { ...rebindPacket,
|
|
2462
|
+
rebind_ack_digest: evidence.ack.ack_digest,
|
|
2463
|
+
control_event_digest: evidence.control_event_digest,
|
|
2464
|
+
controller_registration_digest: evidence.controller_registration_digest },
|
|
2465
|
+
recordedAt: recoveryReboundAt });
|
|
2466
|
+
recoveryReboundEvents.push(event);
|
|
2467
|
+
recoveryReboundBatch.push(event);
|
|
2468
|
+
}
|
|
2469
|
+
events = await publishRuntimeEventBatch({ runDir,
|
|
2470
|
+
transactionId: transaction.request_id,
|
|
2471
|
+
phase: `epoch_rebound_recovery_${nonceDigest}`,
|
|
2472
|
+
planEpoch: committed.pointer.plan_epoch,
|
|
2473
|
+
bindingDigest: digestArtifact({ successor_bundle_digest: committed.bundle.bundle_digest,
|
|
2474
|
+
supervisor_session_nonce_digest: nonceDigest,
|
|
2475
|
+
ordered_rebind_packet_digests: Object.entries(committed.bundle.rebind_packets)
|
|
2476
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
2477
|
+
.map(([, packet]) => packet.packet_digest) }),
|
|
2478
|
+
currentEvents: events, proposedBatch: recoveryReboundBatch, crashInjector });
|
|
2479
|
+
transaction.state = 'recovery_ready';
|
|
2480
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2481
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2482
|
+
const activationDigest = digestArtifact({ pointer_digest: committed.pointer.pointer_digest,
|
|
2483
|
+
staged_bundle_digest: committed.bundle.bundle_digest });
|
|
2484
|
+
let activated;
|
|
2485
|
+
try {
|
|
2486
|
+
activated = await managedSupervisor.commitWriteGate({
|
|
2487
|
+
planEpoch: committed.pointer.plan_epoch,
|
|
2488
|
+
committedEpochDigest: committed.pointer.pointer_digest, activationDigest,
|
|
2489
|
+
commitReleaseBarrier: (barrier) => commitReleaseEpochBarrier({ runDir, barrier }),
|
|
2490
|
+
afterControllerRelease: async (released) => {
|
|
2491
|
+
if (released.release_index === 1 && typeof crashInjector === 'function') {
|
|
2492
|
+
await crashInjector('after_first_controller_release', released);
|
|
2493
|
+
}
|
|
2494
|
+
},
|
|
2495
|
+
committedAt: canonicalNow(),
|
|
2496
|
+
});
|
|
2497
|
+
} catch (error) {
|
|
2498
|
+
transaction.recovery_error = `${error?.code ?? 'ERROR'}:${error?.message ?? error}`;
|
|
2499
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2500
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2501
|
+
throw error;
|
|
2502
|
+
}
|
|
2503
|
+
if (transaction.queue_replay_commit !== null) {
|
|
2504
|
+
await replaceCanonicalJsonAtomically(runDir, 'queued-events.json',
|
|
2505
|
+
transaction.queue_replay_commit);
|
|
2506
|
+
}
|
|
2507
|
+
if (!events.some((event) => event.kind === 'intake_resumed'
|
|
2508
|
+
&& event.plan_epoch === committed.pointer.plan_epoch
|
|
2509
|
+
&& event.payload?.write_gate_digest === activated.gate.gate_digest)) {
|
|
2510
|
+
events.push(buildNextRunEvent({ events, runId: committed.meta.run_id,
|
|
2511
|
+
kind: 'intake_resumed', planEpoch: committed.pointer.plan_epoch,
|
|
2512
|
+
subject: { kind: 'runtime_plan', ref: committed.bundle.plan.plan_ref },
|
|
2513
|
+
payload: { plan_diff_digest: committed.bundle.plan_diff.diff_digest,
|
|
2514
|
+
write_gate_digest: activated.gate.gate_digest }, recordedAt: canonicalNow() }));
|
|
2515
|
+
await replaceEventsAtomically(runDir, events);
|
|
2516
|
+
}
|
|
2517
|
+
transaction.state = 'gate_committed';
|
|
2518
|
+
transaction.gate_digest = activated.gate.gate_digest;
|
|
2519
|
+
transaction.transaction_digest = selfDigest(transaction, 'transaction_digest');
|
|
2520
|
+
await replaceCanonicalJsonAtomically(runDir, 'epoch-activation-transaction.json', transaction);
|
|
2521
|
+
if (typeof crashInjector === 'function') await crashInjector('after_successor_intake_resume', {
|
|
2522
|
+
request_id: controlRequest.request_id, gate_digest: activated.gate.gate_digest,
|
|
2523
|
+
event_digest: events.at(-1).event_digest,
|
|
2524
|
+
});
|
|
2525
|
+
recompiled = true;
|
|
2526
|
+
}
|
|
2527
|
+
const queue = controlRequest.operation === 'reprocess'
|
|
2528
|
+
? await readBoundedJson(path.join(runDir, 'queued-events.json'), 'runtime queue').catch(() => null)
|
|
2529
|
+
: null;
|
|
2530
|
+
let resumedReprocess = false;
|
|
2531
|
+
if (controlRequest.operation === 'reprocess' && committed !== null
|
|
2532
|
+
&& committed.pointer.plan_epoch >= controlRequest.payload.expected_epoch) {
|
|
2533
|
+
const pending = await readBoundedJson(path.join(runDir, 'pending-recompile.json'),
|
|
2534
|
+
'pending recompile').catch(() => null);
|
|
2535
|
+
if (pending?.schema === 'lattice.runtime_pending_recompile.v1'
|
|
2536
|
+
&& pending.pending_digest === selfDigest(pending, 'pending_digest')) {
|
|
2537
|
+
const resumeRequest = createRuntimeControlRequest({
|
|
2538
|
+
requestId: pending.control_request_id, runId: committed.meta.run_id,
|
|
2539
|
+
operation: 'recompile', sessionNonce, payload: controlOperationPayload({
|
|
2540
|
+
operation: 'recompile', runRef: controlRequest.payload.run_ref,
|
|
2541
|
+
artifact: pending.recompile_request,
|
|
2542
|
+
artifactDigest: digestArtifact(pending.recompile_request), checkpointDigest: null,
|
|
2543
|
+
expectedEpoch: pending.predecessor_epoch,
|
|
2544
|
+
expectedQueueDigest: pending.reprocess_queue_digest, shutdownReason: null,
|
|
2545
|
+
}),
|
|
2546
|
+
});
|
|
2547
|
+
let resumed;
|
|
2548
|
+
if (committed.pointer.plan_epoch === pending.predecessor_epoch) {
|
|
2549
|
+
resumed = await executeControl(resumeRequest);
|
|
2550
|
+
} else {
|
|
2551
|
+
try {
|
|
2552
|
+
resumed = await handler(resumeRequest);
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
pending.recovery_response_outcome = `nested_error:${error?.code ?? 'ERROR'}:${error?.message ?? error}`;
|
|
2555
|
+
pending.pending_digest = selfDigest(pending, 'pending_digest');
|
|
2556
|
+
await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pending);
|
|
2557
|
+
throw error;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
pending.recovery_response_outcome = resumed.outcome;
|
|
2561
|
+
pending.pending_digest = selfDigest(pending, 'pending_digest');
|
|
2562
|
+
await replaceCanonicalJsonAtomically(runDir, 'pending-recompile.json', pending);
|
|
2563
|
+
if (resumed.outcome === 'completed') {
|
|
2564
|
+
events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events');
|
|
2565
|
+
committed = await readCommittedEpochStore(runDir);
|
|
2566
|
+
resumedReprocess = true;
|
|
2567
|
+
const currentJournal = await eventStore.readEvents();
|
|
2568
|
+
const outerResult = buildControlResult({ operation: 'reprocess',
|
|
2569
|
+
outcome: 'reprocessed', eventHeadDigest: events.at(-1)?.event_digest ?? null,
|
|
2570
|
+
controlHeadDigest: currentJournal.at(-1)?.event_digest ?? null,
|
|
2571
|
+
activeEpoch: committed.pointer.plan_epoch, stagedEpoch: null });
|
|
2572
|
+
const outerResponse = buildControlResponse(controlRequest, 'completed', outerResult,
|
|
2573
|
+
currentJournal.at(-1)?.event_digest ?? null);
|
|
2574
|
+
await replaceCanonicalJsonAtomically(runDir,
|
|
2575
|
+
`reprocess-operation-response-${controlRequest.request_id}.json`, outerResponse);
|
|
2576
|
+
if (sessionRecoveryResponse?.outcome === 'completed') return sessionRecoveryResponse;
|
|
2577
|
+
return sessionRecoveryResponse?.outcome === 'rejected'
|
|
2578
|
+
? requestStore.recoverCompletedRequest(controlRequest, outerResponse)
|
|
2579
|
+
: requestStore.completeRequest(controlRequest, outerResponse);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
const reprocessed = controlRequest.operation === 'reprocess'
|
|
2584
|
+
&& (resumedReprocess || (committed?.pointer.plan_epoch === controlRequest.payload.expected_epoch + 1
|
|
2585
|
+
&& managedSupervisor.frozen === false
|
|
2586
|
+
&& !(await isManagedRunFrozen(runDir, events))));
|
|
2587
|
+
const closed = events.findLast((event) => event.kind === 'run_closed');
|
|
2588
|
+
const terminal = held || found || recordedFinding !== null || recompiled || reprocessed
|
|
2589
|
+
|| (controlRequest.operation === 'close' && closed?.payload?.outcome === 'completed')
|
|
2590
|
+
|| (controlRequest.operation === 'abandon' && closed?.payload?.outcome === 'abandoned');
|
|
2591
|
+
if (terminal) {
|
|
2592
|
+
outcome = 'completed';
|
|
2593
|
+
operationOutcome = held ? 'held' : found ? 'frozen'
|
|
2594
|
+
: recordedFinding !== null ? 'recorded'
|
|
2595
|
+
: recompiled ? 'recompiled' : reprocessed ? 'reprocessed'
|
|
2596
|
+
: controlRequest.operation === 'close' ? 'closed' : 'abandoned';
|
|
2597
|
+
unmet = [];
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
const result = buildControlResult({ operation: controlRequest.operation,
|
|
2601
|
+
outcome: operationOutcome, eventHeadDigest: events.at(-1)?.event_digest ?? null,
|
|
2602
|
+
controlHeadDigest: journal.at(-1)?.event_digest ?? null,
|
|
2603
|
+
activeEpoch: (await readCommittedEpochStore(runDir).catch(() => null))?.pointer.plan_epoch ?? 1,
|
|
2604
|
+
unmet });
|
|
2605
|
+
if (operationOutcome === 'recorded') {
|
|
2606
|
+
const names = await readdir(path.join(runDir, 'findings')).catch(() => []);
|
|
2607
|
+
const recovered = await Promise.all(names.filter((entry) => /^[0-9a-f]{64}\.json$/u.test(entry))
|
|
2608
|
+
.map((entry) => readBoundedJson(path.join(runDir, 'findings', entry), 'runtime finding').catch(() => null)));
|
|
2609
|
+
const candidate = recovered.find((entry) => validateRuntimeFindingRecord(entry)
|
|
2610
|
+
&& entry.source_checkpoint_digest === controlRequest.payload.checkpoint_digest);
|
|
2611
|
+
if (candidate !== undefined) {
|
|
2612
|
+
result.finding_digest = candidate.finding_digest;
|
|
2613
|
+
const body = { ...result }; delete body.result_digest;
|
|
2614
|
+
result.result_digest = digestArtifact(body);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
const response = buildControlResponse(controlRequest, outcome, result,
|
|
2618
|
+
journal.at(-1)?.event_digest ?? null);
|
|
2619
|
+
if (sessionRecoveryResponse !== null) {
|
|
2620
|
+
if (outcome !== 'completed') return response;
|
|
2621
|
+
return sessionRecoveryResponse.outcome === 'completed' ? sessionRecoveryResponse
|
|
2622
|
+
: requestStore.recoverCompletedRequest(controlRequest, response);
|
|
2623
|
+
}
|
|
2624
|
+
return known?.state === 'in_progress'
|
|
2625
|
+
? requestStore.completeRequest(controlRequest, response)
|
|
2626
|
+
: response;
|
|
2627
|
+
}
|
|
2628
|
+
if (known?.state === 'in_progress') {
|
|
2629
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
|
|
2630
|
+
const journal = await eventStore.readEvents();
|
|
2631
|
+
const result = buildControlResult({ operation: controlRequest.operation, outcome: 'unknown',
|
|
2632
|
+
eventHeadDigest: events.at(-1)?.event_digest ?? null,
|
|
2633
|
+
controlHeadDigest: journal.at(-1)?.event_digest ?? null, activeEpoch: 1,
|
|
2634
|
+
unmet: ['RUN_OUTCOME_UNKNOWN', '同一request_idの処理結果はまだ確定していない'] });
|
|
2635
|
+
return buildControlResponse(controlRequest, 'unknown', result,
|
|
2636
|
+
journal.at(-1)?.event_digest ?? null);
|
|
2637
|
+
}
|
|
2638
|
+
let lock;
|
|
2639
|
+
try {
|
|
2640
|
+
lock = await acquireRuntimeLifecycleLock({ runDir,
|
|
2641
|
+
sessionNonceDigest: digestArtifact(sessionNonce), operation: controlRequest.operation,
|
|
2642
|
+
requestId: controlRequest.request_id, timeoutMs: 0 });
|
|
2643
|
+
} catch (error) {
|
|
2644
|
+
const code = error?.code ?? 'RUN_BUSY';
|
|
2645
|
+
const events = await readBoundedJson(path.join(runDir, 'events.json'), 'run events').catch(() => []);
|
|
2646
|
+
const journal = await eventStore.readEvents().catch(() => []);
|
|
2647
|
+
const result = buildControlResult({ operation: controlRequest.operation, outcome: 'rejected',
|
|
2648
|
+
eventHeadDigest: events.at(-1)?.event_digest ?? null,
|
|
2649
|
+
controlHeadDigest: journal.at(-1)?.event_digest ?? null, activeEpoch: 1,
|
|
2650
|
+
unmet: [code, String(error?.message ?? error)] });
|
|
2651
|
+
return buildControlResponse(controlRequest, 'rejected', result,
|
|
2652
|
+
journal.at(-1)?.event_digest ?? null);
|
|
2653
|
+
}
|
|
2654
|
+
try {
|
|
2655
|
+
const begun = await requestStore.beginRequest(controlRequest);
|
|
2656
|
+
if (begun.disposition === 'completed') return begun.response;
|
|
2657
|
+
if (begun.disposition === 'in_progress') {
|
|
2658
|
+
throw new ManagedRuntimeError('RUN_OUTCOME_UNKNOWN', '同一request_idが処理中');
|
|
2659
|
+
}
|
|
2660
|
+
const response = await executeControl(controlRequest);
|
|
2661
|
+
return requestStore.completeRequest(controlRequest, response);
|
|
2662
|
+
} finally {
|
|
2663
|
+
await lock.release();
|
|
2664
|
+
}
|
|
2665
|
+
};
|
|
2666
|
+
server = await serveRuntimeControlSocket({ socketPath, handler });
|
|
2667
|
+
registerDaemonCleanup(async (signal) => {
|
|
2668
|
+
if (activationCommitted && activation !== null) {
|
|
2669
|
+
await appendControl({ run_id: request.request_id, kind: 'supervisor_stopped',
|
|
2670
|
+
session_nonce_digest: digestArtifact(sessionNonce), payload: { signal } });
|
|
2671
|
+
}
|
|
2672
|
+
await activation?.disposeController?.();
|
|
2673
|
+
await Promise.all(additionalActivations.map((entry) => entry.disposeController?.()));
|
|
2674
|
+
if (server?.listening) await new Promise((resolve) => server.close(resolve));
|
|
2675
|
+
await rm(socketPath, { force: true });
|
|
2676
|
+
const active = await resolveActiveRuntimePaths({ runDir }).catch(() => null);
|
|
2677
|
+
if (active !== null) await rm(active.sessionPath, { force: true });
|
|
2678
|
+
await rm(path.join(supervisorDir, 'session'), { force: true });
|
|
2679
|
+
});
|
|
2680
|
+
onReady(socketPath);
|
|
2681
|
+
await new Promise((resolve) => server.once('close', resolve));
|
|
2682
|
+
}
|
|
2683
|
+
|
|
405
2684
|
async function eventVerify({ runDir, stdout }) {
|
|
406
2685
|
const { events, compileArtifact } = await readRunStore(runDir);
|
|
407
2686
|
const chain = verifyRunEventChain({ events });
|
|
@@ -438,13 +2717,14 @@ async function eventVerify({ runDir, stdout }) {
|
|
|
438
2717
|
output.result_digest = digestArtifact(output);
|
|
439
2718
|
stdout.write(`${JSON.stringify(output)}\n`);
|
|
440
2719
|
if (failed.length > 0) {
|
|
441
|
-
|
|
2720
|
+
// ADR 0052 Decision 1: failed_conditionsはstdoutのevent_verification.v1が既に持つ=detailで二重化しない。
|
|
2721
|
+
throw new CliContractError('EVENT_VERIFICATION_FAILED', 'event検証が失敗');
|
|
442
2722
|
}
|
|
443
2723
|
return 0;
|
|
444
2724
|
}
|
|
445
2725
|
|
|
446
2726
|
/**
|
|
447
|
-
* `--version
|
|
2727
|
+
* `--version`以外の全argvを受け、exit codeを返す(`doctor --json`はADR 0052で退役済み)。
|
|
448
2728
|
* exact argument contract: 位置・順序・件数の完全一致だけを受理する。
|
|
449
2729
|
*/
|
|
450
2730
|
export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
@@ -452,6 +2732,18 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
452
2732
|
|| typeof stdout?.write !== 'function' || typeof stderr?.write !== 'function') {
|
|
453
2733
|
throw new TypeError('runRuntimeCli optionsが不正');
|
|
454
2734
|
}
|
|
2735
|
+
let requestIdOverride = null;
|
|
2736
|
+
if (argv.length >= 2 && argv.at(-2) === '--request-id') {
|
|
2737
|
+
if (!/^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u.test(argv.at(-1) ?? '')) {
|
|
2738
|
+
return typedFailure(stderr, 'INVALID_REQUEST_ID', 'request-idは128文字以下の識別子でなければならない');
|
|
2739
|
+
}
|
|
2740
|
+
requestIdOverride = argv.at(-1);
|
|
2741
|
+
argv = argv.slice(0, -2);
|
|
2742
|
+
if (argv[0] !== 'run' || !['activate', 'close', 'abandon', 'conflict', 'hold', 'recompile', 'reprocess'].includes(argv[1])
|
|
2743
|
+
&& !(argv[0] === 'run' && argv[1] === 'finding' && argv[2] === 'record')) {
|
|
2744
|
+
return typedFailure(stderr, 'INVALID_REQUEST_ID', '--request-idはrun mutationだけで指定できる');
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
455
2747
|
let action = null;
|
|
456
2748
|
if (argv.length === 4
|
|
457
2749
|
&& argv[0] === 'plan' && argv[1] === 'compile' && argv[2] === '--request'
|
|
@@ -478,16 +2770,97 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
478
2770
|
stdout,
|
|
479
2771
|
});
|
|
480
2772
|
} else if (argv.length === 4
|
|
481
|
-
&& argv[0] === 'run' &&
|
|
2773
|
+
&& argv[0] === 'run' && argv[1] === 'activate'
|
|
482
2774
|
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
2775
|
+
action = async () => {
|
|
2776
|
+
const { repoRoot, runDir, runRef } = await resolveRunStore(cwd, argv[3]);
|
|
2777
|
+
return runActivate({ runDir, runRef, repoRoot, stdout, requestId: requestIdOverride });
|
|
2778
|
+
};
|
|
2779
|
+
} else if (argv.length === 4
|
|
2780
|
+
&& argv[0] === 'run' && ['observe', 'status', 'resume', 'close'].includes(argv[1])
|
|
2781
|
+
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
2782
|
+
action = async () => {
|
|
2783
|
+
const { repoRoot, runDir } = await resolveRunStore(cwd, argv[3]);
|
|
2784
|
+
if (argv[1] === 'observe') return runObserve({ runDir, stdout });
|
|
2785
|
+
if (argv[1] === 'status') return runStatus({ runDir, stdout });
|
|
2786
|
+
if (argv[1] === 'resume') return runResume({ runDir, repoRoot, stdout });
|
|
2787
|
+
return runClose({ runDir, repoRoot, stdout, requestId: requestIdOverride });
|
|
2788
|
+
};
|
|
2789
|
+
} else if (argv.length === 6
|
|
2790
|
+
&& argv[0] === 'run' && argv[1] === 'abandon'
|
|
2791
|
+
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
|
|
2792
|
+
&& argv[4] === '--reason' && typeof argv[5] === 'string' && argv[5].length > 0) {
|
|
2793
|
+
action = async () => {
|
|
2794
|
+
const { runDir } = await resolveRunStore(cwd, argv[3]);
|
|
2795
|
+
return runAbandon({ runDir, runRef: argv[3], reason: argv[5], stdout,
|
|
2796
|
+
requestId: requestIdOverride });
|
|
2797
|
+
};
|
|
2798
|
+
} else if (argv.length === 6
|
|
2799
|
+
&& argv[0] === 'run' && argv[1] === 'conflict'
|
|
2800
|
+
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
|
|
2801
|
+
&& argv[4] === '--finding' && /^[0-9a-f]{64}$/u.test(argv[5])) {
|
|
2802
|
+
action = async () => {
|
|
2803
|
+
const { runDir, runRef } = await resolveRunStore(cwd, argv[3]);
|
|
2804
|
+
return runManagedControl({
|
|
2805
|
+
runDir, runRef, operation: 'conflict', artifactDigest: argv[5], stdout,
|
|
2806
|
+
requestId: requestIdOverride,
|
|
2807
|
+
});
|
|
2808
|
+
};
|
|
2809
|
+
} else if (argv.length === 8
|
|
2810
|
+
&& argv[0] === 'run' && argv[1] === 'finding' && argv[2] === 'record'
|
|
2811
|
+
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
2812
|
+
&& argv[5] === '--checkpoint' && /^[0-9a-f]{64}$/u.test(argv[6])
|
|
2813
|
+
&& argv[7] === '--input') {
|
|
2814
|
+
// exact argvにはinput valueが必要なため、この8要素形は必ずusage拒否される。
|
|
2815
|
+
action = null;
|
|
2816
|
+
} else if (argv.length === 9
|
|
2817
|
+
&& argv[0] === 'run' && argv[1] === 'finding' && argv[2] === 'record'
|
|
2818
|
+
&& argv[3] === '--run' && typeof argv[4] === 'string' && argv[4].length > 0
|
|
2819
|
+
&& argv[5] === '--checkpoint' && /^[0-9a-f]{64}$/u.test(argv[6])
|
|
2820
|
+
&& argv[7] === '--input' && typeof argv[8] === 'string' && argv[8].length > 0) {
|
|
2821
|
+
action = async () => {
|
|
2822
|
+
const { runDir, runRef } = await resolveRunStore(cwd, argv[4]);
|
|
2823
|
+
if (await readCommittedEpochStore(runDir) === null) {
|
|
2824
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
|
|
2825
|
+
}
|
|
2826
|
+
const artifact = await readBoundedJson(path.resolve(cwd, argv[8]), 'runtime finding candidate');
|
|
2827
|
+
return runManagedControl({ runDir, runRef, operation: 'finding_record', artifact,
|
|
2828
|
+
artifactDigest: digestArtifact(artifact), checkpointDigest: argv[6], stdout,
|
|
2829
|
+
requestId: requestIdOverride });
|
|
2830
|
+
};
|
|
2831
|
+
} else if (argv.length === 6
|
|
2832
|
+
&& argv[0] === 'run' && argv[1] === 'recompile'
|
|
2833
|
+
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0
|
|
2834
|
+
&& argv[4] === '--input' && typeof argv[5] === 'string' && argv[5].length > 0) {
|
|
2835
|
+
action = async () => {
|
|
2836
|
+
const { runDir, runRef } = await resolveRunStore(cwd, argv[3]);
|
|
2837
|
+
if (await readCommittedEpochStore(runDir) === null) {
|
|
2838
|
+
throw new CliContractError('RUN_NOT_MANAGED', 'runがmanaged storeへactivateされていない');
|
|
2839
|
+
}
|
|
2840
|
+
const artifact = await readBoundedJson(path.resolve(cwd, argv[5]), 'runtime recompile request');
|
|
2841
|
+
return runManagedControl({ runDir, runRef, operation: 'recompile', artifact,
|
|
2842
|
+
artifactDigest: digestArtifact(artifact), stdout, requestId: requestIdOverride });
|
|
2843
|
+
};
|
|
2844
|
+
} else if (argv.length === 4
|
|
2845
|
+
&& argv[0] === 'run' && ['hold', 'reprocess'].includes(argv[1])
|
|
2846
|
+
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
2847
|
+
action = async () => {
|
|
2848
|
+
const { runDir, runRef } = await resolveRunStore(cwd, argv[3]);
|
|
2849
|
+
return runManagedControl({
|
|
2850
|
+
runDir, runRef, operation: argv[1], artifactDigest: null, stdout,
|
|
2851
|
+
requestId: requestIdOverride,
|
|
2852
|
+
});
|
|
2853
|
+
};
|
|
2854
|
+
} else if (argv.length === 3
|
|
2855
|
+
&& argv[0] === 'run' && argv[1] === 'list' && argv[2] === '--json') {
|
|
2856
|
+
action = () => runList({ cwd, stdout });
|
|
487
2857
|
} else if (argv.length === 4
|
|
488
2858
|
&& argv[0] === 'event' && argv[1] === 'verify'
|
|
489
2859
|
&& argv[2] === '--run' && typeof argv[3] === 'string' && argv[3].length > 0) {
|
|
490
|
-
action = () =>
|
|
2860
|
+
action = async () => {
|
|
2861
|
+
const { runDir } = await resolveRunStore(cwd, argv[3]);
|
|
2862
|
+
return eventVerify({ runDir, stdout });
|
|
2863
|
+
};
|
|
491
2864
|
}
|
|
492
2865
|
if (action === null) return usageFailure(stderr, argv);
|
|
493
2866
|
try {
|
|
@@ -499,6 +2872,12 @@ export async function runRuntimeCli({ argv, cwd, stdout, stderr }) {
|
|
|
499
2872
|
if (error instanceof TypeError) {
|
|
500
2873
|
return typedFailure(stderr, 'CONTRACT_VIOLATION', error.message);
|
|
501
2874
|
}
|
|
2875
|
+
if (error instanceof RuntimeEpochStoreError) {
|
|
2876
|
+
return typedFailure(stderr, error.code, error.message);
|
|
2877
|
+
}
|
|
2878
|
+
if (error instanceof ManagedRuntimeError) {
|
|
2879
|
+
return typedFailure(stderr, error.code, error.message);
|
|
2880
|
+
}
|
|
502
2881
|
throw error;
|
|
503
2882
|
}
|
|
504
2883
|
}
|