@kontourai/flow-agents 3.8.0 → 3.10.0
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/.github/workflows/ci.yml +8 -0
- package/CHANGELOG.md +24 -0
- package/README.md +1 -1
- package/build/src/builder-flow-runtime.js +16 -10
- package/build/src/cli/assignment-provider.js +4 -18
- package/build/src/cli/continuation-adapter.d.ts +26 -0
- package/build/src/cli/continuation-adapter.js +243 -0
- package/build/src/cli/validate-workflow-artifacts.js +21 -1
- package/build/src/cli/workflow-sidecar.js +15 -20
- package/build/src/cli/workflow.js +99 -6
- package/build/src/continuation-driver.d.ts +116 -0
- package/build/src/continuation-driver.js +527 -0
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/tools/build-universal-bundles.js +52 -25
- package/build/src/tools/validate-source-tree.js +5 -2
- package/context/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/context/scripts/hooks/stop-goal-fit.js +244 -73
- package/context/scripts/hooks/workflow-steering.js +24 -15
- package/context/scripts/telemetry/lib/pricing.sh +18 -6
- package/context/scripts/telemetry/lib/transport.sh +139 -37
- package/context/scripts/telemetry/lib/usage.sh +282 -32
- package/context/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/context/scripts/telemetry/telemetry.sh +76 -1
- package/docs/coordination-guide.md +21 -5
- package/docs/fixture-ownership.md +2 -1
- package/docs/public-workflow-cli.md +98 -0
- package/docs/spec/runtime-hook-surface.md +8 -4
- package/docs/workflow-usage-guide.md +7 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +13 -0
- package/evals/acceptance/prove-capture-teeth.sh +13 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/codex-legacy-agents/5273878130bdafc8a024a650bb5b66c9b003f1f859b5dc6e5b588cbf4ab23228.md +25 -0
- package/evals/fixtures/telemetry/usage-transcript-adversarial.jsonl +2 -0
- package/evals/fixtures/telemetry/usage-transcript-malformed-sandwich.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-negative-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-prefix.jsonl +201 -0
- package/evals/fixtures/telemetry/usage-transcript-oversized-tokens.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-lower.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-prefixed-secret-upper.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model-jwt.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-secret-model.jsonl +1 -0
- package/evals/fixtures/telemetry/usage-transcript-torn-trailing.jsonl +2 -0
- package/evals/integration/test_actor_identity.sh +63 -0
- package/evals/integration/test_assignment_provider_local_file.sh +34 -0
- package/evals/integration/test_builder_entry_enforcement.sh +5 -1
- package/evals/integration/test_bundle_install.sh +106 -17
- package/evals/integration/test_bundle_lifecycle.sh +17 -3
- package/evals/integration/test_captured_fail_reconciliation.sh +11 -0
- package/evals/integration/test_ci_actor_identity.sh +2 -2
- package/evals/integration/test_command_log_concurrency.sh +11 -0
- package/evals/integration/test_command_log_fork_classification.sh +11 -0
- package/evals/integration/test_command_log_integrity.sh +11 -0
- package/evals/integration/test_current_json_per_actor.sh +416 -21
- package/evals/integration/test_enforcer_expects_driven.sh +11 -0
- package/evals/integration/test_ensure_session_ownership_guard.sh +39 -0
- package/evals/integration/test_evidence_capture_hook.sh +25 -6
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flowdef_union_floor_regression.sh +11 -0
- package/evals/integration/test_gate_bypass_chain.sh +11 -0
- package/evals/integration/test_gate_lockdown.sh +10 -0
- package/evals/integration/test_goal_fit_ghost_session.sh +11 -0
- package/evals/integration/test_goal_fit_hook.sh +257 -9
- package/evals/integration/test_goal_fit_rederive.sh +11 -0
- package/evals/integration/test_install_merge.sh +141 -3
- package/evals/integration/test_liveness_conflict_injection.sh +39 -3
- package/evals/integration/test_liveness_heartbeat.sh +46 -9
- package/evals/integration/test_phase_map_and_gate_claim.sh +10 -1
- package/evals/integration/test_public_workflow_cli.sh +6 -6
- package/evals/integration/test_session_resume_roundtrip.sh +30 -0
- package/evals/integration/test_stop_hook_release.sh +111 -11
- package/evals/integration/test_telemetry_tool_usage.sh +394 -0
- package/evals/integration/test_verify_hold.sh +64 -1
- package/evals/integration/test_workflow_sidecar_writer.sh +10 -1
- package/evals/integration/test_workflow_steering_hook.sh +20 -4
- package/evals/run.sh +2 -0
- package/evals/static/test_universal_bundles.sh +123 -0
- package/package.json +4 -4
- package/packaging/README.md +21 -0
- package/packaging/codex-legacy-agents-fingerprints.json +33 -0
- package/scripts/README.md +2 -1
- package/scripts/audit-codex-legacy-agents.js +43 -0
- package/scripts/classify-codex-legacy-agents.js +71 -0
- package/scripts/hooks/evidence-capture.js +18 -6
- package/scripts/hooks/lib/actor-identity.js +62 -29
- package/scripts/hooks/lib/continuation-turn-authority.js +386 -0
- package/scripts/hooks/lib/current-pointer.js +114 -12
- package/scripts/hooks/lib/liveness-heartbeat.js +15 -12
- package/scripts/hooks/stop-goal-fit.js +244 -73
- package/scripts/hooks/workflow-steering.js +24 -15
- package/scripts/install-codex-home.sh +8 -7
- package/scripts/telemetry/lib/pricing.sh +18 -6
- package/scripts/telemetry/lib/transport.sh +71 -0
- package/scripts/telemetry/lib/usage.sh +282 -32
- package/scripts/telemetry/lib/usage_model_guard.sh +42 -0
- package/scripts/telemetry/telemetry.sh +76 -1
- package/src/builder-flow-runtime.ts +9 -3
- package/src/cli/assignment-provider.ts +5 -26
- package/src/cli/builder-flow-runtime.test.mjs +215 -2
- package/src/cli/continuation-adapter.ts +265 -0
- package/src/cli/continuation-driver.test.mjs +1303 -0
- package/src/cli/public-api.test.mjs +1 -0
- package/src/cli/validate-workflow-artifacts.ts +19 -1
- package/src/cli/workflow-sidecar.ts +18 -23
- package/src/cli/workflow.ts +113 -6
- package/src/continuation-driver.ts +638 -0
- package/src/index.ts +24 -0
- package/src/tools/build-universal-bundles.ts +61 -25
- package/src/tools/validate-source-tree.ts +5 -2
|
@@ -45,10 +45,22 @@ const {
|
|
|
45
45
|
warnIfFailingOpenInsideGitTree,
|
|
46
46
|
} = require('./lib/local-artifact-paths');
|
|
47
47
|
const { resolveActor, isUnresolvedActor, detectRuntime } = require('./lib/actor-identity.js');
|
|
48
|
-
const { readCurrentPointer } = require('./lib/current-pointer.js');
|
|
48
|
+
const { readCurrentPointer, readOwnCurrentPointer } = require('./lib/current-pointer.js');
|
|
49
49
|
const { isRunnableCommandText, isAmbiguousAbsenceCommand } = require('./lib/runnable-command.js');
|
|
50
|
+
let validateActiveTurnAuthority = () => ({ valid: false, reason: 'continuation authority validator is unavailable' });
|
|
51
|
+
let validateSignedActiveTurnAssignmentAuthority = validateActiveTurnAuthority;
|
|
52
|
+
try {
|
|
53
|
+
({ validateActiveTurnAuthority, validateSignedActiveTurnAssignmentAuthority } = require('./lib/continuation-turn-authority.js'));
|
|
54
|
+
} catch {
|
|
55
|
+
// A partial legacy install must retain ordinary hard Stop blocking, not fail
|
|
56
|
+
// open because the optional continuation feature is unavailable.
|
|
57
|
+
}
|
|
50
58
|
|
|
51
59
|
const MAX_STDIN = 1024 * 1024;
|
|
60
|
+
const MAX_CANONICAL_FLOW_STATE_BYTES = 1024 * 1024;
|
|
61
|
+
const MAX_CANONICAL_FLOW_DEFINITION_BYTES = 1024 * 1024;
|
|
62
|
+
const CANONICAL_FLOW_STATUSES = new Set(['active', 'blocked', 'needs_decision', 'paused', 'canceled', 'completed', 'failed', 'accepted_by_exception']);
|
|
63
|
+
const CANONICAL_FLOW_TERMINAL_STATUSES = new Set(['completed', 'canceled', 'failed']);
|
|
52
64
|
const ACTIVE_STATUSES = new Set([
|
|
53
65
|
'planning',
|
|
54
66
|
'planned',
|
|
@@ -164,15 +176,35 @@ function isEnvironmentError(line) {
|
|
|
164
176
|
return /tsc[:\s]|command not found|npm ERR!|npm error|ENOENT|EACCES|Cannot find module|node_modules\/.bin|TypeScript version|version conflict|error TS[0-9]/i.test(line);
|
|
165
177
|
}
|
|
166
178
|
|
|
179
|
+
function readPackageManifest(packageRoot) {
|
|
180
|
+
try {
|
|
181
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
182
|
+
return manifest && typeof manifest === 'object' && !Array.isArray(manifest) ? manifest : null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resolveArtifactValidator() {
|
|
189
|
+
let packageRoot;
|
|
190
|
+
try { packageRoot = fs.realpathSync(path.resolve(__dirname, '..', '..')); } catch { return null; }
|
|
191
|
+
const rootManifest = readPackageManifest(packageRoot);
|
|
192
|
+
const buildManifest = readPackageManifest(path.join(packageRoot, 'build'));
|
|
193
|
+
if (rootManifest?.name !== '@kontourai/flow-agents'
|
|
194
|
+
&& buildManifest?.name !== '@kontourai/flow-agents') return null;
|
|
195
|
+
const builtValidator = path.join(packageRoot, 'build', 'src', 'cli', 'validate-workflow-artifacts.js');
|
|
196
|
+
try {
|
|
197
|
+
const stat = fs.lstatSync(builtValidator);
|
|
198
|
+
if (stat.isFile() && !stat.isSymbolicLink()) return { packageRoot, builtValidator };
|
|
199
|
+
} catch { /* validator is unavailable until this package is built */ }
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
167
203
|
function sidecarValidation(root, artifactDir) {
|
|
168
204
|
const requireSidecars = String(process.env.FLOW_AGENTS_REQUIRE_SIDECARS || '').toLowerCase() === 'true';
|
|
169
205
|
const requireCritique = String(process.env.FLOW_AGENTS_REQUIRE_CRITIQUE || '').toLowerCase() === 'true';
|
|
170
206
|
if (!requireSidecars && !requireCritique && !hasSidecars(artifactDir)) return [];
|
|
171
207
|
|
|
172
|
-
const packageRoot = fs.existsSync(path.join(root, 'package.json'))
|
|
173
|
-
? root
|
|
174
|
-
: path.resolve(__dirname, '..', '..');
|
|
175
|
-
|
|
176
208
|
let sidecarFiles = [];
|
|
177
209
|
try {
|
|
178
210
|
sidecarFiles = fs.readdirSync(artifactDir)
|
|
@@ -210,36 +242,25 @@ function sidecarValidation(root, artifactDir) {
|
|
|
210
242
|
|
|
211
243
|
if (sidecarFiles.length === 0) return [];
|
|
212
244
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
245
|
+
// Resolve validation from Flow Agents itself. A consumer package.json does not imply
|
|
246
|
+
// that the consumer owns this script; installed hooks commonly run inside unrelated
|
|
247
|
+
// Node repositories whose npm scripts must not become Flow Agents validators.
|
|
248
|
+
const validator = resolveArtifactValidator();
|
|
249
|
+
if (!validator) {
|
|
250
|
+
return [`${relative(root, artifactDir)} sidecar validation skipped: Flow Agents validator is unavailable`];
|
|
251
|
+
}
|
|
219
252
|
|
|
220
253
|
const validatorArgs = ['--skip-markdown-validation'];
|
|
221
254
|
if (requireSidecars) validatorArgs.push('--require-sidecars');
|
|
222
255
|
if (requireCritique) validatorArgs.push('--require-critique');
|
|
223
256
|
validatorArgs.push(artifactDir);
|
|
224
257
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
timeout: 30000,
|
|
232
|
-
});
|
|
233
|
-
} else {
|
|
234
|
-
// Dev checkout without build/: fall back to npm run (may trigger tsc).
|
|
235
|
-
// If this also fails due to environment issues, Part 2 handles it below.
|
|
236
|
-
const npmArgs = ['run', 'workflow:validate-artifacts', '--silent', '--', ...validatorArgs];
|
|
237
|
-
result = spawnSync('npm', npmArgs, {
|
|
238
|
-
cwd: packageRoot,
|
|
239
|
-
encoding: 'utf8',
|
|
240
|
-
timeout: 30000,
|
|
241
|
-
});
|
|
242
|
-
}
|
|
258
|
+
// Direct node invocation: no PATH executable lookup and no consumer npm script.
|
|
259
|
+
const result = spawnSync(process.execPath, [validator.builtValidator, ...validatorArgs], {
|
|
260
|
+
cwd: validator.packageRoot,
|
|
261
|
+
encoding: 'utf8',
|
|
262
|
+
timeout: 30000,
|
|
263
|
+
});
|
|
243
264
|
|
|
244
265
|
// Part 2 fix: treat validator-environment failures as SKIP, never as blocking.
|
|
245
266
|
// A spawn error (ENOENT, timeout) means the validator couldn't run at all.
|
|
@@ -335,14 +356,14 @@ async function tryLoadSurface() {
|
|
|
335
356
|
// returns null when build/ is absent, require throws, or current.json has no
|
|
336
357
|
// active_flow_id / active_step_id. The caller (bundleEnforcement, sidecarGuidance)
|
|
337
358
|
// treats null as "no active FlowDefinition" and falls back to the workflow.* path.
|
|
338
|
-
function loadActiveFlowStep(flowAgentsDir) {
|
|
359
|
+
function loadActiveFlowStep(flowAgentsDir, actorKey) {
|
|
339
360
|
const packageRoot = path.resolve(__dirname, '..', '..');
|
|
340
361
|
const builtResolver = path.join(packageRoot, 'build', 'src', 'lib', 'flow-resolver.js');
|
|
341
362
|
if (!fs.existsSync(builtResolver)) return null; // hasBuild guard: no build/ yet
|
|
342
363
|
try {
|
|
343
364
|
const resolver = require(builtResolver);
|
|
344
365
|
if (typeof resolver.resolveActiveFlowStep !== 'function') return null;
|
|
345
|
-
return resolver.resolveActiveFlowStep(flowAgentsDir);
|
|
366
|
+
return resolver.resolveActiveFlowStep(flowAgentsDir, actorKey);
|
|
346
367
|
} catch {
|
|
347
368
|
return null; // require failed or resolver threw — fail-open
|
|
348
369
|
}
|
|
@@ -1745,7 +1766,7 @@ async function bundleEnforcement(artifactDir, activeFlowStep) {
|
|
|
1745
1766
|
* to scanning all of .kontourai/flow-agents (newest-mtime).
|
|
1746
1767
|
*/
|
|
1747
1768
|
function preferredArtifactDir(flowAgentsDir) {
|
|
1748
|
-
const { payload: current } =
|
|
1769
|
+
const { payload: current } = readOwnCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
|
|
1749
1770
|
if (!current) return null;
|
|
1750
1771
|
const slug = current.artifact_dir || current.active_slug;
|
|
1751
1772
|
if (typeof slug !== 'string' || !slug.trim()) return null;
|
|
@@ -1760,9 +1781,9 @@ function hasSidecarPresence(artifactDir) {
|
|
|
1760
1781
|
}
|
|
1761
1782
|
|
|
1762
1783
|
function canonicalFlowState(root, artifactDir) {
|
|
1763
|
-
if (!artifactDir) return { state: null, error: null };
|
|
1784
|
+
if (!artifactDir) return { state: null, definition: null, error: null };
|
|
1764
1785
|
const slug = path.basename(artifactDir);
|
|
1765
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return { state: null, error: 'canonical Flow run slug is malformed' };
|
|
1786
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) return { state: null, definition: null, error: 'canonical Flow run slug is malformed' };
|
|
1766
1787
|
const runDir = path.join(root, '.kontourai', 'flow', 'runs', slug);
|
|
1767
1788
|
const components = [
|
|
1768
1789
|
path.join(root, '.kontourai'),
|
|
@@ -1771,22 +1792,105 @@ function canonicalFlowState(root, artifactDir) {
|
|
|
1771
1792
|
runDir,
|
|
1772
1793
|
];
|
|
1773
1794
|
try {
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
const
|
|
1779
|
-
const stat = fs.lstatSync(file);
|
|
1780
|
-
if (stat.isSymbolicLink() || !stat.isFile()) return { state: null, error: 'canonical Flow state must be a non-symlink regular file' };
|
|
1781
|
-
const state = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1795
|
+
const parents = components.map(component => secureDirectoryIdentity(component, 'canonical Flow run parent'));
|
|
1796
|
+
const stateRead = readSecureCanonicalJson(path.join(runDir, 'state.json'), 'canonical Flow state', parents, MAX_CANONICAL_FLOW_STATE_BYTES);
|
|
1797
|
+
const definitionRead = readSecureCanonicalJson(path.join(runDir, 'definition.json'), 'canonical Flow definition', parents, MAX_CANONICAL_FLOW_DEFINITION_BYTES);
|
|
1798
|
+
const state = stateRead.value;
|
|
1799
|
+
const definition = definitionRead.value;
|
|
1782
1800
|
if (!state || typeof state !== 'object' || Array.isArray(state)
|
|
1783
|
-
||
|
|
1801
|
+
|| !CANONICAL_FLOW_STATUSES.has(state.status)
|
|
1802
|
+
|| state.run_id !== slug
|
|
1803
|
+
|| typeof state.definition_id !== 'string' || !state.definition_id
|
|
1804
|
+
|| typeof state.definition_version !== 'string' || !state.definition_version
|
|
1784
1805
|
|| typeof state.current_step !== 'string' || !state.current_step.trim()) {
|
|
1785
|
-
return { state: null, error: 'canonical Flow state is malformed' };
|
|
1806
|
+
return { state: null, definition: null, error: 'canonical Flow state is malformed' };
|
|
1807
|
+
}
|
|
1808
|
+
if (!definition || typeof definition !== 'object' || Array.isArray(definition)
|
|
1809
|
+
|| typeof definition.id !== 'string' || !definition.id
|
|
1810
|
+
|| typeof definition.version !== 'string' || !definition.version
|
|
1811
|
+
|| !Array.isArray(definition.steps) || definition.steps.length === 0
|
|
1812
|
+
|| definition.steps.some(step => !step || typeof step !== 'object' || Array.isArray(step) || typeof step.id !== 'string' || !step.id)) {
|
|
1813
|
+
return { state: null, definition: null, error: 'canonical Flow definition is malformed' };
|
|
1814
|
+
}
|
|
1815
|
+
if (state.definition_id !== definition.id || state.definition_version !== definition.version) {
|
|
1816
|
+
return { state: null, definition: null, error: 'canonical Flow definition identity does not match state' };
|
|
1786
1817
|
}
|
|
1787
|
-
|
|
1818
|
+
if (!definition.steps.some(step => step.id === state.current_step)) {
|
|
1819
|
+
return { state: null, definition: null, error: 'canonical Flow current_step is not present in definition' };
|
|
1820
|
+
}
|
|
1821
|
+
assertSecureCanonicalReadStable(stateRead);
|
|
1822
|
+
assertSecureCanonicalReadStable(definitionRead);
|
|
1823
|
+
assertSecureDirectoriesStable(parents);
|
|
1824
|
+
return { state, definition, error: null };
|
|
1788
1825
|
} catch (error) {
|
|
1789
|
-
return { state: null, error: `canonical Flow state is unavailable or malformed: ${safeOneLine(error && error.message || error, 120)}` };
|
|
1826
|
+
return { state: null, definition: null, error: `canonical Flow state is unavailable or malformed: ${safeOneLine(error && error.message || error, 120)}` };
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function readSecureCanonicalJson(file, label, parents, maxBytes) {
|
|
1831
|
+
const expected = secureFileIdentity(file, label);
|
|
1832
|
+
if (expected.size > maxBytes) throw new Error(`${label} exceeds maximum size`);
|
|
1833
|
+
assertSecureDirectoriesStable(parents);
|
|
1834
|
+
const noFollow = typeof fs.constants.O_NOFOLLOW === 'number' ? fs.constants.O_NOFOLLOW : 0;
|
|
1835
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
1836
|
+
let bytes;
|
|
1837
|
+
try {
|
|
1838
|
+
const opened = fs.fstatSync(fd);
|
|
1839
|
+
if (!opened.isFile() || opened.size > maxBytes || !sameFileIdentity(expected, opened)) throw new Error(`${label} identity changed while opening`);
|
|
1840
|
+
bytes = fs.readFileSync(fd);
|
|
1841
|
+
const after = fs.fstatSync(fd);
|
|
1842
|
+
if (!sameFileIdentity(expected, after)) throw new Error(`${label} identity changed while reading`);
|
|
1843
|
+
} finally {
|
|
1844
|
+
fs.closeSync(fd);
|
|
1845
|
+
}
|
|
1846
|
+
const final = secureFileIdentity(file, label);
|
|
1847
|
+
if (!sameFileIdentity(expected, final)) throw new Error(`${label} identity changed after reading`);
|
|
1848
|
+
assertSecureDirectoriesStable(parents);
|
|
1849
|
+
return { value: JSON.parse(bytes.toString('utf8')), file, label, identity: expected };
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function assertSecureCanonicalReadStable(read) {
|
|
1853
|
+
const final = secureFileIdentity(read.file, read.label);
|
|
1854
|
+
if (!sameFileIdentity(read.identity, final)) throw new Error(`${read.label} identity changed before authorization`);
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function secureDirectoryIdentity(target, label) {
|
|
1858
|
+
const stat = fs.lstatSync(target);
|
|
1859
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`${label} must be a non-symlink directory: ${target}`);
|
|
1860
|
+
return { path: target, realpath: fs.realpathSync(target), dev: stat.dev, ino: stat.ino };
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function secureFileIdentity(target, label) {
|
|
1864
|
+
const stat = fs.lstatSync(target);
|
|
1865
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error(`${label} must be a non-symlink regular file`);
|
|
1866
|
+
return { dev: stat.dev, ino: stat.ino, size: stat.size };
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function sameFileIdentity(left, right) {
|
|
1870
|
+
return Boolean(left && right && left.dev === right.dev && left.ino === right.ino);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function assertSecureDirectoriesStable(parents) {
|
|
1874
|
+
for (const parent of parents) {
|
|
1875
|
+
const current = secureDirectoryIdentity(parent.path, 'canonical Flow run parent');
|
|
1876
|
+
if (!sameFileIdentity(parent, current) || current.realpath !== parent.realpath) throw new Error('canonical Flow run parent identity changed');
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
function validatedActiveTurnScope(root) {
|
|
1881
|
+
try {
|
|
1882
|
+
const runId = process.env.FLOW_AGENTS_CONTINUATION_RUN_ID;
|
|
1883
|
+
const turnSecret = process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET;
|
|
1884
|
+
if (typeof runId !== 'string' || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(runId) || typeof turnSecret !== 'string' || !turnSecret) return null;
|
|
1885
|
+
const artifactRoot = path.resolve(flowAgentsArtifactRoot(root));
|
|
1886
|
+
const candidate = path.resolve(artifactRoot, runId);
|
|
1887
|
+
if (path.dirname(candidate) !== artifactRoot) return null;
|
|
1888
|
+
const base = validateSignedActiveTurnAssignmentAuthority({ sessionDir: candidate, runId, turnSecret });
|
|
1889
|
+
if (!base.valid) return null;
|
|
1890
|
+
const canonicalFlow = canonicalFlowState(root, candidate);
|
|
1891
|
+
return { artifactDir: candidate, canonicalFlow, baseAuthority: base };
|
|
1892
|
+
} catch {
|
|
1893
|
+
return null;
|
|
1790
1894
|
}
|
|
1791
1895
|
}
|
|
1792
1896
|
|
|
@@ -1794,7 +1898,7 @@ function canonicalFlowState(root, artifactDir) {
|
|
|
1794
1898
|
// return that slug so analyze() can log the staleness rather than silently falling back to
|
|
1795
1899
|
// a global mtime scan that could resurface an abandoned/never-real session as active.
|
|
1796
1900
|
function staleCurrentSlug(flowAgentsDir) {
|
|
1797
|
-
const { payload: current } =
|
|
1901
|
+
const { payload: current } = readOwnCurrentPointer(flowAgentsDir, resolveActor(process.env).actor);
|
|
1798
1902
|
if (!current) return null;
|
|
1799
1903
|
const slug = current.artifact_dir || current.active_slug;
|
|
1800
1904
|
if (typeof slug !== 'string' || !slug.trim()) return null;
|
|
@@ -1908,9 +2012,35 @@ const FULL_BLOCK = /status:|Definition Of Done|Goal Fit|sidecar validation:|cont
|
|
|
1908
2012
|
|
|
1909
2013
|
async function analyze(root, now = Date.now()) {
|
|
1910
2014
|
const flowAgentsDirs = flowAgentsArtifactRootsForRead(root);
|
|
2015
|
+
const { actor: actorKey } = resolveActor(process.env);
|
|
2016
|
+
const activeTurnScope = validatedActiveTurnScope(root);
|
|
1911
2017
|
// Scope to the session's current task when current.json names one, so an
|
|
1912
2018
|
// unrelated active workflow elsewhere in the repo does not gate this stop.
|
|
1913
|
-
const scoped = flowAgentsDirs.map(preferredArtifactDir).find(Boolean);
|
|
2019
|
+
const scoped = activeTurnScope?.artifactDir || flowAgentsDirs.map(preferredArtifactDir).find(Boolean);
|
|
2020
|
+
|
|
2021
|
+
// #440 D1/D2: a RESOLVED actor with no scoped own artifactDir (no per-actor pointer, or an
|
|
2022
|
+
// own pointer naming a nonexistent dir) never falls back to the legacy current.json or a
|
|
2023
|
+
// global newest-mtime scan for BLOCKING purposes — those are informational-only for a
|
|
2024
|
+
// resolved actor. Accepted gap: this actor's stop stays ungated until its next
|
|
2025
|
+
// workflow-sidecar command re-establishes its own per-actor pointer; it is never gated on
|
|
2026
|
+
// another actor's unrelated work. #589: a signed continuation active-turn scope (validated via
|
|
2027
|
+
// FLOW_AGENTS_CONTINUATION_RUN_ID/TURN_SECRET, independent of actor identity/ownership) is
|
|
2028
|
+
// resolved into `activeTurnScope` above and OR'd into `scoped` first, so a resolved actor
|
|
2029
|
+
// legitimately handling an authorized continuation-driver turn is never treated as "no own
|
|
2030
|
+
// work to scope to" here, even without its own per-actor current pointer — this check only
|
|
2031
|
+
// fires when NEITHER mechanism finds a scope.
|
|
2032
|
+
if (!scoped && !isUnresolvedActor(actorKey)) {
|
|
2033
|
+
const ownStale = flowAgentsDirs.map(staleCurrentSlug).find(Boolean);
|
|
2034
|
+
process.stderr.write(ownStale
|
|
2035
|
+
? `[Hook] Goal Fit: actor "${safeOneLine(actorKey, 80)}"'s own current-pointer names slug "${safeOneLine(ownStale, 80)}" but no such session directory exists — ignoring the stale own pointer; other sessions' sidecars are informational only, not blocking (#440).\n`
|
|
2036
|
+
: `[Hook] Goal Fit: no per-actor current-pointer for actor "${safeOneLine(actorKey, 80)}" — stop-gate evidence scoping finds no own work to scope to; not blocking (any workflow-sidecar command re-establishes it; other sessions' sidecars are informational only, #440).\n`);
|
|
2037
|
+
return { warnings: [], blocking: false, activeFlowRun: false, latestArtifactDir: null };
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Everything below this point is UNCHANGED for: (a) a resolved actor with a valid `scoped`
|
|
2041
|
+
// own artifactDir (the common, post-fix path), (b) an unresolved actor (full legacy +
|
|
2042
|
+
// global-scan compat, D3), and (c) an authorized continuation-driver turn (#589, scoped via
|
|
2043
|
+
// activeTurnScope regardless of actor ownership).
|
|
1914
2044
|
// WS8 (AC10a): if current.json points at a nonexistent slug, LOG the staleness and, in
|
|
1915
2045
|
// the global fallback, require sidecar presence so a stale pointer cannot resurface an
|
|
1916
2046
|
// abandoned/never-real markdown-only directory as "the active session".
|
|
@@ -1928,7 +2058,9 @@ async function analyze(root, now = Date.now()) {
|
|
|
1928
2058
|
artifacts = artifacts.filter(a => a && hasSidecarPresence(path.dirname(a.file)));
|
|
1929
2059
|
}
|
|
1930
2060
|
|
|
1931
|
-
const scopedCanonicalFlow =
|
|
2061
|
+
const scopedCanonicalFlow = activeTurnScope && scoped === activeTurnScope.artifactDir
|
|
2062
|
+
? activeTurnScope.canonicalFlow
|
|
2063
|
+
: canonicalFlowState(root, scoped);
|
|
1932
2064
|
const scopedState = scoped ? readJsonFile(path.join(scoped, 'state.json')) : null;
|
|
1933
2065
|
const scopedProjectedActive = Boolean(scopedState && scopedState.flow_run && normalizedStatus(scopedState.flow_run.status) === 'active');
|
|
1934
2066
|
if (scopedProjectedActive && scopedCanonicalFlow.error) {
|
|
@@ -1940,18 +2072,25 @@ async function analyze(root, now = Date.now()) {
|
|
|
1940
2072
|
gatePrefix: '[stop-gate]',
|
|
1941
2073
|
};
|
|
1942
2074
|
}
|
|
2075
|
+
if (activeTurnScope && scoped === activeTurnScope.artifactDir && scopedCanonicalFlow.error) {
|
|
2076
|
+
return {
|
|
2077
|
+
warnings: [`workflow state: canonical Flow state is unsafe or malformed for the signed continuation session: ${scopedCanonicalFlow.error}. Resolve the canonical run before stopping.`],
|
|
2078
|
+
blocking: true,
|
|
2079
|
+
activeFlowRun: false,
|
|
2080
|
+
activeTurnAuthority: false,
|
|
2081
|
+
latestArtifactDir: scoped,
|
|
2082
|
+
gatePrefix: '[stop-gate]',
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
1943
2085
|
if (artifacts.length === 0) {
|
|
1944
|
-
if (
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
gatePrefix: '[stop-gate]',
|
|
1952
|
-
};
|
|
2086
|
+
if (scoped) {
|
|
2087
|
+
const stateFile = path.join(scoped, 'state.json');
|
|
2088
|
+
let mtimeMs = now;
|
|
2089
|
+
try { mtimeMs = fs.statSync(stateFile).mtimeMs; } catch { /* missing state is evaluated below */ }
|
|
2090
|
+
artifacts = [{ file: stateFile, status: normalizedStatus(scopedState?.status || 'unknown'), type: 'deliver', mtimeMs }];
|
|
2091
|
+
} else {
|
|
2092
|
+
return { warnings: [], blocking: false, activeFlowRun: false, latestArtifactDir: null };
|
|
1953
2093
|
}
|
|
1954
|
-
return { warnings: [], blocking: false, activeFlowRun: false, latestArtifactDir: null };
|
|
1955
2094
|
}
|
|
1956
2095
|
|
|
1957
2096
|
const latest = artifacts[0];
|
|
@@ -1971,7 +2110,7 @@ async function analyze(root, now = Date.now()) {
|
|
|
1971
2110
|
|
|
1972
2111
|
// ADR 0016 P-c: load the active FlowDefinition gate (fail-open: null when absent).
|
|
1973
2112
|
// Null → existing workflow.* fallback path unchanged. Non-null → expects[]-driven claim selection.
|
|
1974
|
-
const activeFlowStep = loadActiveFlowStep(path.dirname(latestArtifactDir));
|
|
2113
|
+
const activeFlowStep = loadActiveFlowStep(path.dirname(latestArtifactDir), actorKey);
|
|
1975
2114
|
|
|
1976
2115
|
warnings.push(...sidecarValidation(root, latestArtifactDir));
|
|
1977
2116
|
warnings.push(...sidecarGuidance(root, latestArtifactDir, activeFlowStep));
|
|
@@ -2020,7 +2159,9 @@ async function analyze(root, now = Date.now()) {
|
|
|
2020
2159
|
// Use module-scope HARD_BLOCK / FULL_BLOCK (defined above analyze()).
|
|
2021
2160
|
// pre-execution/terminal tasks: only HARD_BLOCK signals cause a block.
|
|
2022
2161
|
// execution-onward tasks: FULL_BLOCK signals cause a block.
|
|
2023
|
-
const canonicalFlow =
|
|
2162
|
+
const canonicalFlow = activeTurnScope && latestArtifactDir === activeTurnScope.artifactDir
|
|
2163
|
+
? activeTurnScope.canonicalFlow
|
|
2164
|
+
: canonicalFlowState(root, latestArtifactDir);
|
|
2024
2165
|
const activeProjectedFlow = gateState && gateState.flow_run && normalizedStatus(gateState.flow_run.status) === 'active';
|
|
2025
2166
|
const unsafeActiveCanonical = Boolean(activeProjectedFlow && canonicalFlow.error);
|
|
2026
2167
|
if (unsafeActiveCanonical) {
|
|
@@ -2033,12 +2174,38 @@ async function analyze(root, now = Date.now()) {
|
|
|
2033
2174
|
warnings.push(`workflow state: canonical Flow run remains active at step ${activeStep}; complete or explicitly cancel the run before stopping.`);
|
|
2034
2175
|
}
|
|
2035
2176
|
const blockRe = ((preExecution && !activeFlowRun) || terminal) ? HARD_BLOCK : FULL_BLOCK;
|
|
2036
|
-
const
|
|
2177
|
+
const activeTurnAuthority = activeFlowRun && canonicalFlow.state && !unsafeActiveCanonical
|
|
2178
|
+
? validateActiveTurnAuthority({
|
|
2179
|
+
sessionDir: latestArtifactDir,
|
|
2180
|
+
runId: process.env.FLOW_AGENTS_CONTINUATION_RUN_ID,
|
|
2181
|
+
turnSecret: process.env.FLOW_AGENTS_CONTINUATION_TURN_SECRET,
|
|
2182
|
+
canonicalState: canonicalFlow.state,
|
|
2183
|
+
})
|
|
2184
|
+
: { valid: false, reason: 'canonical Flow is not safely active' };
|
|
2185
|
+
if (activeTurnAuthority.valid) {
|
|
2186
|
+
warnings.push('continuation driver active turn is authorized; the ordinary unfinished canonical Flow gate is advisory until this adapter turn returns.');
|
|
2187
|
+
}
|
|
2188
|
+
const blockingRe = activeTurnAuthority.valid ? FULL_BLOCK : blockRe;
|
|
2189
|
+
const blocking = (activeFlowRun && !activeTurnAuthority.valid) || warnings.some(w => {
|
|
2037
2190
|
// Capture cross-reference warn-mode notes never block (operator opted out).
|
|
2038
2191
|
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
2039
|
-
|
|
2192
|
+
if (activeTurnAuthority.valid && isOrdinaryActiveGateWarning(w, relPath)) return false;
|
|
2193
|
+
return blockingRe.test(w);
|
|
2040
2194
|
});
|
|
2041
|
-
return { warnings, blocking, activeFlowRun, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
2195
|
+
return { warnings, blocking, activeFlowRun, activeTurnAuthority: activeTurnAuthority.valid, preExecution, gatePrefix: gateLabel(activeFlowStep), latestArtifactDir };
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
function isOrdinaryActiveGateWarning(warning, relPath) {
|
|
2199
|
+
return warning.startsWith(`${relPath} is still status:`)
|
|
2200
|
+
|| /(?:^|\/)\.kontourai\/flow-agents\/[^/]+ workflow state:/.test(warning)
|
|
2201
|
+
|| /(?:^|\/)\.kontourai\/flow-agents\/[^/]+ (?:next action|required skills|required operations|next command):/.test(warning)
|
|
2202
|
+
|| /canonical Flow run remains active at step .+; complete or explicitly cancel the run before stopping\.$/.test(warning);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
function isHardStopWarning(warning, relPath, activeTurnAuthority) {
|
|
2206
|
+
if (/\[backstop in warn mode — not blocking\]/.test(warning)) return false;
|
|
2207
|
+
if (!activeTurnAuthority) return HARD_BLOCK.test(warning);
|
|
2208
|
+
return FULL_BLOCK.test(warning) && !isOrdinaryActiveGateWarning(warning, relPath);
|
|
2042
2209
|
}
|
|
2043
2210
|
|
|
2044
2211
|
/**
|
|
@@ -2286,9 +2453,16 @@ function releaseOnNonTerminalStop(root, artifactDir) {
|
|
|
2286
2453
|
|
|
2287
2454
|
const state = readJsonFile(path.join(artifactDir, 'state.json'));
|
|
2288
2455
|
if (!state) return; // AC5: no state.json — nothing to gate a release decision on.
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2456
|
+
const signedScope = validatedActiveTurnScope(root);
|
|
2457
|
+
const exactSignedScope = signedScope && signedScope.artifactDir === artifactDir;
|
|
2458
|
+
const canonicalFlow = exactSignedScope ? signedScope.canonicalFlow : canonicalFlowState(root, artifactDir);
|
|
2459
|
+
const canonicalStatus = normalizedStatus(canonicalFlow.state?.status);
|
|
2460
|
+
const projectedCanonicalStatus = normalizedStatus(state.flow_run?.status);
|
|
2461
|
+
const canonicalOwned = CANONICAL_FLOW_STATUSES.has(canonicalStatus) && !CANONICAL_FLOW_TERMINAL_STATUSES.has(canonicalStatus);
|
|
2462
|
+
const projectedCanonicalOwned = CANONICAL_FLOW_STATUSES.has(projectedCanonicalStatus) && !CANONICAL_FLOW_TERMINAL_STATUSES.has(projectedCanonicalStatus);
|
|
2463
|
+
if ((exactSignedScope && canonicalFlow.error) || canonicalOwned || projectedCanonicalOwned) {
|
|
2464
|
+
const ownershipLabel = canonicalStatus === 'active' || projectedCanonicalStatus === 'active' ? 'active Flow run' : 'nonterminal Flow run';
|
|
2465
|
+
process.stderr.write(`[Hook] Goal Fit: stop-hook release skipped for ${ownershipLabel} "${safeOneLine(state.flow_run?.run_id || state.task_slug || path.basename(artifactDir), 80)}"; continuation remains governed by canonical Flow state.\n`);
|
|
2292
2466
|
return;
|
|
2293
2467
|
}
|
|
2294
2468
|
|
|
@@ -2434,15 +2608,12 @@ async function run(rawInput) {
|
|
|
2434
2608
|
// with runtime-constructed paths or by modifying the warning
|
|
2435
2609
|
// text so the hash changes. The real anchor is external (signed checkpoints + human
|
|
2436
2610
|
// review). This raises the cost of the burn-through-the-counter escape vector.
|
|
2437
|
-
const isHardBlock = result.activeFlowRun || result.warnings.some(w =>
|
|
2438
|
-
if (/\[backstop in warn mode — not blocking\]/.test(w)) return false;
|
|
2439
|
-
return HARD_BLOCK.test(w);
|
|
2440
|
-
});
|
|
2611
|
+
const isHardBlock = (result.activeFlowRun && !result.activeTurnAuthority) || result.warnings.some(w => isHardStopWarning(w, relative(root, result.latestArtifactDir || root), result.activeTurnAuthority));
|
|
2441
2612
|
if (isHardBlock) {
|
|
2442
2613
|
// Do NOT clear the streak — keep accumulating so the same hard block stays visible.
|
|
2443
2614
|
return {
|
|
2444
2615
|
stdout: rawInput,
|
|
2445
|
-
stderr: result.activeFlowRun
|
|
2616
|
+
stderr: result.activeFlowRun && !result.activeTurnAuthority
|
|
2446
2617
|
? `${message}\n${gatePrefix} max-blocks reached but canonical Flow remains active — not auto-releasing; complete or explicitly cancel the run.`
|
|
2447
2618
|
: `${message}\n${gatePrefix} max-blocks reached but the block is a caught false-completion / integrity failure — not auto-releasing; requires a real fix or operator override.`,
|
|
2448
2619
|
exitCode: 2,
|
|
@@ -2493,4 +2664,4 @@ if (require.main === module) {
|
|
|
2493
2664
|
});
|
|
2494
2665
|
}
|
|
2495
2666
|
|
|
2496
|
-
module.exports = { analyze, run, resolveGoalFitMode, uncheckedInSection, findRepoRoot, sidecarGuidance, safeOneLine, captureCrossReference, bundleEnforcement, loadActiveFlowStep, readCommandLog, resolveTrustedCommand, declaredManifestTarget, verifyCommandLogChain, CHAIN_GENESIS_VERIFY, hasLaunderingOperator, releaseOnNonTerminalStop };
|
|
2667
|
+
module.exports = { analyze, run, resolveGoalFitMode, uncheckedInSection, findRepoRoot, sidecarGuidance, safeOneLine, captureCrossReference, bundleEnforcement, loadActiveFlowStep, readCommandLog, resolveTrustedCommand, declaredManifestTarget, verifyCommandLogChain, CHAIN_GENESIS_VERIFY, hasLaunderingOperator, releaseOnNonTerminalStop, isHardStopWarning, canonicalFlowState };
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { readLivenessEvents, freshHolders } = require('./lib/liveness-read');
|
|
20
|
-
const { resolveActor } = require('./lib/actor-identity');
|
|
20
|
+
const { resolveActor, isUnresolvedActor } = require('./lib/actor-identity');
|
|
21
21
|
const { flowAgentsArtifactRootsForRead, resolveSharedRepoRoot, warnIfFailingOpenInsideGitTree } = require('./lib/local-artifact-paths');
|
|
22
|
-
const {
|
|
22
|
+
const { readOwnCurrentPointer } = require('./lib/current-pointer');
|
|
23
23
|
const { workflowTriggersFor } = require('./lib/kit-catalog');
|
|
24
24
|
|
|
25
25
|
const STEERING = {
|
|
@@ -129,17 +129,20 @@ function readJson(file) {
|
|
|
129
129
|
/**
|
|
130
130
|
* #291 (Conflict #2, Wave 2 Task 2.4): actor-scoped preference consulted BEFORE the global
|
|
131
131
|
* newest-mtime scan below. For each read-root, resolve the "current" pointer via
|
|
132
|
-
* `
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
132
|
+
* `readOwnCurrentPointer` and, if it names a session dir whose `state.json` is still in an
|
|
133
|
+
* ACTIVE_STATE_STATUSES status, return that state immediately — so actor A's own ambient
|
|
134
|
+
* steering hint always prefers A's own current task over a globally-newer-mtime `state.json`
|
|
135
|
+
* some other actor (B) happened to touch more recently. Returns null (never throws) when no root
|
|
136
|
+
* yields an actor-scoped resolution.
|
|
137
|
+
*
|
|
138
|
+
* #440: for a RESOLVED `actorKey`, `readOwnCurrentPointer` reads ONLY that actor's own per-actor
|
|
139
|
+
* `current/<actor>.json` projection — it never falls back to the shared legacy `current.json`.
|
|
140
|
+
* The caller (`latestWorkflowState`) gates its own global-scan fallback below to UNRESOLVED
|
|
141
|
+
* actors only (D1) — so a resolved actor with no own per-actor pointer yet now correctly returns
|
|
142
|
+
* null all the way up (no banner) rather than grounding onto another actor's globally-newer
|
|
143
|
+
* `state.json`, the exact cross-actor steering bug #440 fixes. Only for an empty/unresolved
|
|
144
|
+
* `actorKey` does `readOwnCurrentPointer` delegate to `readCurrentPointer`, which DOES fall back
|
|
145
|
+
* to the legacy global file — that compat-shim degrade (D3) is unchanged from before #291/#440.
|
|
143
146
|
*
|
|
144
147
|
* @param {string} root
|
|
145
148
|
* @param {string} actorKey
|
|
@@ -147,7 +150,7 @@ function readJson(file) {
|
|
|
147
150
|
*/
|
|
148
151
|
function actorScopedWorkflowState(root, actorKey) {
|
|
149
152
|
for (const flowAgentsDir of flowAgentsArtifactRootsForRead(root)) {
|
|
150
|
-
const { payload: current } =
|
|
153
|
+
const { payload: current } = readOwnCurrentPointer(flowAgentsDir, actorKey);
|
|
151
154
|
if (!current) continue;
|
|
152
155
|
const slug = current.artifact_dir || current.active_slug;
|
|
153
156
|
if (typeof slug !== 'string' || !slug.trim()) continue;
|
|
@@ -165,9 +168,15 @@ function actorScopedWorkflowState(root, actorKey) {
|
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
function latestWorkflowState(root) {
|
|
168
|
-
const
|
|
171
|
+
const actorKey = resolveActor(process.env).actor;
|
|
172
|
+
const preferred = actorScopedWorkflowState(root, actorKey);
|
|
169
173
|
if (preferred) return preferred;
|
|
170
174
|
|
|
175
|
+
// #440 D1/D2: a resolved actor's RESUME/STATE/SUPERSESSION banners never re-ground onto
|
|
176
|
+
// another actor's session via the global newest-mtime scan below — that fallback is
|
|
177
|
+
// legacy/unresolved-actor compat only (D3). No own active state this turn -> no banner.
|
|
178
|
+
if (!isUnresolvedActor(actorKey)) return null;
|
|
179
|
+
|
|
171
180
|
const states = flowAgentsArtifactRootsForRead(root)
|
|
172
181
|
.flatMap(artifactRoot => walkStateFiles(artifactRoot))
|
|
173
182
|
.map(file => {
|
|
@@ -32,14 +32,26 @@ pricing_registry() {
|
|
|
32
32
|
age=$(( now - mtime ))
|
|
33
33
|
if [[ "$age" -lt "$ttl" ]] && pricing_registry_valid_json < "$cache"; then cat "$cache"; return 0; fi
|
|
34
34
|
fi
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
# Cache-only callers (per-tool-call join; see usage.sh's usage_last_turn_usage,
|
|
36
|
+
# which runs once per tool call, not once per session) never perform a
|
|
37
|
+
# network fetch here. A STALE-but-JSON-valid cache is still served (by the
|
|
38
|
+
# unconditional `[[ -f "$cache" ]] && pricing_registry_valid_json ...`
|
|
39
|
+
# fallback just below) before ever falling through to the bundled
|
|
40
|
+
# snapshot -- only a genuinely MISSING (or invalid) cache reaches the
|
|
41
|
+
# bundled snapshot. This still avoids blocking on a `curl --max-time 5`
|
|
42
|
+
# round-trip (and racing this function's fixed ${cache}.tmp path) on
|
|
43
|
+
# every tool call. session.usage (once per session, at stop) is
|
|
44
|
+
# unaffected and still resolves the registry normally (network-eligible).
|
|
45
|
+
if [[ "${TELEMETRY_PRICING_CACHE_ONLY:-}" != "1" ]]; then
|
|
46
|
+
if curl -fsS --max-time 5 "$url" -o "${cache}.tmp" 2>/dev/null && [[ -s "${cache}.tmp" ]]; then
|
|
47
|
+
if pricing_registry_valid_json < "${cache}.tmp"; then
|
|
48
|
+
mv "${cache}.tmp" "$cache"
|
|
49
|
+
cat "$cache"
|
|
50
|
+
return 0
|
|
51
|
+
fi
|
|
40
52
|
fi
|
|
53
|
+
rm -f "${cache}.tmp" 2>/dev/null
|
|
41
54
|
fi
|
|
42
|
-
rm -f "${cache}.tmp" 2>/dev/null
|
|
43
55
|
[[ -f "$cache" ]] && pricing_registry_valid_json < "$cache" && { cat "$cache"; return 0; }
|
|
44
56
|
fi
|
|
45
57
|
|