humanish 0.109.1 → 0.110.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/ARCHITECTURE.md +14 -11
- package/CHANGELOG.md +24 -0
- package/CONTEXT.md +8 -6
- package/README.md +97 -48
- package/dist/actors/computer-use/loop/ending.js +4 -4
- package/dist/actors/local-agent/claude-participant.d.ts +74 -0
- package/dist/actors/local-agent/claude-participant.js +243 -0
- package/dist/actors/local-agent/claude-session.d.ts +16 -3
- package/dist/actors/local-agent/claude-session.js +64 -23
- package/dist/actors/local-agent/cli.d.ts +11 -0
- package/dist/actors/local-agent/cli.js +103 -24
- package/dist/actors/local-agent/readiness.d.ts +2 -2
- package/dist/actors/local-agent/readiness.js +18 -11
- package/dist/cli/commands/analyze.js +21 -5
- package/dist/cli/commands/comms.js +25 -7
- package/dist/cli/commands/observe.js +4 -2
- package/dist/cli/commands/run-command.js +4 -3
- package/dist/cli/commands/run-signals.d.ts +2 -0
- package/dist/cli/commands/run-signals.js +96 -44
- package/dist/cli/commands/runs.d.ts +5 -0
- package/dist/cli/commands/runs.js +89 -67
- package/dist/cli/commands/setup.js +19 -5
- package/dist/cli/commands/study-format.js +29 -2
- package/dist/cli/commands/study-route-computer-use.js +2 -0
- package/dist/cli/commands/study-route-scripted.js +2 -0
- package/dist/cli/commands/study-route-shared-world.js +2 -0
- package/dist/cli/commands/study-route-terminal.js +2 -0
- package/dist/cli/commands/study.js +4 -3
- package/dist/cli/commands/tui.js +4 -3
- package/dist/cli/commands/watch.js +2 -2
- package/dist/cli/doctor.d.ts +7 -2
- package/dist/cli/doctor.js +47 -8
- package/dist/cli/findings.d.ts +103 -0
- package/dist/cli/findings.js +316 -0
- package/dist/cli/first-run-path.d.ts +5 -4
- package/dist/cli/first-run-path.js +14 -7
- package/dist/cli/io.d.ts +17 -3
- package/dist/cli/io.js +22 -4
- package/dist/cli/product-sentence.d.ts +3 -3
- package/dist/cli/product-sentence.js +3 -3
- package/dist/comms/receiving-runtime.js +1 -1
- package/dist/comms/setup.js +1 -1
- package/dist/evidence/redaction.d.ts +7 -3
- package/dist/evidence/redaction.js +68 -24
- package/dist/feedback/export-bundle.js +6 -1
- package/dist/feedback/export.js +3 -0
- package/dist/keys/env-file.d.ts +4 -0
- package/dist/keys/env-file.js +17 -0
- package/dist/keys/key-resolution.d.ts +7 -0
- package/dist/keys/key-resolution.js +10 -8
- package/dist/observer/data.d.ts +14 -1
- package/dist/observer/data.js +19 -9
- package/dist/observer/library.d.ts +4 -0
- package/dist/observer/library.js +18 -15
- package/dist/observer/render.js +8 -2
- package/dist/observer/run-routes.d.ts +2 -0
- package/dist/observer/run-routes.js +43 -34
- package/dist/observer-app.html +3 -3
- package/dist/routes/computer-use/bundle.js +3 -1
- package/dist/routes/computer-use/fanout-records.js +1 -1
- package/dist/routes/computer-use/live-phase.d.ts +1 -0
- package/dist/routes/computer-use/participant-execution.js +3 -1
- package/dist/routes/computer-use/participant-model.js +5 -2
- package/dist/routes/computer-use/plan.js +4 -4
- package/dist/routes/computer-use/preflight.js +2 -2
- package/dist/routes/computer-use/result.js +17 -28
- package/dist/routes/computer-use/single-bundle.js +7 -1
- package/dist/routes/computer-use/types.d.ts +6 -0
- package/dist/routes/scripted/bundle.js +4 -2
- package/dist/routes/scripted/plan.js +2 -2
- package/dist/routes/scripted/result.d.ts +3 -5
- package/dist/routes/scripted/result.js +16 -22
- package/dist/routes/scripted/scenario.js +4 -4
- package/dist/routes/scripted/types.d.ts +1 -0
- package/dist/routes/shared-world/bundle-records.js +6 -1
- package/dist/routes/shared-world/bundle.js +2 -0
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/shared-world/result.js +22 -22
- package/dist/routes/shared-world/route.js +1 -1
- package/dist/routes/shared-world/types.d.ts +2 -0
- package/dist/routes/terminal/bundle.js +4 -2
- package/dist/routes/terminal/credentials.js +1 -1
- package/dist/routes/terminal/dry-run.js +8 -12
- package/dist/routes/terminal/ledger.d.ts +3 -3
- package/dist/routes/terminal/ledger.js +6 -6
- package/dist/routes/terminal/live-finish.js +23 -17
- package/dist/routes/terminal/plan.js +4 -4
- package/dist/routes/terminal/result.d.ts +6 -7
- package/dist/routes/terminal/result.js +6 -22
- package/dist/routes/terminal/session.js +1 -1
- package/dist/routes/terminal/types.d.ts +3 -0
- package/dist/run/bundle-shape.d.ts +3 -1
- package/dist/run/bundle-shape.js +26 -1
- package/dist/run/bundle.d.ts +30 -0
- package/dist/run/display.d.ts +59 -0
- package/dist/run/display.js +106 -0
- package/dist/run/dry-run.js +11 -7
- package/dist/run/judge.d.ts +3 -2
- package/dist/run/judge.js +10 -0
- package/dist/run/pricing.js +1 -1
- package/dist/run/projection.d.ts +0 -2
- package/dist/run/projection.js +0 -11
- package/dist/run/reclaim-outcomes.d.ts +89 -0
- package/dist/run/reclaim-outcomes.js +213 -0
- package/dist/run/reclaim.d.ts +18 -14
- package/dist/run/reclaim.js +179 -105
- package/dist/run/results.d.ts +11 -18
- package/dist/run/results.js +2 -2
- package/dist/run/run-index.d.ts +11 -1
- package/dist/run/run-index.js +11 -0
- package/dist/run/run.d.ts +34 -18
- package/dist/run/run.js +177 -72
- package/dist/run/sandbox-creates.d.ts +59 -0
- package/dist/run/sandbox-creates.js +138 -0
- package/dist/run/sandbox-ids.d.ts +53 -0
- package/dist/run/sandbox-ids.js +198 -0
- package/dist/run/sandbox-receipts.d.ts +16 -0
- package/dist/run/sandbox-receipts.js +70 -0
- package/dist/run/stats.d.ts +5 -0
- package/dist/run/stats.js +8 -4
- package/dist/run/status.d.ts +6 -9
- package/dist/run/status.js +7 -20
- package/dist/run/stored-runs.d.ts +5 -14
- package/dist/run/stored-runs.js +31 -137
- package/dist/run/streams.d.ts +3 -2
- package/dist/run-study.js +6 -1
- package/dist/study/doctor.d.ts +1 -1
- package/dist/study/doctor.js +7 -4
- package/dist/study/init-templates.js +2 -2
- package/dist/study/init.js +14 -3
- package/dist/study/keys.d.ts +12 -0
- package/dist/study/keys.js +32 -0
- package/dist/study/persona.d.ts +4 -0
- package/dist/study/persona.js +4 -2
- package/dist/study/types.d.ts +1 -1
- package/dist/substrates/e2b/peer-install.d.ts +51 -0
- package/dist/substrates/e2b/peer-install.js +161 -0
- package/dist/substrates/e2b/sandbox.d.ts +34 -6
- package/dist/substrates/e2b/sandbox.js +128 -35
- package/dist/substrates/e2b/sdk.d.ts +33 -0
- package/dist/substrates/e2b/sdk.js +21 -21
- package/dist/substrates/shell.d.ts +2 -0
- package/dist/substrates/shell.js +4 -0
- package/dist/tui-app.js +133 -133
- package/dist/verify/artifacts.d.ts +4 -0
- package/dist/verify/artifacts.js +8 -0
- package/dist/verify/liveness.d.ts +19 -3
- package/dist/verify/liveness.js +53 -28
- package/dist/verify/verify.d.ts +4 -1
- package/dist/verify/verify.js +27 -3
- package/docs/architecture/actor-contract.md +2 -1
- package/docs/architecture/observer.md +22 -7
- package/docs/architecture/restricted-codex-analysis.md +1 -1
- package/docs/architecture/task-protocol-support.md +13 -13
- package/docs/architecture/terminal-product-route.md +1 -1
- package/docs/contracts/run-bundle.md +90 -7
- package/docs/contracts/schemas.md +39 -22
- package/docs/contracts/study-analysis.md +45 -0
- package/docs/contracts/study-costs.md +8 -0
- package/docs/decisions/0001-run-bundle-is-the-source-of-truth.md +3 -0
- package/docs/decisions/0003-contained-paths-and-cleanup-authority.md +9 -3
- package/docs/principles/invariants-and-defaults.md +11 -8
- package/package.json +5 -3
- package/skills/humanish/SKILL.md +18 -4
|
@@ -60,12 +60,65 @@ function redactLocalPaths(text, label) {
|
|
|
60
60
|
* passes only this value at a sandbox-id key.
|
|
61
61
|
*/
|
|
62
62
|
export const REDACTED_SANDBOX_ID = "[redacted-sandbox-id]";
|
|
63
|
-
/** The keys run writers put a sandbox id under;
|
|
63
|
+
/** The keys run writers put a sandbox id under; resource entries hold one as `id`. */
|
|
64
64
|
const SANDBOX_ID_KEYS = new Set(["sandboxId", "subjectSandboxId"]);
|
|
65
|
+
/** Arrays of sandbox records: run.json's `providerResources` and cleanup.json's `resources`. */
|
|
66
|
+
const RESOURCE_ARRAY_KEYS = new Set(["providerResources", "resources"]);
|
|
67
|
+
/** A sandbox id's public stand-in: its digest matches a receipt without naming the sandbox. */
|
|
68
|
+
export function sandboxIdDigest(id) {
|
|
69
|
+
return digestText(id, 16);
|
|
70
|
+
}
|
|
71
|
+
/** The digest field written beside a redacted id: `idDigest`, `sandboxIdDigest`, … */
|
|
72
|
+
const digestKey = (key) => `${key}Digest`;
|
|
73
|
+
/**
|
|
74
|
+
* The entries of `object` with each raw id at one of `keys` replaced by REDACTED_SANDBOX_ID and its
|
|
75
|
+
* digest written beside it, unless the object already records one.
|
|
76
|
+
*/
|
|
77
|
+
function redactIdEntries(object, keys) {
|
|
78
|
+
const entries = Object.entries(object);
|
|
79
|
+
const present = new Set(entries.map(([key]) => key));
|
|
80
|
+
return entries.flatMap(([key, child]) => {
|
|
81
|
+
if (!keys.has(key) || typeof child !== "string" || child === REDACTED_SANDBOX_ID)
|
|
82
|
+
return [[key, child]];
|
|
83
|
+
// A label that a text scrub left at the key already carries the id's digest.
|
|
84
|
+
const digest = SANDBOX_ID_LABEL.exec(child)?.[1] ?? sandboxIdDigest(child);
|
|
85
|
+
return [
|
|
86
|
+
[key, REDACTED_SANDBOX_ID],
|
|
87
|
+
...(present.has(digestKey(key)) ? [] : [[digestKey(key), digest]]),
|
|
88
|
+
];
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** How free text names a sandbox: the marker with the id's digest inside it. */
|
|
92
|
+
const SANDBOX_ID_LABEL = /^\[redacted-sandbox-id ([0-9a-f]{16})\]$/;
|
|
93
|
+
const RESOURCE_ID_KEYS = new Set(["id"]);
|
|
94
|
+
/** A raw id at a sandbox-id key: neither the marker nor a label that names its digest. */
|
|
95
|
+
const isRawSandboxId = (value) => typeof value === "string" && value !== REDACTED_SANDBOX_ID && !SANDBOX_ID_LABEL.test(value);
|
|
96
|
+
/** The raw ids at the keys redactSandboxIds replaces, anywhere in `value`. */
|
|
97
|
+
export function collectSandboxIds(value, into = new Set()) {
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
for (const item of value)
|
|
100
|
+
collectSandboxIds(item, into);
|
|
101
|
+
return into;
|
|
102
|
+
}
|
|
103
|
+
if (value === null || typeof value !== "object")
|
|
104
|
+
return into;
|
|
105
|
+
for (const [key, child] of Object.entries(value)) {
|
|
106
|
+
if (SANDBOX_ID_KEYS.has(key) && isRawSandboxId(child))
|
|
107
|
+
into.add(child);
|
|
108
|
+
if (RESOURCE_ARRAY_KEYS.has(key) && Array.isArray(child))
|
|
109
|
+
for (const resource of child) {
|
|
110
|
+
const id = resource !== null && typeof resource === "object" ? Reflect.get(resource, "id") : null;
|
|
111
|
+
if (isRawSandboxId(id))
|
|
112
|
+
into.add(id);
|
|
113
|
+
}
|
|
114
|
+
collectSandboxIds(child, into);
|
|
115
|
+
}
|
|
116
|
+
return into;
|
|
117
|
+
}
|
|
65
118
|
/**
|
|
66
|
-
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID: strings at
|
|
67
|
-
* `subjectSandboxId`, and the `id` of each `providerResources` entry.
|
|
68
|
-
*
|
|
119
|
+
* `value` with every sandbox id replaced by REDACTED_SANDBOX_ID plus its digest: strings at
|
|
120
|
+
* `sandboxId` and `subjectSandboxId`, and the `id` of each `providerResources` or `resources` entry. Raw ids live
|
|
121
|
+
* only in a run's sandbox-receipts.ndjson; every record and output names a sandbox by digest.
|
|
69
122
|
*/
|
|
70
123
|
export function redactSandboxIds(value) {
|
|
71
124
|
if (Array.isArray(value)) {
|
|
@@ -74,28 +127,19 @@ export function redactSandboxIds(value) {
|
|
|
74
127
|
}
|
|
75
128
|
if (value === null || typeof value !== "object")
|
|
76
129
|
return value;
|
|
77
|
-
let changed = false;
|
|
78
130
|
// fromEntries keeps a key such as __proto__ an own property, as JSON.parse made it.
|
|
79
|
-
const entries =
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
? Object.fromEntries(Object.entries(redactSandboxIds(resource)).map(([field, entry]) => [
|
|
89
|
-
field,
|
|
90
|
-
field === "id" ? REDACTED_SANDBOX_ID : entry,
|
|
91
|
-
]))
|
|
92
|
-
: redactSandboxIds(resource));
|
|
93
|
-
else
|
|
94
|
-
next = redactSandboxIds(child);
|
|
95
|
-
if (next !== child && JSON.stringify(next) !== JSON.stringify(child))
|
|
96
|
-
changed = true;
|
|
97
|
-
return [key, next];
|
|
131
|
+
const entries = redactIdEntries(value, SANDBOX_ID_KEYS).map(([key, child]) => {
|
|
132
|
+
if (RESOURCE_ARRAY_KEYS.has(key) && Array.isArray(child))
|
|
133
|
+
return [
|
|
134
|
+
key,
|
|
135
|
+
child.map((resource) => resource !== null && typeof resource === "object" && !Array.isArray(resource)
|
|
136
|
+
? Object.fromEntries(redactIdEntries(redactSandboxIds(resource), RESOURCE_ID_KEYS))
|
|
137
|
+
: redactSandboxIds(resource)),
|
|
138
|
+
];
|
|
139
|
+
return [key, redactSandboxIds(child)];
|
|
98
140
|
});
|
|
141
|
+
const changed = entries.length !== Object.keys(value).length ||
|
|
142
|
+
entries.some(([key, child]) => child !== value[key]);
|
|
99
143
|
return changed ? Object.fromEntries(entries) : value;
|
|
100
144
|
}
|
|
101
145
|
/** Redact secrets to [REDACTED_SECRET] and local paths to their path labels. */
|
|
@@ -8,6 +8,7 @@ import { parse as parseYaml } from "yaml";
|
|
|
8
8
|
import { ACTOR_TRACE_SCHEMA } from "../actors/contract.js";
|
|
9
9
|
import { renderObserver } from "../observer/render.js";
|
|
10
10
|
import { buildObserverData } from "../observer/data.js";
|
|
11
|
+
import { inventorySandboxIds, scrubSandboxIdBytes } from "../run/sandbox-ids.js";
|
|
11
12
|
import { containsSensitive, redactSandboxIds, redactScreenshot, redactText, } from "../evidence/redaction.js";
|
|
12
13
|
import { decodeEscapes } from "../evidence/encoded-text.js";
|
|
13
14
|
import { readPlainText } from "../evidence/plain-text.js";
|
|
@@ -300,6 +301,7 @@ function keptPngPaths(source, bundle) {
|
|
|
300
301
|
async function writeDerivative(source, stagePaths, bundle) {
|
|
301
302
|
const entries = [];
|
|
302
303
|
const imagePaths = keptPngPaths(source, bundle);
|
|
304
|
+
const sandboxIds = inventorySandboxIds(source.files);
|
|
303
305
|
let images = 0;
|
|
304
306
|
for (const file of source.files) {
|
|
305
307
|
const isPng = path.extname(file.path).toLowerCase() === ".png";
|
|
@@ -343,6 +345,7 @@ async function writeDerivative(source, stagePaths, bundle) {
|
|
|
343
345
|
}
|
|
344
346
|
bytes = jsonBytes(transformed);
|
|
345
347
|
}
|
|
348
|
+
bytes = scrubSandboxIdBytes(file.path, bytes, sandboxIds);
|
|
346
349
|
action = bytes.equals(file.bytes) ? "copied" : "updated";
|
|
347
350
|
}
|
|
348
351
|
await writeContainedOutputFile(stagePaths, file.path, bytes);
|
|
@@ -496,7 +499,9 @@ async function verifyFrozenSource(workspace, source, runId) {
|
|
|
496
499
|
verified.shareSafety.reasons.every(
|
|
497
500
|
// Both are repaired in the copy: frames are blurred, and an unread PNG is dropped or
|
|
498
501
|
// refused by keptPngPaths. Any other unread file fails as an unsupported format.
|
|
499
|
-
(reason) => reason.code === "RAW_SCREENSHOTS" ||
|
|
502
|
+
(reason) => reason.code === "RAW_SCREENSHOTS" ||
|
|
503
|
+
reason.code === "RAW_SANDBOX_ID" ||
|
|
504
|
+
reason.code === "UNSCANNED_ARTIFACT")))) {
|
|
500
505
|
return { shareSafety: verified.shareSafety };
|
|
501
506
|
}
|
|
502
507
|
const loaded = await loadRunBundlePrepared(rawSnapshot.physicalPath, snapshotPaths);
|
package/dist/feedback/export.js
CHANGED
|
@@ -384,6 +384,9 @@ function shareSafetyRemedy(codes) {
|
|
|
384
384
|
if (codes.includes("RAW_SCREENSHOTS")) {
|
|
385
385
|
steps.push("Re-run with policies.redactScreenshots: true to blur screenshots at capture.");
|
|
386
386
|
}
|
|
387
|
+
if (codes.includes("RAW_SANDBOX_ID")) {
|
|
388
|
+
steps.push("Export with --format bundle --redact-screenshots, which writes a copy without raw sandbox ids, and share from that copy.");
|
|
389
|
+
}
|
|
387
390
|
if (codes.includes("UNSCANNED_ARTIFACT")) {
|
|
388
391
|
steps.push("verify cannot read the files that `humanish verify --json` lists under UNSCANNED_ARTIFACT as text: remove them from the run folder, or keep only images an actor trace references as stream screenshots.");
|
|
389
392
|
}
|
package/dist/keys/env-file.d.ts
CHANGED
|
@@ -11,5 +11,9 @@ export interface EnvFileLoadResult {
|
|
|
11
11
|
message: string;
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
/** Records in `env` the names a `--dotenv` file set there. */
|
|
15
|
+
export declare function recordDotenvNames(env: NodeJS.ProcessEnv, names: readonly string[]): void;
|
|
16
|
+
/** The names `--dotenv` set in `env`, as recordDotenvNames recorded them. */
|
|
17
|
+
export declare function dotenvSetNames(env: Readonly<Record<string, string | undefined>>): Set<string>;
|
|
14
18
|
export declare function loadEnvFile(cwd: string, envFile: string, env?: NodeJS.ProcessEnv): Promise<EnvFileLoadResult>;
|
|
15
19
|
export {};
|
package/dist/keys/env-file.js
CHANGED
|
@@ -2,6 +2,23 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
const ENV_FILE_RESULT_SCHEMA = "humanish.env-file-result.v1";
|
|
4
4
|
const envNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
5
|
+
/**
|
|
6
|
+
* Names a `--dotenv` file set, kept as a comma list in the environment the values went into, so a
|
|
7
|
+
* process humanish starts with that environment (a run the TUI launches) knows them too. A
|
|
8
|
+
* host-side participant (the Claude Code local agent) never gets these names, even ones it would
|
|
9
|
+
* otherwise keep, such as a proxy.
|
|
10
|
+
*/
|
|
11
|
+
const DOTENV_NAMES = "HUMANISH_DOTENV_NAMES";
|
|
12
|
+
/** Records in `env` the names a `--dotenv` file set there. */
|
|
13
|
+
export function recordDotenvNames(env, names) {
|
|
14
|
+
if (names.length === 0)
|
|
15
|
+
return;
|
|
16
|
+
env[DOTENV_NAMES] = [...new Set([...dotenvSetNames(env), ...names])].join(",");
|
|
17
|
+
}
|
|
18
|
+
/** The names `--dotenv` set in `env`, as recordDotenvNames recorded them. */
|
|
19
|
+
export function dotenvSetNames(env) {
|
|
20
|
+
return new Set((env[DOTENV_NAMES] ?? "").split(",").filter((name) => name.length > 0));
|
|
21
|
+
}
|
|
5
22
|
export async function loadEnvFile(cwd, envFile, env = process.env) {
|
|
6
23
|
const resolvedCwd = path.resolve(cwd);
|
|
7
24
|
const envPath = path.resolve(resolvedCwd, envFile);
|
|
@@ -22,6 +22,11 @@ export declare function discoverProviderKeys(args: {
|
|
|
22
22
|
deps?: KeyResolutionDeps;
|
|
23
23
|
announced?: ReadonlySet<string>;
|
|
24
24
|
}): Promise<ResolvedKeyFill[]>;
|
|
25
|
+
/** What `--dotenv` loaded: the path as given, and the names it set because they were unset. */
|
|
26
|
+
export interface DotenvLoad {
|
|
27
|
+
path: string;
|
|
28
|
+
names: readonly string[];
|
|
29
|
+
}
|
|
25
30
|
export interface KeySourceProbe {
|
|
26
31
|
name: string;
|
|
27
32
|
/** The source that supplies the key right now, or null when missing everywhere. */
|
|
@@ -43,6 +48,8 @@ export declare function probeKeySources(names: readonly string[], args: {
|
|
|
43
48
|
cwd: string;
|
|
44
49
|
env: NodeJS.ProcessEnv;
|
|
45
50
|
deps?: KeyResolutionDeps;
|
|
51
|
+
/** The names `--dotenv` put into `env`, so their rows name the file, not the process env. */
|
|
52
|
+
dotenv?: DotenvLoad;
|
|
46
53
|
}): Promise<KeySourceProbe[]>;
|
|
47
54
|
/** Resolve a `humanish keys set` target: a vendor alias or a raw env name. Null = invalid. */
|
|
48
55
|
export declare function resolveKeyName(vendorOrName: string): string | null;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Provider-key discovery: resolve the keys a live run needs through each vendor's
|
|
2
|
-
// native chain instead of demanding a per-repo --
|
|
2
|
+
// native chain instead of demanding a per-repo --dotenv that operators hand-copy keys into.
|
|
3
3
|
//
|
|
4
4
|
// The chain, per key, fill-only (a rung never overrides anything already present):
|
|
5
|
-
// 1. process env, including whatever --
|
|
5
|
+
// 1. process env, including whatever --dotenv just loaded (explicit always wins);
|
|
6
6
|
// 2. `.humanish/local/provider.env`, the project-local overlay this CLI's own --help
|
|
7
7
|
// examples document;
|
|
8
8
|
// 3. the owning vendor's native store, where one exists:
|
|
@@ -24,7 +24,7 @@ import { loadEnvFile } from "./env-file.js";
|
|
|
24
24
|
/** The only names implicit discovery may fill (and `humanish keys set` may store). Everything
|
|
25
25
|
* else in an overlay/store file is ignored-and-named: a repo-planted NODE_OPTIONS/LD_PRELOAD
|
|
26
26
|
* must never enter process env off a file the operator did not explicitly pass (an explicit
|
|
27
|
-
* --
|
|
27
|
+
* --dotenv remains the operator's own full-file load). */
|
|
28
28
|
const KNOWN_PROVIDER_KEYS = [
|
|
29
29
|
"OPENAI_API_KEY",
|
|
30
30
|
"ANTHROPIC_API_KEY",
|
|
@@ -213,7 +213,7 @@ export async function discoverProviderKeys(args) {
|
|
|
213
213
|
if (announced === undefined || announced.has(fill_.name))
|
|
214
214
|
announce(`humanish keys: ${fill_.name} from ${fill_.source}`);
|
|
215
215
|
for (const name of ignored)
|
|
216
|
-
announce(`humanish keys: ignored ${name}, which is not a provider key. Discovery loads provider keys only; pass the file with --
|
|
216
|
+
announce(`humanish keys: ignored ${name}, which is not a provider key. Discovery loads provider keys only; pass the file with --dotenv to load every name in it.`);
|
|
217
217
|
return fills;
|
|
218
218
|
}
|
|
219
219
|
/** True when the file's parent directory is (or traverses) a symlink at its last component:
|
|
@@ -253,7 +253,7 @@ export function describeMissingKeys(names, env) {
|
|
|
253
253
|
const hints = names.map((name) => `${name}: ${missingKeyHint(name)}`).join("; ");
|
|
254
254
|
const strict = env.HUMANISH_STRICT_KEYS?.trim() === "1";
|
|
255
255
|
const chain = strict
|
|
256
|
-
? "key discovery is disabled (HUMANISH_STRICT_KEYS=1); only process env and --
|
|
256
|
+
? "key discovery is disabled (HUMANISH_STRICT_KEYS=1); only process env and --dotenv are read"
|
|
257
257
|
: `also checked ${PROJECT_OVERLAY_RELATIVE}, ~/.e2b/config.json, gh auth token, and ${path.join("~", ".config", "humanish", "keys.env")}`;
|
|
258
258
|
return `${chain}. Fill: ${hints}.`;
|
|
259
259
|
}
|
|
@@ -278,7 +278,9 @@ export async function probeKeySources(names, args) {
|
|
|
278
278
|
args.env.GITHUB_TOKEN !== undefined &&
|
|
279
279
|
args.env.GITHUB_TOKEN.trim() !== "";
|
|
280
280
|
const source = inEnv
|
|
281
|
-
?
|
|
281
|
+
? args.dotenv?.names.includes(name)
|
|
282
|
+
? `--dotenv ${args.dotenv.path}`
|
|
283
|
+
: "process env"
|
|
282
284
|
: aliasInEnv
|
|
283
285
|
? "process env (GITHUB_TOKEN)"
|
|
284
286
|
: (bySource.get(name) ?? null);
|
|
@@ -307,7 +309,7 @@ export function setUserKey(name, value, env, deps = {}) {
|
|
|
307
309
|
throw new Error(`Not a valid env name: ${name}`);
|
|
308
310
|
}
|
|
309
311
|
if (!PROVIDER_KEY_SET.has(name)) {
|
|
310
|
-
throw new Error(`The store holds provider keys only (${[...PROVIDER_KEY_SET].join(", ")}). For anything else, use an explicit --
|
|
312
|
+
throw new Error(`The store holds provider keys only (${[...PROVIDER_KEY_SET].join(", ")}). For anything else, use an explicit --dotenv.`);
|
|
311
313
|
}
|
|
312
314
|
const storePath = userKeyStorePath(env, deps);
|
|
313
315
|
const dir = path.dirname(storePath);
|
|
@@ -325,7 +327,7 @@ export function setUserKey(name, value, env, deps = {}) {
|
|
|
325
327
|
// would silently resolve to a different secret on the next run.
|
|
326
328
|
const roundTrip = parseStoreLine(`${name}=${trimmed}`);
|
|
327
329
|
if (roundTrip === null || roundTrip[0] !== name || roundTrip[1] !== trimmed) {
|
|
328
|
-
throw new Error("The value does not round-trip the store format (avoid leading quotes and '#'); pass it via --
|
|
330
|
+
throw new Error("The value does not round-trip the store format (avoid leading quotes and '#'); pass it via --dotenv instead.");
|
|
329
331
|
}
|
|
330
332
|
writeStore(storePath, entries);
|
|
331
333
|
return { path: storePath };
|
package/dist/observer/data.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ActorEnding } from "../actors/stop-cause.js";
|
|
2
2
|
import type { RunBundle, RunCostSummary, RunEvent, RunSimulation } from "../run/bundle.js";
|
|
3
|
+
import { type RunDisplay } from "../run/display.js";
|
|
3
4
|
import type { RunStream, RunStreamKind } from "../run/streams.js";
|
|
4
5
|
export declare const OBSERVER_DATA_SCHEMA = "humanish.observer-data.v1";
|
|
5
6
|
interface ObserverArtifactLink {
|
|
@@ -20,7 +21,14 @@ export interface ObserverData {
|
|
|
20
21
|
run: {
|
|
21
22
|
runId: string;
|
|
22
23
|
mode: RunBundle["mode"];
|
|
24
|
+
/** The participants' verdict. `display` says whether the run passed. */
|
|
23
25
|
status: RunBundle["review"]["verdict"];
|
|
26
|
+
/**
|
|
27
|
+
* How the run reads (runDisplay in src/run/display.ts): `passed` only when the verdict is pass
|
|
28
|
+
* and run.json's outcome does not say the run failed. Absent in observer-data.json written
|
|
29
|
+
* before it existed.
|
|
30
|
+
*/
|
|
31
|
+
display?: RunDisplay;
|
|
24
32
|
title: string;
|
|
25
33
|
createdAt: string;
|
|
26
34
|
simCount: number;
|
|
@@ -109,7 +117,12 @@ interface ObserverLaneGroup {
|
|
|
109
117
|
surface?: string;
|
|
110
118
|
caseGroup?: string;
|
|
111
119
|
}
|
|
112
|
-
|
|
120
|
+
/**
|
|
121
|
+
* The Observer's projection of a bundle. `statusRecord` is the run's status.json, read for a
|
|
122
|
+
* bundle with no outcome in run.json: a run in progress, interrupted, or recorded before run.json
|
|
123
|
+
* carried its outcome.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildObserverData(bundle: RunBundle, generatedAt?: string, statusRecord?: unknown): ObserverData;
|
|
113
126
|
/** Refresh presentation from recorded actor evidence, including older exported snapshots. */
|
|
114
127
|
export declare function withObserverEndings(data: ObserverData): ObserverData;
|
|
115
128
|
export {};
|
package/dist/observer/data.js
CHANGED
|
@@ -2,6 +2,7 @@ import { actorEnding } from "../actors/stop-cause.js";
|
|
|
2
2
|
import { isCommsReceivingEvidence, receivingAnalysisContext } from "../comms/receiving-evidence.js";
|
|
3
3
|
import { formatParticipantOutcomes, formatRunTaskFunnel, participantOutcomeDetails, withCuaReviewProvenance, } from "../run/outcomes.js";
|
|
4
4
|
import { cuaGoalSource, CUA_COMPLETION_NOTE } from "../actors/goal-source.js";
|
|
5
|
+
import { bundleDisplayFacts, runDisplay } from "../run/display.js";
|
|
5
6
|
export const OBSERVER_DATA_SCHEMA = "humanish.observer-data.v1";
|
|
6
7
|
/** Discard a forged or stale runtime grant before projecting persisted evidence. */
|
|
7
8
|
export function recordedStreamEmbed(embed) {
|
|
@@ -19,7 +20,12 @@ const allKinds = [
|
|
|
19
20
|
"artifact",
|
|
20
21
|
"summary",
|
|
21
22
|
];
|
|
22
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The Observer's projection of a bundle. `statusRecord` is the run's status.json, read for a
|
|
25
|
+
* bundle with no outcome in run.json: a run in progress, interrupted, or recorded before run.json
|
|
26
|
+
* carried its outcome.
|
|
27
|
+
*/
|
|
28
|
+
export function buildObserverData(bundle, generatedAt = new Date().toISOString(), statusRecord) {
|
|
23
29
|
const byKind = Object.fromEntries(allKinds.map((kind) => [kind, 0]));
|
|
24
30
|
const events = [...(bundle.events ?? [])];
|
|
25
31
|
// Analysis cites this source-backed aggregate as context, not a participant
|
|
@@ -32,14 +38,17 @@ export function buildObserverData(bundle, generatedAt = new Date().toISOString()
|
|
|
32
38
|
const streams = (bundle.streams ?? []).map((stream) => {
|
|
33
39
|
const sim = bundle.simulations.find((candidate) => candidate.id === stream.simId) ??
|
|
34
40
|
fallbackSimulation(bundle, stream);
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
// The judge's status for the participant wins where the run recorded one, so a participant
|
|
42
|
+
// the verdict counted as blocked or as never engaging shows that way here. A bundle written
|
|
43
|
+
// before the judge's status was recorded keeps the typed-outcome reading: a natural session
|
|
44
|
+
// that finished while declaring a blocker. Never turn an active or failed harness into a
|
|
45
|
+
// participant outcome.
|
|
46
|
+
const status = stream.judgedStatus ??
|
|
47
|
+
((stream.status === "passed" || stream.status === "complete") &&
|
|
48
|
+
stream.actor?.completionReason === "goal_satisfied" &&
|
|
49
|
+
stream.actor.declaredOutcome === "blocked"
|
|
50
|
+
? "blocked"
|
|
51
|
+
: stream.status);
|
|
43
52
|
byKind[stream.kind] += 1;
|
|
44
53
|
return {
|
|
45
54
|
...stream,
|
|
@@ -77,6 +86,7 @@ export function buildObserverData(bundle, generatedAt = new Date().toISOString()
|
|
|
77
86
|
runId: bundle.runId,
|
|
78
87
|
mode: bundle.mode,
|
|
79
88
|
status: bundle.review.verdict,
|
|
89
|
+
display: runDisplay(bundleDisplayFacts(bundle, statusRecord)),
|
|
80
90
|
title: `${bundle.scenario.title} - ${bundle.persona.name}`,
|
|
81
91
|
createdAt: bundle.createdAt,
|
|
82
92
|
simCount: bundle.simCount ?? bundle.simulations.length,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RunDisplay } from "../run/display.js";
|
|
1
2
|
export interface LibraryHistory {
|
|
2
3
|
latestRunId: string | null;
|
|
3
4
|
runs: Array<{
|
|
@@ -5,7 +6,10 @@ export interface LibraryHistory {
|
|
|
5
6
|
createdAt: string | null;
|
|
6
7
|
mode: string | null;
|
|
7
8
|
href: string;
|
|
9
|
+
/** The participants' verdict. `display` says whether the run passed. */
|
|
8
10
|
status: string;
|
|
11
|
+
/** How the run reads (runDisplay in src/run/display.ts); absent when its data could not be read. */
|
|
12
|
+
display?: RunDisplay;
|
|
9
13
|
/** Current contained status-record observation, separate from the evidence verdict. */
|
|
10
14
|
runtimeState?: "running" | "finished" | "interrupted" | "unknown";
|
|
11
15
|
streamCount: number;
|
package/dist/observer/library.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
// The pip color for each RunDisplay tone; a run with no display gets the neutral gray.
|
|
2
|
+
const DISPLAY_TONES = {
|
|
3
|
+
pass: "#3fb970",
|
|
4
|
+
live: "#4f8ff7",
|
|
5
|
+
fail: "#e5534b",
|
|
6
|
+
warn: "#d29922",
|
|
7
|
+
neutral: "#57606f",
|
|
7
8
|
};
|
|
8
9
|
export function renderLibraryHtml(history, opts) {
|
|
9
10
|
const emptyState = opts.safe
|
|
@@ -83,7 +84,7 @@ main { padding: 1rem 1.1rem 3rem; max-width: 44rem; margin: 0 auto; }
|
|
|
83
84
|
}
|
|
84
85
|
// prose-check: script (the library page's JavaScript)
|
|
85
86
|
function libraryClientJs() {
|
|
86
|
-
const tones = JSON.stringify(
|
|
87
|
+
const tones = JSON.stringify(DISPLAY_TONES);
|
|
87
88
|
return `
|
|
88
89
|
(function () {
|
|
89
90
|
var TONES = ${tones};
|
|
@@ -103,9 +104,11 @@ function libraryClientJs() {
|
|
|
103
104
|
link.href = run.href;
|
|
104
105
|
var top = document.createElement("div");
|
|
105
106
|
top.className = "run-top";
|
|
107
|
+
var display = run.display || null;
|
|
108
|
+
var live = display !== null && display.state === "running";
|
|
106
109
|
var pip = document.createElement("span");
|
|
107
|
-
pip.className =
|
|
108
|
-
pip.style.background = TONES[
|
|
110
|
+
pip.className = live ? "pip running" : "pip";
|
|
111
|
+
pip.style.background = (display && TONES[display.tone]) || TONES.neutral;
|
|
109
112
|
var id = document.createElement("span");
|
|
110
113
|
id.className = "run-id";
|
|
111
114
|
id.textContent = run.runId;
|
|
@@ -116,15 +119,15 @@ function libraryClientJs() {
|
|
|
116
119
|
latest.className = "badge latest";
|
|
117
120
|
latest.textContent = "latest";
|
|
118
121
|
top.appendChild(latest);
|
|
119
|
-
} else if (
|
|
120
|
-
var
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
top.appendChild(
|
|
122
|
+
} else if (live) {
|
|
123
|
+
var badge = document.createElement("span");
|
|
124
|
+
badge.className = "badge";
|
|
125
|
+
badge.textContent = "live";
|
|
126
|
+
top.appendChild(badge);
|
|
124
127
|
}
|
|
125
128
|
var meta = document.createElement("div");
|
|
126
129
|
meta.className = "run-meta";
|
|
127
|
-
var metaBits = [run.mode || "unknown", run.streamCount + " participants", run.createdAt || ""];
|
|
130
|
+
var metaBits = [display ? display.label : "", run.mode || "unknown", run.streamCount + " participants", run.createdAt || ""];
|
|
128
131
|
// Labeled cost token: ALWAYS "~$X est." (never a bare "$X"), so the library never implies an
|
|
129
132
|
// authoritative charge. Null = omitted (advisory, fail-open on display).
|
|
130
133
|
if (run.costLabel) {
|
package/dist/observer/render.js
CHANGED
|
@@ -11,7 +11,8 @@ import { projectShareCheckedAnalysis } from "../analysis/sharing.js";
|
|
|
11
11
|
import { loadAnalysis } from "../analysis/load.js";
|
|
12
12
|
import { physicalCwdOf, runIdOf, bindExistingRunArtifactPaths, isSafeRunIdSegment, resolveLatestRunDirectory, validatePreparedRunArtifactPaths, } from "../run/paths.js";
|
|
13
13
|
import { writeContainedOutputFile } from "../run/contained-output.js";
|
|
14
|
-
import { loadRunBundlePrepared } from "../run/locate.js";
|
|
14
|
+
import { loadRunBundlePrepared, readRunJsonIfExists } from "../run/locate.js";
|
|
15
|
+
import { RUN_STATUS_FILE } from "../run/status.js";
|
|
15
16
|
import { verifyRunPrepared } from "../verify/verify.js";
|
|
16
17
|
import { renderObserverHtml } from "./artifact.js";
|
|
17
18
|
import { buildObserverData } from "./data.js";
|
|
@@ -99,7 +100,12 @@ export async function renderObserver(cwdInput, runInput, options = {}) {
|
|
|
99
100
|
...projectShareCheckedAnalysis(await loadAnalysis(preparedRunPaths)),
|
|
100
101
|
...(spend === null ? {} : { spend }),
|
|
101
102
|
};
|
|
102
|
-
|
|
103
|
+
// A run.json without an outcome is in progress, interrupted, or older than the field: its
|
|
104
|
+
// status.json says which, and holds an older run's ok.
|
|
105
|
+
const statusRecord = loaded.bundle.outcome === undefined
|
|
106
|
+
? await readRunJsonIfExists(preparedRunPaths, RUN_STATUS_FILE)
|
|
107
|
+
: undefined;
|
|
108
|
+
const observerData = buildObserverData(loaded.bundle, undefined, statusRecord);
|
|
103
109
|
observerData.publicSafety.share = {
|
|
104
110
|
status: verified.shareSafety.status,
|
|
105
111
|
verifiedAt: new Date().toISOString(),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { RunDisplay } from "../run/display.js";
|
|
2
3
|
import { type ObserverData } from "./data.js";
|
|
3
4
|
import { type PinnedDirectory } from "./pinned-files.js";
|
|
4
5
|
export interface ObserverRuntimeStreamUrl {
|
|
@@ -22,6 +23,7 @@ export declare function buildHistoryIndex(proofRoot: PinnedDirectory, admitRun?:
|
|
|
22
23
|
mode: string | null;
|
|
23
24
|
href: string;
|
|
24
25
|
status: string;
|
|
26
|
+
display?: RunDisplay;
|
|
25
27
|
runtimeState?: NonNullable<ObserverData["runtime"]>["state"];
|
|
26
28
|
streamCount: number;
|
|
27
29
|
estimatedCostUsd: number | null;
|
|
@@ -8,7 +8,7 @@ import { listRuns } from "../run/stored-runs.js";
|
|
|
8
8
|
import { readAnalysisAccounting } from "../run/costs.js";
|
|
9
9
|
import { bindExistingRunArtifactPaths, isPathInside, isSafeRunIdSegment, } from "../run/paths.js";
|
|
10
10
|
import { analysisCostOf, runCost, runCostLabel } from "../run/run-cost.js";
|
|
11
|
-
import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord } from "../run/status.js";
|
|
11
|
+
import { RUN_STATUS_FILE, RUN_STATUS_STALE_MS, isRunStatusRecord, } from "../run/status.js";
|
|
12
12
|
import { renderObserverHtml } from "./artifact.js";
|
|
13
13
|
import { buildObserverData, recordedStreamEmbed, withObserverEndings, } from "./data.js";
|
|
14
14
|
import { buildArtifactSecurityHeaders } from "./http.js";
|
|
@@ -120,6 +120,7 @@ export async function serveRunPath(runRoot, relativePath, response, runtimeStrea
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
async function readObserverData(runRoot, runtimeStreamUrls = []) {
|
|
123
|
+
const record = await readLocalRunStatus(runRoot);
|
|
123
124
|
// Best-effort load from either source. Both reads swallow all errors on
|
|
124
125
|
// purpose: this runs on every browser poll of a live run, where run.json may
|
|
125
126
|
// be absent, still being written (a partial-JSON parse error), or superseded
|
|
@@ -130,57 +131,64 @@ async function readObserverData(runRoot, runtimeStreamUrls = []) {
|
|
|
130
131
|
if (!bundleBytes)
|
|
131
132
|
throw new Error("run.json unavailable");
|
|
132
133
|
const bundle = JSON.parse(bundleBytes.toString("utf8"));
|
|
133
|
-
return withRuntimeStreamUrls(
|
|
134
|
+
return withRuntimeStreamUrls(withLocalRunStatus(record, buildObserverData(bundle, undefined, record)), runtimeStreamUrls);
|
|
134
135
|
}
|
|
135
136
|
catch { }
|
|
136
137
|
try {
|
|
137
138
|
const observerBytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, "observer", "observer-data.json"));
|
|
138
139
|
if (!observerBytes)
|
|
139
140
|
throw new Error("observer-data.json unavailable");
|
|
140
|
-
return withRuntimeStreamUrls(
|
|
141
|
+
return withRuntimeStreamUrls(withLocalRunStatus(record, withObserverEndings(JSON.parse(observerBytes.toString("utf8")))), runtimeStreamUrls);
|
|
141
142
|
}
|
|
142
143
|
catch { }
|
|
143
144
|
return null;
|
|
144
145
|
}
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
* A stale heartbeat means unknown: neither an old timestamp nor a persisted PID proves that a
|
|
148
|
-
* process died (the evidence may have been copied from another machine). No PID is served/probed.
|
|
149
|
-
*/
|
|
150
|
-
async function withLocalRunStatus(runRoot, input) {
|
|
151
|
-
// A served observation must never be inherited from a persisted projection or export.
|
|
152
|
-
const { runtime: _persistedRuntime, ...data } = input;
|
|
146
|
+
/** The run directory's own status record, when it is well formed and names that directory. */
|
|
147
|
+
async function readLocalRunStatus(runRoot) {
|
|
153
148
|
try {
|
|
154
149
|
const bytes = await readContainedFile(runRoot, path.join(runRoot.physicalPath, RUN_STATUS_FILE));
|
|
155
150
|
if (!bytes)
|
|
156
|
-
return
|
|
151
|
+
return undefined;
|
|
157
152
|
const record = JSON.parse(bytes.toString("utf8"));
|
|
158
|
-
|
|
159
|
-
record
|
|
160
|
-
|
|
161
|
-
record.runId !== path.basename(runRoot.physicalPath))
|
|
162
|
-
return data;
|
|
163
|
-
const now = Date.now();
|
|
164
|
-
const started = Date.parse(record.startedAt);
|
|
165
|
-
const updated = Date.parse(record.updatedAt);
|
|
166
|
-
const timestampsValid = Number.isFinite(started) && Number.isFinite(updated) && started <= updated && updated <= now;
|
|
167
|
-
let state = "unknown";
|
|
168
|
-
if (timestampsValid) {
|
|
169
|
-
if (record.state === "finished") {
|
|
170
|
-
state = "finished";
|
|
171
|
-
}
|
|
172
|
-
else if (record.state === "running" && now - updated <= RUN_STATUS_STALE_MS) {
|
|
173
|
-
state = "running";
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
...data,
|
|
178
|
-
runtime: { state, observedAt: new Date(now).toISOString(), source: "local-run-status" },
|
|
179
|
-
};
|
|
153
|
+
return isRunStatusRecord(record) && record.runId === path.basename(runRoot.physicalPath)
|
|
154
|
+
? record
|
|
155
|
+
: undefined;
|
|
180
156
|
}
|
|
181
157
|
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Liveness is a current read of a contained local status record, separate from run evidence.
|
|
163
|
+
* A stale heartbeat means unknown: neither an old timestamp nor a persisted PID proves that a
|
|
164
|
+
* process died (the evidence may have been copied from another machine). A record its stopped
|
|
165
|
+
* process wrote is interrupted. No PID is served/probed.
|
|
166
|
+
*/
|
|
167
|
+
function withLocalRunStatus(record, input) {
|
|
168
|
+
// A served observation must never be inherited from a persisted projection or export.
|
|
169
|
+
const { runtime: _persistedRuntime, ...data } = input;
|
|
170
|
+
if (record === undefined || record.runId !== data.run.runId || record.mode !== data.run.mode)
|
|
182
171
|
return data;
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
const started = Date.parse(record.startedAt);
|
|
174
|
+
const updated = Date.parse(record.updatedAt);
|
|
175
|
+
const timestampsValid = Number.isFinite(started) && Number.isFinite(updated) && started <= updated && updated <= now;
|
|
176
|
+
let state = "unknown";
|
|
177
|
+
if (timestampsValid) {
|
|
178
|
+
if (record.state === "finished") {
|
|
179
|
+
state = "finished";
|
|
180
|
+
}
|
|
181
|
+
else if (record.state === "interrupted") {
|
|
182
|
+
state = "interrupted";
|
|
183
|
+
}
|
|
184
|
+
else if (record.state === "running" && now - updated <= RUN_STATUS_STALE_MS) {
|
|
185
|
+
state = "running";
|
|
186
|
+
}
|
|
183
187
|
}
|
|
188
|
+
return {
|
|
189
|
+
...data,
|
|
190
|
+
runtime: { state, observedAt: new Date(now).toISOString(), source: "local-run-status" },
|
|
191
|
+
};
|
|
184
192
|
}
|
|
185
193
|
/** internal: exported for the stream lifecycle tests. */
|
|
186
194
|
export function withRuntimeStreamUrls(data, runtimeStreamUrls) {
|
|
@@ -249,6 +257,7 @@ admitRun) {
|
|
|
249
257
|
mode: admitRun ? (data?.run.mode ?? null) : run.mode,
|
|
250
258
|
href: `/_humanish/runs/${encodeURIComponent(run.runId)}/observer/index.html`,
|
|
251
259
|
status: data?.run.status ?? "unknown",
|
|
260
|
+
...(data?.run.display === undefined ? {} : { display: data.run.display }),
|
|
252
261
|
...(data?.runtime ? { runtimeState: data.runtime.state } : {}),
|
|
253
262
|
streamCount: data?.streams.length ?? 0,
|
|
254
263
|
// Labeled run-total cost estimate (advisory; null when the run carries no cost summary).
|