humanish 0.0.1 → 0.15.1
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/AGENTS.md +77 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +437 -8
- package/SECURITY.md +34 -0
- package/dist/actor-contract.d.ts +107 -0
- package/dist/actor-contract.js +286 -0
- package/dist/actor-contract.js.map +1 -0
- package/dist/actor-registry.d.ts +73 -0
- package/dist/actor-registry.js +93 -0
- package/dist/actor-registry.js.map +1 -0
- package/dist/adapter-extension.d.ts +54 -0
- package/dist/adapter-extension.js +214 -0
- package/dist/adapter-extension.js.map +1 -0
- package/dist/argv.d.ts +1 -0
- package/dist/argv.js +8 -0
- package/dist/argv.js.map +1 -0
- package/dist/artifact-reference.d.ts +22 -0
- package/dist/artifact-reference.js +47 -0
- package/dist/artifact-reference.js.map +1 -0
- package/dist/browser-evidence-hygiene.d.ts +7 -0
- package/dist/browser-evidence-hygiene.js +40 -0
- package/dist/browser-evidence-hygiene.js.map +1 -0
- package/dist/claude-agent-sdk.d.ts +94 -0
- package/dist/claude-agent-sdk.js +363 -0
- package/dist/claude-agent-sdk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-app-server-ui.d.ts +39 -0
- package/dist/codex-app-server-ui.js +357 -0
- package/dist/codex-app-server-ui.js.map +1 -0
- package/dist/codex-app-server.d.ts +144 -0
- package/dist/codex-app-server.js +754 -0
- package/dist/codex-app-server.js.map +1 -0
- package/dist/command-failure.d.ts +29 -0
- package/dist/command-failure.js +54 -0
- package/dist/command-failure.js.map +1 -0
- package/dist/computer-use-actor.d.ts +52 -0
- package/dist/computer-use-actor.js +48 -0
- package/dist/computer-use-actor.js.map +1 -0
- package/dist/computer-use.d.ts +232 -0
- package/dist/computer-use.js +615 -0
- package/dist/computer-use.js.map +1 -0
- package/dist/concurrency.d.ts +1 -0
- package/dist/concurrency.js +27 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/concurrent-shared-world-lab.d.ts +115 -0
- package/dist/concurrent-shared-world-lab.js +1094 -0
- package/dist/concurrent-shared-world-lab.js.map +1 -0
- package/dist/core/git-state.d.ts +33 -0
- package/dist/core/git-state.js +277 -0
- package/dist/core/git-state.js.map +1 -0
- package/dist/core/git-workspace.d.ts +31 -0
- package/dist/core/git-workspace.js +447 -0
- package/dist/core/git-workspace.js.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/run-primitives.d.ts +66 -0
- package/dist/core/run-primitives.js +120 -0
- package/dist/core/run-primitives.js.map +1 -0
- package/dist/cua-actor-lab.d.ts +621 -0
- package/dist/cua-actor-lab.js +2845 -0
- package/dist/cua-actor-lab.js.map +1 -0
- package/dist/device-presets.d.ts +67 -0
- package/dist/device-presets.js +50 -0
- package/dist/device-presets.js.map +1 -0
- package/dist/e2b-desktop-executor.d.ts +101 -0
- package/dist/e2b-desktop-executor.js +309 -0
- package/dist/e2b-desktop-executor.js.map +1 -0
- package/dist/e2b-desktop-launch.d.ts +144 -0
- package/dist/e2b-desktop-launch.js +59 -0
- package/dist/e2b-desktop-launch.js.map +1 -0
- package/dist/e2b-detached.d.ts +53 -0
- package/dist/e2b-detached.js +130 -0
- package/dist/e2b-detached.js.map +1 -0
- package/dist/e2b-terminal-lab.d.ts +345 -0
- package/dist/e2b-terminal-lab.js +1532 -0
- package/dist/e2b-terminal-lab.js.map +1 -0
- package/dist/env-file.d.ts +14 -0
- package/dist/env-file.js +108 -0
- package/dist/env-file.js.map +1 -0
- package/dist/feedback.d.ts +51 -0
- package/dist/feedback.js +351 -0
- package/dist/feedback.js.map +1 -0
- package/dist/image-evidence.d.ts +2 -0
- package/dist/image-evidence.js +33 -0
- package/dist/image-evidence.js.map +1 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/init-templates.d.ts +12 -0
- package/dist/init-templates.js +369 -0
- package/dist/init-templates.js.map +1 -0
- package/dist/init.d.ts +26 -0
- package/dist/init.js +429 -0
- package/dist/init.js.map +1 -0
- package/dist/lab-app-runner.d.ts +78 -0
- package/dist/lab-app-runner.js +403 -0
- package/dist/lab-app-runner.js.map +1 -0
- package/dist/lab-config.d.ts +583 -0
- package/dist/lab-config.js +1789 -0
- package/dist/lab-config.js.map +1 -0
- package/dist/lab-engine.d.ts +77 -0
- package/dist/lab-engine.js +216 -0
- package/dist/lab-engine.js.map +1 -0
- package/dist/lab-preflight.d.ts +67 -0
- package/dist/lab-preflight.js +385 -0
- package/dist/lab-preflight.js.map +1 -0
- package/dist/labs.d.ts +53 -0
- package/dist/labs.js +382 -0
- package/dist/labs.js.map +1 -0
- package/dist/observer-assets.d.ts +2 -0
- package/dist/observer-assets.js +2630 -0
- package/dist/observer-assets.js.map +1 -0
- package/dist/observer-data.d.ts +63 -0
- package/dist/observer-data.js +145 -0
- package/dist/observer-data.js.map +1 -0
- package/dist/observer-static.d.ts +39 -0
- package/dist/observer-static.js +306 -0
- package/dist/observer-static.js.map +1 -0
- package/dist/observer.d.ts +46 -0
- package/dist/observer.js +550 -0
- package/dist/observer.js.map +1 -0
- package/dist/openai-responses-cu.d.ts +113 -0
- package/dist/openai-responses-cu.js +545 -0
- package/dist/openai-responses-cu.js.map +1 -0
- package/dist/oss-lab.d.ts +52 -0
- package/dist/oss-lab.js +344 -0
- package/dist/oss-lab.js.map +1 -0
- package/dist/oss-meta-lab-scoring.d.ts +15 -0
- package/dist/oss-meta-lab-scoring.js +145 -0
- package/dist/oss-meta-lab-scoring.js.map +1 -0
- package/dist/oss-meta-lab.d.ts +277 -0
- package/dist/oss-meta-lab.js +4529 -0
- package/dist/oss-meta-lab.js.map +1 -0
- package/dist/oss-remote-telemetry.d.ts +77 -0
- package/dist/oss-remote-telemetry.js +403 -0
- package/dist/oss-remote-telemetry.js.map +1 -0
- package/dist/persona.d.ts +35 -0
- package/dist/persona.js +133 -0
- package/dist/persona.js.map +1 -0
- package/dist/pi-agent-core.d.ts +77 -0
- package/dist/pi-agent-core.js +203 -0
- package/dist/pi-agent-core.js.map +1 -0
- package/dist/program.d.ts +54 -0
- package/dist/program.js +2349 -0
- package/dist/program.js.map +1 -0
- package/dist/redaction.d.ts +90 -0
- package/dist/redaction.js +292 -0
- package/dist/redaction.js.map +1 -0
- package/dist/run-paths.d.ts +56 -0
- package/dist/run-paths.js +340 -0
- package/dist/run-paths.js.map +1 -0
- package/dist/run.d.ts +794 -0
- package/dist/run.js +4844 -0
- package/dist/run.js.map +1 -0
- package/dist/scripted-browser-actor.d.ts +297 -0
- package/dist/scripted-browser-actor.js +1399 -0
- package/dist/scripted-browser-actor.js.map +1 -0
- package/dist/scripted-browser-lab.d.ts +107 -0
- package/dist/scripted-browser-lab.js +788 -0
- package/dist/scripted-browser-lab.js.map +1 -0
- package/dist/selected-output-paths.d.ts +49 -0
- package/dist/selected-output-paths.js +354 -0
- package/dist/selected-output-paths.js.map +1 -0
- package/dist/shared-world-lab.d.ts +208 -0
- package/dist/shared-world-lab.js +1049 -0
- package/dist/shared-world-lab.js.map +1 -0
- package/dist/source-archive.d.ts +82 -0
- package/dist/source-archive.js +408 -0
- package/dist/source-archive.js.map +1 -0
- package/dist/stop-conditions.d.ts +26 -0
- package/dist/stop-conditions.js +66 -0
- package/dist/stop-conditions.js.map +1 -0
- package/dist/terminal-agent-actor.d.ts +36 -0
- package/dist/terminal-agent-actor.js +23 -0
- package/dist/terminal-agent-actor.js.map +1 -0
- package/docs/architecture/actor-contract.md +431 -0
- package/docs/architecture/github-feedback-loop.md +189 -0
- package/docs/architecture/local-codex-tui-actor.md +213 -0
- package/docs/architecture/observer.md +118 -0
- package/docs/architecture/oss-lab-poc.md +250 -0
- package/docs/architecture/project-layout.md +166 -0
- package/docs/architecture/state-driven-executor.md +203 -0
- package/docs/architecture/terminal-product-lane.md +148 -0
- package/docs/contracts/adapter-fixtures.md +80 -0
- package/docs/contracts/core.md +71 -0
- package/docs/contracts/feedback.md +145 -0
- package/docs/contracts/policy.md +311 -0
- package/docs/contracts/run-bundle.md +358 -0
- package/docs/contracts/schemas.md +984 -0
- package/docs/goals/current.md +428 -0
- package/docs/principles/invariants-and-defaults.md +135 -0
- package/docs/principles/self-driving-harness.md +129 -0
- package/docs/product/open-source-install-experience.md +237 -0
- package/docs/ramp/README.md +207 -0
- package/docs/release/open-source-readiness.md +208 -0
- package/docs/release/public-readiness-standard.md +205 -0
- package/docs/roadmap/world-class-open-source-v0.md +311 -0
- package/package.json +102 -8
- package/skills/humanish/SKILL.md +244 -0
- package/skills/humanish/agents/openai.yaml +7 -0
- package/index.js +0 -1
|
@@ -0,0 +1,1094 @@
|
|
|
1
|
+
// The CONCURRENT shared-world lab backend (#164 phase 2): N persona lanes drive ONE shared,
|
|
2
|
+
// mutable service plane SIMULTANEOUSLY — the actual leverage of a sim. A recomposition of shipped
|
|
3
|
+
// pieces + the getHost wrapper:
|
|
4
|
+
//
|
|
5
|
+
// - ONE SUBJECT sandbox: provisionCloneSubject ONCE (clone+install+build+seed) + serve on
|
|
6
|
+
// 0.0.0.0, exposed via getHost(port) → a tokenless reachable URL (the headless service host;
|
|
7
|
+
// no GUI seat).
|
|
8
|
+
// - N ACTOR desktop sandboxes: fan-out's runCuaLane machinery (per-lane device/persona, by-id
|
|
9
|
+
// teardown) bounded by execution.concurrency, each browser pointed at the getHost URL —
|
|
10
|
+
// driving the shared service AT THE SAME TIME. INDEPENDENT (FIX-11): no pipeline gate, no
|
|
11
|
+
// fail-fast — one actor's failure must not block the swarm or corrupt the "M of N" outcomes.
|
|
12
|
+
// - A background prober snapshots the subject DB checkpoint digests on a cadence → a stateSeries
|
|
13
|
+
// of the shared world evolving under load.
|
|
14
|
+
// - ALL N+1 sandboxes torn down BY exact id in a finally — NEVER Sandbox.list.
|
|
15
|
+
//
|
|
16
|
+
// HONEST ATTRIBUTION (verify-enforced, doctrine-audit fixes incorporated): the bundle declares
|
|
17
|
+
// attributionClass: shared-world + a CONCURRENT humanish.shared-world.v1 block (topologyMode
|
|
18
|
+
// "concurrent"; laneWindows + stateSeries + outcomes; NO timeline) whose attributionLimits drop
|
|
19
|
+
// `sequential-only`/`no-concurrent-races` and add `concurrent`,
|
|
20
|
+
// `best-effort-causal-attribution`, `non-deterministic-shared-state`,
|
|
21
|
+
// `window-and-snapshot-granularity`, `contention-observed-not-proven-safe`,
|
|
22
|
+
// `state-change-not-isolated-to-actors`. laneWindows + stateSeries are INDEPENDENT series with NO
|
|
23
|
+
// per-delta→actor field — causation under concurrency is structurally inexpressible.
|
|
24
|
+
//
|
|
25
|
+
// CAPABILITY vs PROOF (FIX-1): the deterministic $0 gate proves the PLUMBING + the honesty
|
|
26
|
+
// contract — the real mapWithConcurrency produces genuinely overlapping laneWindows (a rendezvous
|
|
27
|
+
// latch in the fake session forces two lane fns in-flight while the REAL orchestrator clock
|
|
28
|
+
// measures the windows). It does NOT prove "we ran many concurrent users at scale" — that
|
|
29
|
+
// CAPABILITY is backed only by the deferred, separately-authorized live receipt.
|
|
30
|
+
//
|
|
31
|
+
// Synthetic-subject (FIX-3): a getHost URL is internet-reachable for the run, so this route is
|
|
32
|
+
// synthetic-seeded-subjects ONLY. Verify fail-closes on subject.state.provenance != "seeded" and
|
|
33
|
+
// requires the author attestation subject.exposure: synthetic. This is author-trust + a provenance
|
|
34
|
+
// gate, NOT a no-real-data guarantee (Humanish cannot tell synthetic from real data).
|
|
35
|
+
import { randomBytes } from "node:crypto";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import { pathToFileURL } from "node:url";
|
|
38
|
+
import { adapterScoreFailureMessage, applyBrowserAdapterHooks } from "./adapter-extension.js";
|
|
39
|
+
import { actorRegistry, isCuaActorDescriptor } from "./actor-registry.js";
|
|
40
|
+
import { toErrorMessage } from "./command-failure.js";
|
|
41
|
+
import { mapWithConcurrency } from "./concurrency.js";
|
|
42
|
+
import { commandDigestOf, composeLaneInstructions, defaultPackLocalTree, provisionCloneSubject, provisionLocalTreeSubject, resolveLaneDevice, resolveSubjectState, runCuaLane } from "./cua-actor-lab.js";
|
|
43
|
+
import { createDesktopSandbox, loadE2BDesktopModule } from "./e2b-desktop-launch.js";
|
|
44
|
+
import { concurrentSharedWorldValidationReason } from "./lab-config.js";
|
|
45
|
+
import { buildObserverData } from "./observer-data.js";
|
|
46
|
+
import { attachObserverRuntimeStreamUrls, renderObserver } from "./observer.js";
|
|
47
|
+
import { redactText } from "./redaction.js";
|
|
48
|
+
import { prepareRunArtifactPaths, validatePreparedRunArtifactPaths } from "./run-paths.js";
|
|
49
|
+
import { writeContainedOutputFile, writePreparedRunLatestPointer } from "./selected-output-paths.js";
|
|
50
|
+
import { combineCheckpointDigest, runCheckpointSnapshot, seedRecipeDigest } from "./shared-world-lab.js";
|
|
51
|
+
import { buildRunSource, PUBLIC_TARGET_CWD, REVIEW_SCHEMA, RUN_BUNDLE_SCHEMA, SHARED_WORLD_SCHEMA } from "./run.js";
|
|
52
|
+
export const CONCURRENT_SHARED_WORLD_LAB_SCHEMA = "humanish.concurrent-shared-world-lab-result.v1";
|
|
53
|
+
export const CONCURRENT_SHARED_WORLD_PROVIDER_METADATA = {
|
|
54
|
+
mode: "concurrent-shared-world-lab",
|
|
55
|
+
tool: "humanish"
|
|
56
|
+
};
|
|
57
|
+
// The verify-enforced CONCURRENT attribution ceiling (FIX-5). Mirrored in run.ts's required set.
|
|
58
|
+
export const CONCURRENT_ATTRIBUTION_LIMITS = [
|
|
59
|
+
"concurrent",
|
|
60
|
+
"best-effort-causal-attribution",
|
|
61
|
+
"non-deterministic-shared-state",
|
|
62
|
+
"window-and-snapshot-granularity",
|
|
63
|
+
"contention-observed-not-proven-safe",
|
|
64
|
+
"state-change-not-isolated-to-actors"
|
|
65
|
+
];
|
|
66
|
+
const DEFAULT_SESSION_TIMEOUT_MS = 300_000;
|
|
67
|
+
const SANDBOX_TIMEOUT_BUFFER_MS = 10 * 60_000;
|
|
68
|
+
const SUBJECT_PROVISION_BUDGET_MS = 30 * 60_000;
|
|
69
|
+
const DEFAULT_STATE_STEP_TIMEOUT_MS = 5 * 60_000;
|
|
70
|
+
const DEFAULT_PROBER_CADENCE_MS = 1000;
|
|
71
|
+
const DEFAULT_MISSION = "You are one of MANY users hitting a shared web application at the same time. The browser is already open at the app. Accomplish your role's task, then stop.";
|
|
72
|
+
function readPositiveInt(value, fallback) {
|
|
73
|
+
if (value === undefined)
|
|
74
|
+
return fallback;
|
|
75
|
+
const parsed = Number.parseInt(value, 10);
|
|
76
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
77
|
+
}
|
|
78
|
+
function makeRunId() {
|
|
79
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
80
|
+
return `concurrent-shared-world-${stamp}-${randomBytes(4).toString("hex")}`;
|
|
81
|
+
}
|
|
82
|
+
/** Extract the in-sandbox port from the (loopback) serve.url so getHost can expose it. */
|
|
83
|
+
function servePort(serveUrl) {
|
|
84
|
+
const url = new URL(serveUrl);
|
|
85
|
+
if (url.port)
|
|
86
|
+
return Number(url.port);
|
|
87
|
+
return url.protocol === "https:" ? 443 : 80;
|
|
88
|
+
}
|
|
89
|
+
/** Resolve an actor's seat URL against the harness-minted getHost base (entry is a same-origin
|
|
90
|
+
* relative path, validated at parse against serve.url). */
|
|
91
|
+
function resolveActorSeatUrl(baseUrl, entry) {
|
|
92
|
+
if (!entry)
|
|
93
|
+
return baseUrl;
|
|
94
|
+
try {
|
|
95
|
+
return new URL(entry, baseUrl).toString();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return baseUrl;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** A getHost URL must be TOKENLESS (no userinfo, no query — no authKey; invariant 1). */
|
|
102
|
+
function isTokenlessHost(value) {
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(value);
|
|
105
|
+
return url.username === "" && url.password === "" && url.search === "";
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** sha256-16 of a URL's ORIGIN — the publish-safe host identity persisted in the bundle (the raw
|
|
112
|
+
* getHost URL embeds the live sandbox id + matches the e2b-URL redaction, so it never lands raw). */
|
|
113
|
+
function hostOriginDigest(url) {
|
|
114
|
+
try {
|
|
115
|
+
return commandDigestOf(new URL(url).origin);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return commandDigestOf(url);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/** A public-safe, human-readable route label for the bundle (host redacted to a placeholder; the
|
|
122
|
+
* entry path kept). Never contains the raw getHost URL. */
|
|
123
|
+
function publicSafeRouteLabel(entry) {
|
|
124
|
+
return `[provisioned-subject]${entry ?? "/"}`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Build the ONE subject sandbox's provenance (invariant 5): clone (repo + optional commit) or
|
|
128
|
+
* local-tree (archiveSha256 + optional commit/dirty from the once-per-run host-packed archive -
|
|
129
|
+
* archiveSha256 IS the pin; there is only ONE archive, so no per-lane unanimity math applies,
|
|
130
|
+
* unlike the cua fan-out route). Used for both the in-progress and final bundle: the archive
|
|
131
|
+
* never changes mid-run (packed before any sandbox exists).
|
|
132
|
+
*/
|
|
133
|
+
function buildSubjectProvenance(args) {
|
|
134
|
+
if (args.localTreeRoute) {
|
|
135
|
+
return {
|
|
136
|
+
source: "local-tree",
|
|
137
|
+
...(args.localTreeArchive === undefined ? {} : { archiveSha256: args.localTreeArchive.archiveSha256 }),
|
|
138
|
+
...(args.subjectCommit === undefined ? {} : { commit: args.subjectCommit }),
|
|
139
|
+
...(args.localTreeArchive?.git === undefined ? {} : { dirty: args.localTreeArchive.git.dirty }),
|
|
140
|
+
envNames: args.subjectEnvNames,
|
|
141
|
+
state: args.state
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
source: "clone",
|
|
146
|
+
repo: args.publicRepo,
|
|
147
|
+
...(args.subjectCommit === undefined ? {} : { commit: args.subjectCommit }),
|
|
148
|
+
envNames: args.subjectEnvNames,
|
|
149
|
+
state: args.state
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function laneTaxonomyLabel(spec) {
|
|
153
|
+
const parts = [
|
|
154
|
+
spec.actorType ? `type:${spec.actorType}` : undefined,
|
|
155
|
+
spec.surface ? `surface:${spec.surface}` : undefined,
|
|
156
|
+
spec.caseGroup ? `case:${spec.caseGroup}` : undefined
|
|
157
|
+
].filter((part) => part !== undefined);
|
|
158
|
+
return parts.length > 0 ? ` (${parts.join(" / ")})` : "";
|
|
159
|
+
}
|
|
160
|
+
/** Build one actor lane's CuaLaneSpec from a roster role (per-actor device IS honored here — each
|
|
161
|
+
* actor has its OWN desktop, unlike the sequential one-sandbox PoC). */
|
|
162
|
+
function buildActorSpec(config, role, index) {
|
|
163
|
+
const mission = config.actors[0]?.mission ?? DEFAULT_MISSION;
|
|
164
|
+
const device = resolveLaneDevice(config, role);
|
|
165
|
+
const composed = composeLaneInstructions({
|
|
166
|
+
mission,
|
|
167
|
+
...(role.persona === undefined ? {} : { persona: role.persona }),
|
|
168
|
+
...(role.instruction === undefined ? {} : { instruction: role.instruction }),
|
|
169
|
+
device: { name: device.name, preset: device.preset }
|
|
170
|
+
});
|
|
171
|
+
const roleId = role.id ?? `role-${String(index + 1).padStart(2, "0")}`;
|
|
172
|
+
const streamId = `stream-${String(index + 1).padStart(3, "0")}`;
|
|
173
|
+
return {
|
|
174
|
+
laneId: roleId,
|
|
175
|
+
...(role.actorType === undefined ? {} : { actorType: role.actorType }),
|
|
176
|
+
...(role.surface === undefined ? {} : { surface: role.surface }),
|
|
177
|
+
...(role.caseGroup === undefined ? {} : { caseGroup: role.caseGroup }),
|
|
178
|
+
laneIndex: index,
|
|
179
|
+
simId: `sim-${String(index + 1).padStart(3, "0")}`,
|
|
180
|
+
streamId,
|
|
181
|
+
persona: composed.persona,
|
|
182
|
+
instructions: composed.instructions,
|
|
183
|
+
...((role.stopWhen ?? config.actors[0]?.stopWhen) === undefined ? {} : { stopWhen: (role.stopWhen ?? config.actors[0]?.stopWhen) }),
|
|
184
|
+
deviceName: device.name,
|
|
185
|
+
devicePreset: device.preset,
|
|
186
|
+
resolution: device.resolution,
|
|
187
|
+
screenshotDir: roleId,
|
|
188
|
+
traceArtifactPath: `actors/${streamId}.json`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function writeConcurrentRunArtifacts(bundle, preparedRunPaths) {
|
|
192
|
+
const runPaths = await validatePreparedRunArtifactPaths(preparedRunPaths);
|
|
193
|
+
const publicBundle = {
|
|
194
|
+
...bundle,
|
|
195
|
+
cwd: PUBLIC_TARGET_CWD
|
|
196
|
+
};
|
|
197
|
+
await writeContainedOutputFile(runPaths, "run.json", `${JSON.stringify(publicBundle, null, 2)}\n`, "utf8");
|
|
198
|
+
await writeContainedOutputFile(runPaths, "review.json", `${JSON.stringify(publicBundle.review, null, 2)}\n`, "utf8");
|
|
199
|
+
await writeContainedOutputFile(runPaths, "review.md", renderConcurrentReviewMarkdown(publicBundle), "utf8");
|
|
200
|
+
await writeContainedOutputFile(runPaths, "events.ndjson", `${publicBundle.events.map((event) => JSON.stringify(event)).join("\n")}\n`, "utf8");
|
|
201
|
+
await writeContainedOutputFile(runPaths, "observer/observer-data.json", `${JSON.stringify(buildObserverData(publicBundle), null, 2)}\n`, "utf8");
|
|
202
|
+
await writePreparedRunLatestPointer(runPaths, `${JSON.stringify({
|
|
203
|
+
schema: "humanish.latest-run.v1",
|
|
204
|
+
runId: publicBundle.runId,
|
|
205
|
+
path: runPaths.relativeRunRoot,
|
|
206
|
+
updatedAt: new Date().toISOString()
|
|
207
|
+
}, null, 2)}\n`, "utf8");
|
|
208
|
+
}
|
|
209
|
+
function observerResultForConcurrentArtifacts(cwd, runId, artifactRoot, warnings = []) {
|
|
210
|
+
const observerPath = path.join(artifactRoot, "observer", "index.html");
|
|
211
|
+
const observerDataPath = path.join(artifactRoot, "observer", "observer-data.json");
|
|
212
|
+
const eventsPath = path.join(artifactRoot, "events.ndjson");
|
|
213
|
+
return {
|
|
214
|
+
schema: "humanish.observer-result.v1",
|
|
215
|
+
ok: true,
|
|
216
|
+
cwd,
|
|
217
|
+
run: runId,
|
|
218
|
+
observerPath: path.relative(cwd, observerPath),
|
|
219
|
+
observerDataPath: path.relative(cwd, observerDataPath),
|
|
220
|
+
eventsPath: path.relative(cwd, eventsPath),
|
|
221
|
+
observerUrl: pathToFileURL(observerPath).href,
|
|
222
|
+
bundlePath: path.join(artifactRoot, "run.json"),
|
|
223
|
+
opened: false,
|
|
224
|
+
warnings
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export async function runConcurrentSharedWorld(options) {
|
|
228
|
+
const { config, dryRun } = options;
|
|
229
|
+
const cwd = path.resolve(options.cwd);
|
|
230
|
+
const hooks = options.hooks ?? {};
|
|
231
|
+
const env = hooks.env ?? process.env;
|
|
232
|
+
const render = hooks.renderObserverFn ?? renderObserver;
|
|
233
|
+
const actorType = config.actors[0]?.type ?? "";
|
|
234
|
+
const concurrency = config.execution?.concurrency ?? 1;
|
|
235
|
+
const roles = config.actors[0]?.lanes ?? [];
|
|
236
|
+
const fail = (code, message, actorLabel) => ({
|
|
237
|
+
schema: CONCURRENT_SHARED_WORLD_LAB_SCHEMA,
|
|
238
|
+
ok: false,
|
|
239
|
+
cwd,
|
|
240
|
+
labId: config.id,
|
|
241
|
+
actor: actorLabel ?? actorType,
|
|
242
|
+
topology: "shared-world",
|
|
243
|
+
topologyMode: "concurrent",
|
|
244
|
+
roleCount: roles.length,
|
|
245
|
+
concurrency,
|
|
246
|
+
dryRun,
|
|
247
|
+
runId: options.runId ?? "not-created",
|
|
248
|
+
roles: [],
|
|
249
|
+
warnings: [],
|
|
250
|
+
error: { code, message }
|
|
251
|
+
});
|
|
252
|
+
const descriptor = actorRegistry[actorType];
|
|
253
|
+
if (!descriptor || !isCuaActorDescriptor(descriptor)) {
|
|
254
|
+
return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_ACTOR_UNSUPPORTED", `actors[0].type "${actorType}" is not a registered computer-use actor.`);
|
|
255
|
+
}
|
|
256
|
+
// Re-enforce the concurrent cross-validation (library API surface).
|
|
257
|
+
const invalidReason = concurrentSharedWorldValidationReason(config);
|
|
258
|
+
if (invalidReason) {
|
|
259
|
+
return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_INVALID", invalidReason, descriptor.id);
|
|
260
|
+
}
|
|
261
|
+
const serve = config.subject.serve;
|
|
262
|
+
const localTreeRoute = config.subject.source === "local-tree";
|
|
263
|
+
const subjectRepo = config.subject.repos?.[0] ?? "";
|
|
264
|
+
const subjectEnvNames = config.subject.env ?? [];
|
|
265
|
+
const checkpoints = config.subject.state?.checkpoint ?? [];
|
|
266
|
+
const runSession = hooks.runSession ?? descriptor.runSession;
|
|
267
|
+
const openaiApiKey = env.OPENAI_API_KEY?.trim() ?? "";
|
|
268
|
+
const e2bApiKey = env.E2B_API_KEY?.trim() ?? "";
|
|
269
|
+
const knownSecretValues = [
|
|
270
|
+
openaiApiKey,
|
|
271
|
+
e2bApiKey,
|
|
272
|
+
...subjectEnvNames.map((name) => env[name] ?? ""),
|
|
273
|
+
...checkpoints.flatMap((probe) => probe.redact ?? [])
|
|
274
|
+
].filter((value) => value.length >= 4);
|
|
275
|
+
const scrubKnownValues = (text) => knownSecretValues.reduce((current, value) => current.split(value).join("[REDACTED_SECRET]"), text);
|
|
276
|
+
const redactRepoLabel = config.policies?.redactRepos ?? subjectEnvNames.includes("GITHUB_TOKEN");
|
|
277
|
+
const publicRepo = redactRepoLabel ? "repo-01" : subjectRepo;
|
|
278
|
+
const hasGithubToken = subjectEnvNames.includes("GITHUB_TOKEN");
|
|
279
|
+
if (!dryRun) {
|
|
280
|
+
const missingKeys = [
|
|
281
|
+
...(openaiApiKey ? [] : ["OPENAI_API_KEY"]),
|
|
282
|
+
...(e2bApiKey ? [] : ["E2B_API_KEY"])
|
|
283
|
+
];
|
|
284
|
+
if (missingKeys.length > 0) {
|
|
285
|
+
return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_KEYS_MISSING", `Live concurrent shared-world labs need ${missingKeys.join(" and ")} in the environment (pass via --env-file; values are never persisted).`, descriptor.id);
|
|
286
|
+
}
|
|
287
|
+
const missingSubjectEnv = subjectEnvNames.filter((name) => !env[name]?.trim());
|
|
288
|
+
if (missingSubjectEnv.length > 0) {
|
|
289
|
+
return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_SUBJECT_ENV_MISSING", `subject.env declares ${missingSubjectEnv.join(", ")} but the environment does not provide ${missingSubjectEnv.length === 1 ? "it" : "them"} (pass via --env-file; values are never persisted).`, descriptor.id);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const runId = options.runId ?? makeRunId();
|
|
293
|
+
const runPaths = await prepareRunArtifactPaths(cwd, runId);
|
|
294
|
+
const artifactRoot = runPaths.absoluteRunRoot;
|
|
295
|
+
const physicalArtifactRoot = runPaths.physicalRunRoot;
|
|
296
|
+
const createdAt = new Date().toISOString();
|
|
297
|
+
const timeoutMs = config.execution?.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS;
|
|
298
|
+
const requestTimeoutMs = readPositiveInt(env.HUMANISH_E2B_REQUEST_TIMEOUT_MS, 60_000);
|
|
299
|
+
const redactScreenshots = config.policies?.redactScreenshots === true;
|
|
300
|
+
const timers = hooks.detachedTimers ?? {};
|
|
301
|
+
const now = hooks.now ?? Date.now;
|
|
302
|
+
const proberCadenceMs = hooks.proberCadenceMs ?? DEFAULT_PROBER_CADENCE_MS;
|
|
303
|
+
const seedDigest = seedRecipeDigest(config);
|
|
304
|
+
const source = await buildRunSource({ capturedAt: createdAt, cwd, humanishSource: "present", packageName: "humanish" });
|
|
305
|
+
const warnings = [];
|
|
306
|
+
const stateStepRecords = [];
|
|
307
|
+
const stateSnapshots = [];
|
|
308
|
+
const actorSpecs = roles.map((role, i) => buildActorSpec(config, role, i));
|
|
309
|
+
let actorResults = [];
|
|
310
|
+
let subjectCommit;
|
|
311
|
+
let subjectSandboxId;
|
|
312
|
+
let subjectKilled = false;
|
|
313
|
+
let getHostUrl;
|
|
314
|
+
let runError;
|
|
315
|
+
let snapshotIndex = 0;
|
|
316
|
+
let liveObserver;
|
|
317
|
+
const runtimeStreamUrls = [];
|
|
318
|
+
// Pack the working tree ONCE per run, on the host, BEFORE the subject sandbox is created
|
|
319
|
+
// (mirrors the sequential route + the cua route's ordering): a packing failure fails the run
|
|
320
|
+
// closed here, never spending sandbox cost. Dry-run packs nothing.
|
|
321
|
+
let localTreeArchive;
|
|
322
|
+
let localTreeArchiveBuffer;
|
|
323
|
+
if (localTreeRoute && !dryRun) {
|
|
324
|
+
const packLocalTree = hooks.packLocalTree ?? defaultPackLocalTree;
|
|
325
|
+
try {
|
|
326
|
+
const packed = await packLocalTree({
|
|
327
|
+
root: cwd,
|
|
328
|
+
...(config.subject.localTree?.exclude === undefined ? {} : { extraExclude: config.subject.localTree.exclude }),
|
|
329
|
+
...(config.subject.localTree?.maxArchiveBytes === undefined ? {} : { maxArchiveBytes: config.subject.localTree.maxArchiveBytes })
|
|
330
|
+
});
|
|
331
|
+
localTreeArchive = packed.archive;
|
|
332
|
+
localTreeArchiveBuffer = packed.buffer;
|
|
333
|
+
process.stderr.write(`humanish concurrent shared-world local-tree: packed ${packed.archive.fileCount} entries, ${packed.archive.totalBytes} bytes, archiveSha256 ${packed.archive.archiveSha256}`
|
|
334
|
+
+ `${packed.archive.git ? ` (commit ${packed.archive.git.commit.slice(0, 12)}, ${packed.archive.git.dirty ? "dirty" : "clean"} working tree)` : " (not a git work tree)"}\n`);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
return fail("HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", `local-tree packing failed: ${redactText(scrubKnownValues(toErrorMessage(error)))}`, descriptor.id);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!dryRun) {
|
|
341
|
+
let subjectModule;
|
|
342
|
+
let subjectDesktop;
|
|
343
|
+
// Background prober dispose signal (FIX-9: cleared in finally).
|
|
344
|
+
let proberDisposed = false;
|
|
345
|
+
let releaseDispose = () => { };
|
|
346
|
+
const disposeSignal = new Promise((resolve) => { releaseDispose = resolve; });
|
|
347
|
+
let proberLoop;
|
|
348
|
+
const proberSnapshot = async () => {
|
|
349
|
+
if (!subjectDesktop)
|
|
350
|
+
return;
|
|
351
|
+
const timestamp = now();
|
|
352
|
+
const idx = snapshotIndex;
|
|
353
|
+
snapshotIndex += 1;
|
|
354
|
+
const snapshot = await runCheckpointSnapshot({
|
|
355
|
+
desktop: subjectDesktop,
|
|
356
|
+
snapshotIndex: idx,
|
|
357
|
+
name: `state-${idx}`,
|
|
358
|
+
checkpoints,
|
|
359
|
+
prevDigest: undefined,
|
|
360
|
+
scrub: scrubKnownValues,
|
|
361
|
+
requestTimeoutMs,
|
|
362
|
+
timers
|
|
363
|
+
});
|
|
364
|
+
stateSnapshots.push({ timestamp, digest: snapshot.digest });
|
|
365
|
+
};
|
|
366
|
+
try {
|
|
367
|
+
subjectModule = await (hooks.loadDesktopModule ?? loadE2BDesktopModule)();
|
|
368
|
+
// The ONE subject sandbox: headless service host (no GUI seat). The SUBJECT env is provisioned
|
|
369
|
+
// HERE; the actor sandboxes get NONE of it (FIX-10). A custom desktop template (image) is
|
|
370
|
+
// honored on BOTH the subject sandbox (here) and every actor sandbox (via runCuaLane, which
|
|
371
|
+
// reads the same config); absent keeps the byte-stable Sandbox.create(opts) default.
|
|
372
|
+
subjectDesktop = await createDesktopSandbox(subjectModule, {
|
|
373
|
+
apiKey: e2bApiKey,
|
|
374
|
+
requestTimeoutMs,
|
|
375
|
+
timeoutMs: timeoutMs + SUBJECT_PROVISION_BUDGET_MS
|
|
376
|
+
+ (config.subject.state?.seed ?? []).reduce((sum, step) => sum + (step.timeoutMs ?? DEFAULT_STATE_STEP_TIMEOUT_MS), 0)
|
|
377
|
+
+ SANDBOX_TIMEOUT_BUFFER_MS,
|
|
378
|
+
metadata: {
|
|
379
|
+
...CONCURRENT_SHARED_WORLD_PROVIDER_METADATA,
|
|
380
|
+
labId: config.id,
|
|
381
|
+
topology: "shared-world",
|
|
382
|
+
topologyMode: "concurrent",
|
|
383
|
+
role: "subject",
|
|
384
|
+
roleCount: String(roles.length)
|
|
385
|
+
},
|
|
386
|
+
...(subjectEnvNames.length > 0
|
|
387
|
+
? { envs: Object.fromEntries(subjectEnvNames.map((name) => [name, env[name]])) }
|
|
388
|
+
: {}),
|
|
389
|
+
dpi: 96,
|
|
390
|
+
lifecycle: { onTimeout: "kill" }
|
|
391
|
+
}, config.execution?.desktop?.template);
|
|
392
|
+
subjectSandboxId = subjectDesktop.sandboxId;
|
|
393
|
+
if (hooks.prepareDesktop) {
|
|
394
|
+
await hooks.prepareDesktop(subjectDesktop);
|
|
395
|
+
}
|
|
396
|
+
// Provision the ONE shared plane: clone + install/build + seed + serve on 0.0.0.0 + probe
|
|
397
|
+
// (clone route), or upload/extract the once-per-run packed archive + the SAME shared serve
|
|
398
|
+
// pipeline (local-tree route).
|
|
399
|
+
const onSubjectPhase = hooks.onPhase ?? ((event) => {
|
|
400
|
+
process.stderr.write(`humanish shared-world (concurrent): ${event.message}${event.durationMs === undefined ? "" : ` (${event.durationMs}ms)`}\n`);
|
|
401
|
+
});
|
|
402
|
+
if (localTreeRoute) {
|
|
403
|
+
await provisionLocalTreeSubject(subjectDesktop, {
|
|
404
|
+
archiveBuffer: localTreeArchiveBuffer,
|
|
405
|
+
serve,
|
|
406
|
+
...(config.subject.state === undefined ? {} : { state: config.subject.state }),
|
|
407
|
+
requestTimeoutMs,
|
|
408
|
+
scrub: scrubKnownValues,
|
|
409
|
+
onStateStep: (record) => { stateStepRecords.push(record); },
|
|
410
|
+
onPhase: onSubjectPhase,
|
|
411
|
+
...timers
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
subjectCommit = await provisionCloneSubject(subjectDesktop, {
|
|
416
|
+
repo: subjectRepo,
|
|
417
|
+
depth: config.subject.clone?.depth ?? 1,
|
|
418
|
+
serve,
|
|
419
|
+
...(config.subject.state === undefined ? {} : { state: config.subject.state }),
|
|
420
|
+
hasGithubToken,
|
|
421
|
+
requestTimeoutMs,
|
|
422
|
+
scrub: scrubKnownValues,
|
|
423
|
+
onCommit: (commit) => { subjectCommit = commit; },
|
|
424
|
+
onStateStep: (record) => { stateStepRecords.push(record); },
|
|
425
|
+
onPhase: onSubjectPhase,
|
|
426
|
+
...timers
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
// Expose the served port via getHost (FIX-2). Fail closed if the SDK lacks it.
|
|
430
|
+
if (typeof subjectDesktop.getHost !== "function") {
|
|
431
|
+
throw new Error("the installed @e2b/desktop SDK does not expose getHost(port); the concurrent shared-world route requires it to reach the subject plane");
|
|
432
|
+
}
|
|
433
|
+
// getHost returns a BARE host (e.g. "3000-<sandboxId>.e2b.app", no scheme); e2b exposes the
|
|
434
|
+
// port over https. Normalize to a full URL before the tokenless check + before persisting.
|
|
435
|
+
const rawHost = subjectDesktop.getHost(servePort(serve.url));
|
|
436
|
+
const hostUrl = /^https?:\/\//i.test(rawHost) ? rawHost : `https://${rawHost}`;
|
|
437
|
+
if (!isTokenlessHost(hostUrl)) {
|
|
438
|
+
throw new Error("getHost returned a non-tokenless URL; refusing to persist a host URL that may carry a credential (invariant 1)");
|
|
439
|
+
}
|
|
440
|
+
getHostUrl = hostUrl;
|
|
441
|
+
// Baseline state snapshot, then start the background cadence prober.
|
|
442
|
+
await proberSnapshot();
|
|
443
|
+
if (options.onObserverReady) {
|
|
444
|
+
const inProgressPlaneCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
|
|
445
|
+
const inProgressSubject = buildSubjectProvenance({
|
|
446
|
+
localTreeRoute,
|
|
447
|
+
publicRepo,
|
|
448
|
+
subjectCommit: inProgressPlaneCommit,
|
|
449
|
+
localTreeArchive,
|
|
450
|
+
subjectEnvNames,
|
|
451
|
+
state: resolveSubjectState({ declared: config.subject.state, dryRun: false, executed: stateStepRecords })
|
|
452
|
+
});
|
|
453
|
+
const inProgressBundle = buildConcurrentSharedWorldBundle({
|
|
454
|
+
config,
|
|
455
|
+
descriptor,
|
|
456
|
+
createdAt,
|
|
457
|
+
dryRun: false,
|
|
458
|
+
inProgress: true,
|
|
459
|
+
runId,
|
|
460
|
+
source,
|
|
461
|
+
roles,
|
|
462
|
+
actorSpecs,
|
|
463
|
+
actorResults: [],
|
|
464
|
+
stateSnapshots,
|
|
465
|
+
subject: inProgressSubject,
|
|
466
|
+
seedDigest,
|
|
467
|
+
...(inProgressPlaneCommit === undefined ? {} : { subjectCommit: inProgressPlaneCommit }),
|
|
468
|
+
hostDigest: hostOriginDigest(getHostUrl)
|
|
469
|
+
});
|
|
470
|
+
await writeConcurrentRunArtifacts(inProgressBundle, runPaths);
|
|
471
|
+
liveObserver = observerResultForConcurrentArtifacts(cwd, runId, artifactRoot, [
|
|
472
|
+
"Live concurrent shared-world Observer is attached before final verification; stream auth URLs are runtime-only and are not persisted."
|
|
473
|
+
]);
|
|
474
|
+
await options.onObserverReady(liveObserver);
|
|
475
|
+
}
|
|
476
|
+
proberLoop = (async () => {
|
|
477
|
+
while (!proberDisposed) {
|
|
478
|
+
let timer;
|
|
479
|
+
await Promise.race([
|
|
480
|
+
new Promise((resolve) => { timer = setTimeout(resolve, proberCadenceMs); }),
|
|
481
|
+
disposeSignal
|
|
482
|
+
]);
|
|
483
|
+
if (timer)
|
|
484
|
+
clearTimeout(timer); // FIX-9: no dangling prober timer.
|
|
485
|
+
if (proberDisposed)
|
|
486
|
+
break;
|
|
487
|
+
await proberSnapshot().catch(() => undefined);
|
|
488
|
+
}
|
|
489
|
+
})();
|
|
490
|
+
// Launch N actor sandboxes CONCURRENTLY, INDEPENDENT (FIX-11: runCuaLane + mapWithConcurrency,
|
|
491
|
+
// NOT runCuaLanes — no pipeline gate / fail-fast). Each actor's window is measured on the ONE
|
|
492
|
+
// orchestrator clock (FIX-1). cloneRoute=false + subjectEnvNames=[] keep subject creds out of
|
|
493
|
+
// every actor sandbox (FIX-10).
|
|
494
|
+
const cuaHooks = {
|
|
495
|
+
...(hooks.loadDesktopModule ? { loadDesktopModule: hooks.loadDesktopModule } : {}),
|
|
496
|
+
...(hooks.detachedTimers ? { detachedTimers: hooks.detachedTimers } : {}),
|
|
497
|
+
...(hooks.env ? { env: hooks.env } : {}),
|
|
498
|
+
...(hooks.prepareDesktop ? { prepareDesktop: (desktop) => hooks.prepareDesktop(desktop) } : {}),
|
|
499
|
+
onRuntimeStreamReady: (stream) => {
|
|
500
|
+
runtimeStreamUrls.push({ streamId: stream.streamId, url: stream.url });
|
|
501
|
+
if (liveObserver) {
|
|
502
|
+
attachObserverRuntimeStreamUrls(liveObserver, runtimeStreamUrls);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
const baseActorDeps = {
|
|
507
|
+
config,
|
|
508
|
+
descriptor,
|
|
509
|
+
cloneRoute: false,
|
|
510
|
+
subjectEnvNames: [],
|
|
511
|
+
hasGithubToken: false,
|
|
512
|
+
env,
|
|
513
|
+
openaiApiKey,
|
|
514
|
+
e2bApiKey,
|
|
515
|
+
requestTimeoutMs,
|
|
516
|
+
perLaneSandboxMs: timeoutMs + SANDBOX_TIMEOUT_BUFFER_MS,
|
|
517
|
+
timeoutMs,
|
|
518
|
+
laneCount: roles.length,
|
|
519
|
+
artifactRoot: runPaths,
|
|
520
|
+
redactScreenshots,
|
|
521
|
+
scrubKnownValues,
|
|
522
|
+
runSession,
|
|
523
|
+
hooks: cuaHooks
|
|
524
|
+
};
|
|
525
|
+
actorResults = await mapWithConcurrency(actorSpecs, Math.max(1, concurrency), async (spec, i) => {
|
|
526
|
+
const route = resolveActorSeatUrl(getHostUrl, roles[i]?.entry);
|
|
527
|
+
const startedAt = now();
|
|
528
|
+
const outcome = await runCuaLane(spec, { ...baseActorDeps, appUrl: route });
|
|
529
|
+
const endedAt = now();
|
|
530
|
+
return { spec, outcome, startedAt, endedAt, route };
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
runError = redactText(scrubKnownValues(toErrorMessage(error)));
|
|
535
|
+
warnings.push(`Concurrent shared-world run failed before completion: ${runError}`);
|
|
536
|
+
}
|
|
537
|
+
finally {
|
|
538
|
+
// FIX-9: stop the prober, take a final snapshot while the subject is still alive, then tear
|
|
539
|
+
// down the ONE subject sandbox BY id (the actor sandboxes are torn down inside runCuaLane).
|
|
540
|
+
proberDisposed = true;
|
|
541
|
+
releaseDispose();
|
|
542
|
+
if (proberLoop) {
|
|
543
|
+
await proberLoop.catch(() => undefined);
|
|
544
|
+
}
|
|
545
|
+
if (subjectDesktop && getHostUrl) {
|
|
546
|
+
await proberSnapshot().catch(() => undefined);
|
|
547
|
+
}
|
|
548
|
+
if (subjectDesktop && subjectModule) {
|
|
549
|
+
if (typeof subjectModule.Sandbox.kill === "function") {
|
|
550
|
+
try {
|
|
551
|
+
await subjectModule.Sandbox.kill(subjectDesktop.sandboxId, { requestTimeoutMs: 60_000 });
|
|
552
|
+
subjectKilled = true;
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
warnings.push(`Subject sandbox teardown failed (server-side kill-on-timeout will reclaim it): ${redactText(scrubKnownValues(toErrorMessage(error)))}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
warnings.push("Installed @e2b/desktop SDK does not expose Sandbox.kill; server-side kill-on-timeout will reclaim the subject sandbox.");
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const subjectState = resolveSubjectState({ declared: config.subject.state, dryRun, executed: stateStepRecords });
|
|
565
|
+
const planeCommit = localTreeRoute ? localTreeArchive?.git?.commit : subjectCommit;
|
|
566
|
+
const subject = buildSubjectProvenance({
|
|
567
|
+
localTreeRoute,
|
|
568
|
+
publicRepo,
|
|
569
|
+
subjectCommit: planeCommit,
|
|
570
|
+
localTreeArchive,
|
|
571
|
+
subjectEnvNames,
|
|
572
|
+
state: subjectState
|
|
573
|
+
});
|
|
574
|
+
// Collect per-actor warnings (each lane's own teardown/raw-screenshot notes).
|
|
575
|
+
for (const result of actorResults) {
|
|
576
|
+
warnings.push(...result.outcome.warnings);
|
|
577
|
+
}
|
|
578
|
+
const bundle = buildConcurrentSharedWorldBundle({
|
|
579
|
+
config,
|
|
580
|
+
descriptor,
|
|
581
|
+
createdAt,
|
|
582
|
+
dryRun,
|
|
583
|
+
runId,
|
|
584
|
+
source,
|
|
585
|
+
roles,
|
|
586
|
+
actorSpecs,
|
|
587
|
+
actorResults,
|
|
588
|
+
stateSnapshots,
|
|
589
|
+
subject,
|
|
590
|
+
seedDigest,
|
|
591
|
+
...(planeCommit === undefined ? {} : { subjectCommit: planeCommit }),
|
|
592
|
+
...(getHostUrl === undefined ? {} : { hostDigest: hostOriginDigest(getHostUrl) }),
|
|
593
|
+
...(runError === undefined ? {} : { runError })
|
|
594
|
+
});
|
|
595
|
+
const adapterWarnings = [];
|
|
596
|
+
await applyBrowserAdapterHooks({
|
|
597
|
+
hooks,
|
|
598
|
+
bundle,
|
|
599
|
+
context: {
|
|
600
|
+
bundle,
|
|
601
|
+
runDir: physicalArtifactRoot,
|
|
602
|
+
labId: config.id,
|
|
603
|
+
runId,
|
|
604
|
+
actor: descriptor.id,
|
|
605
|
+
backend: "concurrent-shared-world",
|
|
606
|
+
dryRun,
|
|
607
|
+
laneCount: roles.length
|
|
608
|
+
},
|
|
609
|
+
sanitize: (text) => redactText(scrubKnownValues(text)),
|
|
610
|
+
warnings: adapterWarnings,
|
|
611
|
+
hookLabel: "sharedWorldHooks"
|
|
612
|
+
});
|
|
613
|
+
await writeConcurrentRunArtifacts(bundle, runPaths);
|
|
614
|
+
const observer = await render(cwd, runId, { open: options.open === true });
|
|
615
|
+
if (observer.ok && liveObserver) {
|
|
616
|
+
attachObserverRuntimeStreamUrls(observer, runtimeStreamUrls);
|
|
617
|
+
}
|
|
618
|
+
const roleOk = (result) => {
|
|
619
|
+
if (dryRun)
|
|
620
|
+
return true;
|
|
621
|
+
return actorLanePassed(result);
|
|
622
|
+
};
|
|
623
|
+
// Concurrent "ok": every actor must produce a terminal, engaged PASSED session. Per-persona
|
|
624
|
+
// mission success is still the "M of N" headline, but a failed actor trace cannot make the
|
|
625
|
+
// route green just because the harness got a terminal.
|
|
626
|
+
const swarmRan = !dryRun && actorResults.length === roles.length
|
|
627
|
+
&& actorResults.every(actorLanePassed);
|
|
628
|
+
const adapterFailure = adapterScoreFailureMessage(bundle);
|
|
629
|
+
const ok = observer.ok && runError === undefined && (dryRun || swarmRan) && adapterFailure === undefined;
|
|
630
|
+
const overlapProven = !dryRun && actorWindowsOverlap(actorResults);
|
|
631
|
+
const roleResults = actorSpecs.map((spec, index) => {
|
|
632
|
+
const result = actorResults[index];
|
|
633
|
+
const base = { id: spec.laneId, index: index + 1, persona: spec.persona.id };
|
|
634
|
+
if (dryRun || !result) {
|
|
635
|
+
return { ...base, status: "contract_proof_only", ok: dryRun };
|
|
636
|
+
}
|
|
637
|
+
const session = result.outcome.session;
|
|
638
|
+
const thisOk = roleOk(result);
|
|
639
|
+
return {
|
|
640
|
+
...base,
|
|
641
|
+
status: session ? session.status : "failed",
|
|
642
|
+
ok: thisOk,
|
|
643
|
+
window: { startedAt: result.startedAt, endedAt: result.endedAt },
|
|
644
|
+
...(session
|
|
645
|
+
? { session: { status: session.status, completionReason: session.completionReason, reason: session.reason, screenshots: result.outcome.screenshots.length } }
|
|
646
|
+
: {}),
|
|
647
|
+
...(result.outcome.sandboxId === undefined
|
|
648
|
+
? {}
|
|
649
|
+
: { sandbox: { sandboxId: result.outcome.sandboxId, killed: result.outcome.killed } }),
|
|
650
|
+
...(thisOk
|
|
651
|
+
? {}
|
|
652
|
+
: {
|
|
653
|
+
error: {
|
|
654
|
+
code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED",
|
|
655
|
+
message: result.outcome.sessionError
|
|
656
|
+
?? (result.outcome.noEngagement
|
|
657
|
+
? "Actor took no actions and produced no message (likely a blank/still-loading screen); not a credible goal_satisfied."
|
|
658
|
+
: result.outcome.selfReportedBlocker
|
|
659
|
+
? "Actor reported goal_satisfied while its final message described a blocker or asked for missing instructions; not a credible pass."
|
|
660
|
+
: session?.completionReason === "harness_error"
|
|
661
|
+
? `Actor seat ended with a harness error: ${session.reason}`
|
|
662
|
+
: "Actor did not produce a terminal session.")
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
};
|
|
666
|
+
});
|
|
667
|
+
const errorResult = (() => {
|
|
668
|
+
if (ok)
|
|
669
|
+
return undefined;
|
|
670
|
+
if (!observer.ok) {
|
|
671
|
+
return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: observer.error?.message ?? "Observer failed for the concurrent shared-world run." };
|
|
672
|
+
}
|
|
673
|
+
if (runError) {
|
|
674
|
+
return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: runError };
|
|
675
|
+
}
|
|
676
|
+
if (adapterFailure !== undefined) {
|
|
677
|
+
return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: adapterFailure };
|
|
678
|
+
}
|
|
679
|
+
const passed = roleResults.filter((role) => role.ok).length;
|
|
680
|
+
return { code: "HUMANISH_CONCURRENT_SHARED_WORLD_LAB_FAILED", message: `Concurrent shared-world run did not run coherently: ${passed}/${roles.length} actor(s) reached a terminal, engaged passed session.` };
|
|
681
|
+
})();
|
|
682
|
+
return {
|
|
683
|
+
schema: CONCURRENT_SHARED_WORLD_LAB_SCHEMA,
|
|
684
|
+
ok,
|
|
685
|
+
cwd,
|
|
686
|
+
labId: config.id,
|
|
687
|
+
actor: descriptor.id,
|
|
688
|
+
topology: "shared-world",
|
|
689
|
+
topologyMode: "concurrent",
|
|
690
|
+
roleCount: roles.length,
|
|
691
|
+
concurrency,
|
|
692
|
+
dryRun,
|
|
693
|
+
runId,
|
|
694
|
+
...(getHostUrl === undefined ? {} : { host: getHostUrl }),
|
|
695
|
+
...(subjectSandboxId === undefined ? {} : { subjectSandbox: { sandboxId: subjectSandboxId, killed: subjectKilled } }),
|
|
696
|
+
...(dryRun ? {} : { overlapProven }),
|
|
697
|
+
subject,
|
|
698
|
+
roles: roleResults,
|
|
699
|
+
observer,
|
|
700
|
+
warnings: [...warnings, ...adapterWarnings, ...observer.warnings],
|
|
701
|
+
...(errorResult === undefined ? {} : { error: errorResult })
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
/** True when ≥2 actor windows overlap in time (the proven-concurrency signal). */
|
|
705
|
+
function actorWindowsOverlap(results) {
|
|
706
|
+
for (let i = 0; i < results.length; i += 1) {
|
|
707
|
+
for (let j = i + 1; j < results.length; j += 1) {
|
|
708
|
+
const a = results[i];
|
|
709
|
+
const b = results[j];
|
|
710
|
+
if (a.startedAt < b.endedAt && b.startedAt < a.endedAt) {
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
}
|
|
717
|
+
function actorLanePassed(result) {
|
|
718
|
+
if (!result)
|
|
719
|
+
return false;
|
|
720
|
+
const session = result.outcome.session;
|
|
721
|
+
return session !== undefined
|
|
722
|
+
&& session.status === "passed"
|
|
723
|
+
&& session.completionReason !== "harness_error"
|
|
724
|
+
&& result.outcome.sessionError === undefined
|
|
725
|
+
&& !result.outcome.noEngagement
|
|
726
|
+
&& !result.outcome.selfReportedBlocker;
|
|
727
|
+
}
|
|
728
|
+
/** Project the concurrent run into a humanish.run-bundle.v1 with the CONCURRENT shared-world block. */
|
|
729
|
+
export function buildConcurrentSharedWorldBundle(args) {
|
|
730
|
+
const { config, descriptor, createdAt, dryRun, actorSpecs, actorResults, roles } = args;
|
|
731
|
+
const inProgress = args.inProgress === true;
|
|
732
|
+
const simulations = [];
|
|
733
|
+
const streams = [];
|
|
734
|
+
const events = [];
|
|
735
|
+
// Public-safe label only — the raw getHost URL never lands in the bundle (it embeds the live
|
|
736
|
+
// sandbox id + matches the e2b-URL redaction). The host identity is carried by plane.hostDigest.
|
|
737
|
+
const appUrl = "[provisioned-subject]";
|
|
738
|
+
const planeCommit = dryRun ? undefined : args.subjectCommit;
|
|
739
|
+
events.push({
|
|
740
|
+
id: "event-000-created",
|
|
741
|
+
at: createdAt,
|
|
742
|
+
level: "info",
|
|
743
|
+
type: "concurrent-shared-world.run.created",
|
|
744
|
+
message: `Created CONCURRENT shared-world run for ${config.id} (actor ${descriptor.id}, ${actorSpecs.length} persona(s) vs ONE shared plane, max ${config.execution?.concurrency ?? 1} concurrent).`
|
|
745
|
+
});
|
|
746
|
+
// Human-readable plane label, byte-stable for the clone route (see shared-world-lab.ts's
|
|
747
|
+
// buildSharedWorldBundle for the same pattern). local-tree has no repo slug: it labels the
|
|
748
|
+
// packed archive instead (archiveSha256 + dirty/clean when the packed root was a git work tree).
|
|
749
|
+
const dryRunPlaneLabel = args.subject.source === "local-tree"
|
|
750
|
+
? "packed working tree"
|
|
751
|
+
: `clone of ${args.subject.repo}`;
|
|
752
|
+
const livePlaneLabel = args.subject.source === "local-tree"
|
|
753
|
+
? (args.subject.archiveSha256
|
|
754
|
+
? `packed working tree (archiveSha256 ${args.subject.archiveSha256}${args.subject.dirty === true ? ", dirty working tree" : args.subject.dirty === false ? ", clean working tree" : ""})`
|
|
755
|
+
: "packed working tree (archive digest unresolved; provisioning failed before resolution)")
|
|
756
|
+
: `clone of ${args.subject.repo}${args.subjectCommit ? `@${args.subjectCommit}` : ""}`;
|
|
757
|
+
events.push({
|
|
758
|
+
id: "event-001-plane",
|
|
759
|
+
at: createdAt,
|
|
760
|
+
level: "info",
|
|
761
|
+
type: "concurrent-shared-world.plane.provenance",
|
|
762
|
+
message: dryRun
|
|
763
|
+
? `Shared plane declared: ${dryRunPlaneLabel}, served + getHost-exposed in-sandbox (dry-run contract; nothing ${args.subject.source === "local-tree" ? "packed" : "cloned"}). Seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`
|
|
764
|
+
: `Shared plane: ${livePlaneLabel}, served + exposed at the harness-minted getHost URL; seed recipe ${args.seedDigest}; SYNTHETIC subject (author-attested); env names: ${args.subject.envNames?.join(", ") || "none"} (values never persisted).`,
|
|
765
|
+
simId: actorSpecs[0]?.simId ?? "sim-001",
|
|
766
|
+
streamId: actorSpecs[0]?.streamId ?? "stream-001"
|
|
767
|
+
});
|
|
768
|
+
let eventSeq = 2;
|
|
769
|
+
const nextEventId = (suffix) => `event-${String(eventSeq++).padStart(3, "0")}-${suffix}`;
|
|
770
|
+
actorSpecs.forEach((spec, index) => {
|
|
771
|
+
const taxonomy = laneTaxonomyLabel(spec);
|
|
772
|
+
const result = actorResults[index];
|
|
773
|
+
const outcome = result?.outcome;
|
|
774
|
+
const session = outcome?.session;
|
|
775
|
+
const screenshots = outcome?.screenshots ?? [];
|
|
776
|
+
const lastScreenshot = screenshots[screenshots.length - 1];
|
|
777
|
+
const route = publicSafeRouteLabel(roles[index]?.entry); // public-safe (host redacted)
|
|
778
|
+
const status = session
|
|
779
|
+
? session.status
|
|
780
|
+
: outcome?.sessionError
|
|
781
|
+
? "failed"
|
|
782
|
+
: inProgress
|
|
783
|
+
? "running"
|
|
784
|
+
: "contract_proof_only";
|
|
785
|
+
const reason = session?.reason
|
|
786
|
+
?? outcome?.sessionError
|
|
787
|
+
?? (inProgress
|
|
788
|
+
? "Actor desktop is running; the attached Observer hydrates the runtime stream URL without persisting it."
|
|
789
|
+
: "Contract actor only: dry-run produced the evidence shape without launching a desktop or spending provider tokens.");
|
|
790
|
+
const traceScreenshotMode = session?.trace.redaction.screenshots;
|
|
791
|
+
const screenshotMode = traceScreenshotMode === "raw" || traceScreenshotMode === "blurred"
|
|
792
|
+
? traceScreenshotMode
|
|
793
|
+
: config.policies?.redactScreenshots === true ? "blurred" : "raw";
|
|
794
|
+
simulations.push({
|
|
795
|
+
id: spec.simId,
|
|
796
|
+
index: index + 1,
|
|
797
|
+
personaId: spec.persona.id,
|
|
798
|
+
scenarioId: `concurrent-shared-world-${config.id}`,
|
|
799
|
+
status,
|
|
800
|
+
streamKind: "browser",
|
|
801
|
+
mode: "browser-sim",
|
|
802
|
+
progress: inProgress ? 35 : 100,
|
|
803
|
+
currentStep: reason,
|
|
804
|
+
summary: session
|
|
805
|
+
? `Persona ${spec.laneId}${taxonomy} (${spec.persona.id}): drove the shared plane concurrently; ${session.completionReason}.`
|
|
806
|
+
: outcome?.sessionError
|
|
807
|
+
? `Persona ${spec.laneId}${taxonomy} failed before a terminal session verdict: ${outcome.sessionError}`
|
|
808
|
+
: inProgress
|
|
809
|
+
? `Persona ${spec.laneId}${taxonomy} (${spec.persona.id}) is running against the shared plane.`
|
|
810
|
+
: `Contract persona ${spec.laneId}${taxonomy} (${spec.persona.id}) for ${descriptor.id} against the shared plane at ${appUrl}.`,
|
|
811
|
+
streamIds: [spec.streamId],
|
|
812
|
+
startedAt: createdAt,
|
|
813
|
+
updatedAt: createdAt
|
|
814
|
+
});
|
|
815
|
+
streams.push({
|
|
816
|
+
id: spec.streamId,
|
|
817
|
+
simId: spec.simId,
|
|
818
|
+
kind: "browser",
|
|
819
|
+
label: `Concurrent persona ${spec.laneId}${taxonomy} — ${config.id}`,
|
|
820
|
+
status,
|
|
821
|
+
transport: "snapshot",
|
|
822
|
+
updatedAt: createdAt,
|
|
823
|
+
embed: lastScreenshot
|
|
824
|
+
? { kind: "screenshot", url: lastScreenshot, title: `Shared plane, persona ${spec.laneId} (${screenshotMode})` }
|
|
825
|
+
: { kind: "placeholder", title: `Shared plane, persona ${spec.laneId}` },
|
|
826
|
+
viewport: {
|
|
827
|
+
width: spec.resolution[0],
|
|
828
|
+
height: spec.resolution[1],
|
|
829
|
+
deviceScaleFactor: spec.devicePreset.deviceScaleFactor,
|
|
830
|
+
isMobile: spec.devicePreset.isMobile
|
|
831
|
+
},
|
|
832
|
+
ui: {
|
|
833
|
+
route,
|
|
834
|
+
intent: `Watch persona ${spec.laneId}${taxonomy} (${spec.persona.id}) drive the SHARED plane concurrently with the other personas.`,
|
|
835
|
+
state: reason,
|
|
836
|
+
...(session ? { actorStatus: session.status } : {}),
|
|
837
|
+
...(lastScreenshot ? { screenshotUrl: lastScreenshot } : {})
|
|
838
|
+
},
|
|
839
|
+
...(session ? { actor: session.trace } : {}),
|
|
840
|
+
artifacts: [
|
|
841
|
+
{ label: "run bundle", path: "run.json", kind: "bundle" },
|
|
842
|
+
{ label: "review", path: "review.md", kind: "review" },
|
|
843
|
+
{ label: "events", path: "events.ndjson", kind: "events" },
|
|
844
|
+
...(session
|
|
845
|
+
? [{ label: `persona ${spec.laneId} actor trace`, path: spec.traceArtifactPath, kind: "trace" }]
|
|
846
|
+
: []),
|
|
847
|
+
...screenshots.map((screenshot, screenshotIndex) => ({
|
|
848
|
+
label: `persona ${spec.laneId} screenshot ${String(screenshotIndex + 1).padStart(2, "0")} (${screenshotMode})`,
|
|
849
|
+
path: screenshot,
|
|
850
|
+
kind: "screenshot"
|
|
851
|
+
}))
|
|
852
|
+
]
|
|
853
|
+
});
|
|
854
|
+
for (const warning of outcome?.warnings ?? []) {
|
|
855
|
+
events.push({
|
|
856
|
+
id: nextEventId(`warning-${spec.laneId}`),
|
|
857
|
+
at: createdAt,
|
|
858
|
+
level: "warn",
|
|
859
|
+
type: "concurrent-shared-world.actor.warning",
|
|
860
|
+
message: `Persona ${spec.laneId}: ${warning}`,
|
|
861
|
+
simId: spec.simId,
|
|
862
|
+
streamId: spec.streamId
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
if (session) {
|
|
866
|
+
events.push({
|
|
867
|
+
id: nextEventId(`session-${spec.laneId}`),
|
|
868
|
+
at: createdAt,
|
|
869
|
+
level: session.status === "passed" ? "info" : "warn",
|
|
870
|
+
type: `concurrent-shared-world.session.${session.completionReason}`,
|
|
871
|
+
message: `Persona ${spec.laneId}: ${session.status} — ${session.reason}`,
|
|
872
|
+
simId: spec.simId,
|
|
873
|
+
streamId: spec.streamId
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
else if (outcome?.sessionError) {
|
|
877
|
+
events.push({
|
|
878
|
+
id: nextEventId(`session-error-${spec.laneId}`),
|
|
879
|
+
at: createdAt,
|
|
880
|
+
level: "error",
|
|
881
|
+
type: "concurrent-shared-world.session.error",
|
|
882
|
+
message: `Persona ${spec.laneId}: ${outcome.sessionError}`,
|
|
883
|
+
simId: spec.simId,
|
|
884
|
+
streamId: spec.streamId
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
else if (inProgress) {
|
|
888
|
+
events.push({
|
|
889
|
+
id: nextEventId(`running-${spec.laneId}`),
|
|
890
|
+
at: createdAt,
|
|
891
|
+
level: "info",
|
|
892
|
+
type: "actor.running",
|
|
893
|
+
message: `Persona ${spec.laneId}: desktop actor is running; live stream URL is runtime-only and not persisted.`,
|
|
894
|
+
simId: spec.simId,
|
|
895
|
+
streamId: spec.streamId
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
events.push({
|
|
900
|
+
id: nextEventId(`contract-${spec.laneId}`),
|
|
901
|
+
at: createdAt,
|
|
902
|
+
level: "info",
|
|
903
|
+
type: "concurrent-shared-world.contract.ready",
|
|
904
|
+
message: `Persona ${spec.laneId}: dry-run contract actor ready; switch scenario.mode to live for a real concurrent session.`,
|
|
905
|
+
simId: spec.simId,
|
|
906
|
+
streamId: spec.streamId
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
// Build the concurrent shared-world evidence block. routeHostDigest is sha256-16 of the ORIGIN of
|
|
911
|
+
// the getHost seat URL each actor drove (publish-safe; verify confirms it == plane.hostDigest).
|
|
912
|
+
const fallbackHostDigest = args.hostDigest ?? commandDigestOf("[provisioned-subject]");
|
|
913
|
+
const laneWindows = actorSpecs.map((spec, index) => {
|
|
914
|
+
const result = actorResults[index];
|
|
915
|
+
const session = result?.outcome.session;
|
|
916
|
+
const routeHostDigest = result ? hostOriginDigest(result.route) : fallbackHostDigest;
|
|
917
|
+
return {
|
|
918
|
+
roleId: spec.laneId,
|
|
919
|
+
...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
|
|
920
|
+
...(spec.surface === undefined ? {} : { surface: spec.surface }),
|
|
921
|
+
...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
|
|
922
|
+
simId: spec.simId,
|
|
923
|
+
streamId: spec.streamId,
|
|
924
|
+
startedAt: result?.startedAt ?? 0,
|
|
925
|
+
endedAt: result?.endedAt ?? 0,
|
|
926
|
+
verdict: session ? session.status : result?.outcome.sessionError ? "failed" : inProgress ? "running" : "contract_proof_only",
|
|
927
|
+
routeHostDigest,
|
|
928
|
+
...(planeCommit === undefined ? {} : { commit: planeCommit }),
|
|
929
|
+
seedDigest: args.seedDigest
|
|
930
|
+
};
|
|
931
|
+
});
|
|
932
|
+
const stateSeries = dryRun
|
|
933
|
+
? [{ timestamp: 0, digest: declaredStateDigest(config) }]
|
|
934
|
+
: [...args.stateSnapshots].sort((a, b) => a.timestamp - b.timestamp);
|
|
935
|
+
const outcomes = actorSpecs.map((spec, index) => {
|
|
936
|
+
const result = actorResults[index];
|
|
937
|
+
const session = result?.outcome.session;
|
|
938
|
+
const ok = !dryRun && actorLanePassed(result);
|
|
939
|
+
return {
|
|
940
|
+
roleId: spec.laneId,
|
|
941
|
+
...(spec.actorType === undefined ? {} : { actorType: spec.actorType }),
|
|
942
|
+
...(spec.surface === undefined ? {} : { surface: spec.surface }),
|
|
943
|
+
...(spec.caseGroup === undefined ? {} : { caseGroup: spec.caseGroup }),
|
|
944
|
+
simId: spec.simId,
|
|
945
|
+
streamId: spec.streamId,
|
|
946
|
+
status: session ? session.status : result?.outcome.sessionError ? "failed" : inProgress ? "running" : "contract_proof_only",
|
|
947
|
+
...(session ? { completionReason: session.completionReason } : {}),
|
|
948
|
+
ok
|
|
949
|
+
};
|
|
950
|
+
});
|
|
951
|
+
const sharedWorld = {
|
|
952
|
+
schema: SHARED_WORLD_SCHEMA,
|
|
953
|
+
topology: "shared-world",
|
|
954
|
+
topologyMode: "concurrent",
|
|
955
|
+
roleCount: actorSpecs.length,
|
|
956
|
+
plane: {
|
|
957
|
+
...(planeCommit === undefined ? {} : { commit: planeCommit }),
|
|
958
|
+
seedDigest: args.seedDigest,
|
|
959
|
+
envNames: args.subject.envNames ?? [],
|
|
960
|
+
...(args.hostDigest === undefined ? {} : { hostDigest: args.hostDigest }),
|
|
961
|
+
exposure: "synthetic"
|
|
962
|
+
},
|
|
963
|
+
attributionLimits: [...CONCURRENT_ATTRIBUTION_LIMITS],
|
|
964
|
+
laneWindows,
|
|
965
|
+
stateSeries,
|
|
966
|
+
outcomes
|
|
967
|
+
};
|
|
968
|
+
const overlaps = actorWindowsOverlap(actorResults);
|
|
969
|
+
const deltas = stateSeries.filter((snapshot, i) => i > 0 && snapshot.digest !== stateSeries[i - 1].digest).length;
|
|
970
|
+
events.push({
|
|
971
|
+
id: nextEventId("concurrency"),
|
|
972
|
+
at: createdAt,
|
|
973
|
+
level: "info",
|
|
974
|
+
type: "concurrent-shared-world.concurrency",
|
|
975
|
+
message: `Concurrency: ${laneWindows.length} actor window(s)${dryRun ? " (dry-run contract; $0)" : `, overlap ${overlaps ? "PROVEN" : "not observed"}`}; stateSeries ${stateSeries.length} snapshot(s), ${deltas} delta(s). Attribution ceiling: ${sharedWorld.attributionLimits.join(", ")}. CAPABILITY at scale is backed only by the deferred live receipt.`
|
|
976
|
+
});
|
|
977
|
+
// Concurrent verdict: dryRun → contract; else every actor produced a terminal, engaged PASSED
|
|
978
|
+
// session → pass; otherwise fail. Per-persona mission success is the M-of-N in outcomes[].
|
|
979
|
+
const verdict = dryRun
|
|
980
|
+
? "contract_proof_only"
|
|
981
|
+
: inProgress
|
|
982
|
+
? "contract_proof_only"
|
|
983
|
+
: (actorResults.length === actorSpecs.length
|
|
984
|
+
&& actorResults.every(actorLanePassed)
|
|
985
|
+
? "pass"
|
|
986
|
+
: "fail");
|
|
987
|
+
const passedMissions = outcomes.filter((outcome) => outcome.ok).length;
|
|
988
|
+
const review = {
|
|
989
|
+
schema: REVIEW_SCHEMA,
|
|
990
|
+
verdict,
|
|
991
|
+
summary: dryRun
|
|
992
|
+
? `Dry-run concurrent shared-world contract: ${actorSpecs.length} persona(s) declared against ONE getHost-exposed plane (${descriptor.id}); no sandboxes launched, $0 spend.`
|
|
993
|
+
: inProgress
|
|
994
|
+
? `In-progress concurrent shared-world Observer snapshot: ${actorSpecs.length} persona(s) running against ONE shared plane; final verification is pending.`
|
|
995
|
+
: `Concurrent shared-world (ONE plane, ${actorSpecs.length} simultaneous personas): swarm ${verdict === "pass" ? "ran coherently" : "did not run coherently"}; ${passedMissions}/${actorSpecs.length} reached their goal; overlap ${overlaps ? "proven" : "not observed"}; ${deltas} state delta(s) under load.`,
|
|
996
|
+
gaps: dryRun
|
|
997
|
+
? ["Live concurrent shared-world session not yet run (dry-run contract only); the concurrency capability at scale is backed only by the deferred live receipt."]
|
|
998
|
+
: inProgress
|
|
999
|
+
? ["Final actor traces, screenshots, state deltas, and verification are pending; this Observer is for live watch only."]
|
|
1000
|
+
: actorResults
|
|
1001
|
+
.filter((result) => result.outcome.sessionError !== undefined
|
|
1002
|
+
|| result.outcome.noEngagement
|
|
1003
|
+
|| result.outcome.selfReportedBlocker
|
|
1004
|
+
|| result.outcome.session === undefined
|
|
1005
|
+
|| result.outcome.session.status !== "passed")
|
|
1006
|
+
.map((result) => `${result.spec.laneId}: ${result.outcome.sessionError ?? result.outcome.session?.reason ?? "did not pass"}`)
|
|
1007
|
+
};
|
|
1008
|
+
const anyRaw = actorResults.some((result) => result.outcome.session?.trace.redaction.screenshots === "raw");
|
|
1009
|
+
const ranLive = actorResults.some((result) => result.outcome.session !== undefined || result.outcome.sessionError !== undefined);
|
|
1010
|
+
return {
|
|
1011
|
+
schema: RUN_BUNDLE_SCHEMA,
|
|
1012
|
+
runId: args.runId,
|
|
1013
|
+
mode: dryRun ? "dry-run" : "live",
|
|
1014
|
+
simCount: actorSpecs.length,
|
|
1015
|
+
createdAt,
|
|
1016
|
+
cwd: PUBLIC_TARGET_CWD,
|
|
1017
|
+
artifactRoot: path.join(".humanish", "runs", args.runId),
|
|
1018
|
+
source: args.source,
|
|
1019
|
+
persona: {
|
|
1020
|
+
id: actorSpecs[0]?.persona.id ?? "concurrent-persona",
|
|
1021
|
+
name: `Concurrent shared-world swarm (${actorSpecs.length} personas)`,
|
|
1022
|
+
source: `lab:${config.id}`,
|
|
1023
|
+
sourceDigest: actorSpecs[0]?.persona.promptDigest ?? args.seedDigest
|
|
1024
|
+
},
|
|
1025
|
+
scenario: {
|
|
1026
|
+
id: `concurrent-shared-world-${config.id}`,
|
|
1027
|
+
title: config.title ?? `Concurrent shared-world: ${config.id}`,
|
|
1028
|
+
goal: actorSpecs[0]?.instructions ?? "Concurrent shared-world interaction.",
|
|
1029
|
+
source: `lab:${config.id}`,
|
|
1030
|
+
sourceDigest: actorSpecs[0]?.persona.promptDigest ?? args.seedDigest
|
|
1031
|
+
},
|
|
1032
|
+
lifecycle: [
|
|
1033
|
+
{
|
|
1034
|
+
at: createdAt,
|
|
1035
|
+
event: "concurrent-shared-world.run.created",
|
|
1036
|
+
message: `Created concurrent shared-world run with ONE shared plane and ${actorSpecs.length} simultaneous actor seats (actor ${descriptor.id}).`
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
simulations,
|
|
1040
|
+
streams,
|
|
1041
|
+
events,
|
|
1042
|
+
redaction: {
|
|
1043
|
+
status: "passed",
|
|
1044
|
+
notes: ranLive
|
|
1045
|
+
? anyRaw
|
|
1046
|
+
? "Typed text recorded as length only and reasoning/messages pass through text redaction. Some personas captured FULL-FIDELITY (raw) screenshots, retained for local use — NOT redacted for publishing; set policies.redactScreenshots: true to blur a share-as-is bundle. stateSeries persists digest-only."
|
|
1047
|
+
: "Typed text recorded as length only and reasoning/messages pass through text redaction. Screenshots are blurred at capture (policies.redactScreenshots: true) for a share-as-is bundle. stateSeries persists digest-only."
|
|
1048
|
+
: inProgress
|
|
1049
|
+
? "In-progress live Observer snapshot: runtime stream auth URLs are process-local only and are not persisted. Final typed text, traces, and screenshots are pending. stateSeries persists digest-only."
|
|
1050
|
+
: "Dry-run concurrent shared-world contract bundle: no sandboxes launched and no screenshots captured. Typed text is recorded as length only and reasoning/messages pass through text redaction whenever a session runs. stateSeries persists digest-only."
|
|
1051
|
+
},
|
|
1052
|
+
artifacts: {
|
|
1053
|
+
run: "run.json",
|
|
1054
|
+
reviewJson: "review.json",
|
|
1055
|
+
reviewMarkdown: "review.md",
|
|
1056
|
+
observerData: "observer/observer-data.json",
|
|
1057
|
+
events: "events.ndjson"
|
|
1058
|
+
},
|
|
1059
|
+
review,
|
|
1060
|
+
feedbackCandidates: [],
|
|
1061
|
+
// Custom desktop image provenance (subject + every actor sandbox launched on it); omitted on the default.
|
|
1062
|
+
...(config.execution?.desktop?.template === undefined ? {} : { desktopTemplate: config.execution.desktop.template }),
|
|
1063
|
+
subject: args.subject,
|
|
1064
|
+
attributionClass: "shared-world",
|
|
1065
|
+
sharedWorld
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
/** The declared (dry-run) state digest: the probe RECIPE (command digests), no run. */
|
|
1069
|
+
function declaredStateDigest(config) {
|
|
1070
|
+
const probes = config.subject.state?.checkpoint ?? [];
|
|
1071
|
+
return combineCheckpointDigest(probes.map((probe) => `${probe.name}=${commandDigestOf(probe.command)}`));
|
|
1072
|
+
}
|
|
1073
|
+
function renderConcurrentReviewMarkdown(bundle) {
|
|
1074
|
+
const plane = bundle.events.find((event) => event.type === "concurrent-shared-world.plane.provenance");
|
|
1075
|
+
const concurrency = bundle.events.find((event) => event.type === "concurrent-shared-world.concurrency");
|
|
1076
|
+
const sw = bundle.sharedWorld;
|
|
1077
|
+
return [
|
|
1078
|
+
`# ${bundle.scenario.title}`,
|
|
1079
|
+
"",
|
|
1080
|
+
`- run: ${bundle.runId}`,
|
|
1081
|
+
`- mode: ${bundle.mode}`,
|
|
1082
|
+
`- attribution class: ${bundle.attributionClass ?? "isolated"}`,
|
|
1083
|
+
`- topology: ${sw?.topology ?? "(none)"} / ${sw?.topologyMode ?? "(none)"}`,
|
|
1084
|
+
`- personas: ${sw?.roleCount ?? 0}`,
|
|
1085
|
+
`- verdict: ${bundle.review.verdict}`,
|
|
1086
|
+
`- summary: ${bundle.review.summary}`,
|
|
1087
|
+
...(plane ? [`- plane: ${plane.message}`] : []),
|
|
1088
|
+
...(concurrency ? [`- concurrency: ${concurrency.message}`] : []),
|
|
1089
|
+
...(sw ? [`- attribution limits: ${sw.attributionLimits.join(", ")}`] : []),
|
|
1090
|
+
...(bundle.review.gaps.length > 0 ? ["", "## Gaps", ...bundle.review.gaps.map((gap) => `- ${gap}`)] : []),
|
|
1091
|
+
""
|
|
1092
|
+
].join("\n");
|
|
1093
|
+
}
|
|
1094
|
+
//# sourceMappingURL=concurrent-shared-world-lab.js.map
|