specrails-core 6.6.5 → 6.7.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/dist/agent-runtime/capabilities.d.ts +5 -2
- package/dist/agent-runtime/capabilities.js +8 -2
- package/dist/agent-runtime/change-scope.d.ts +2 -0
- package/dist/agent-runtime/change-scope.js +5 -0
- package/dist/agent-runtime/cli-executor.js +7 -0
- package/dist/agent-runtime/cli.d.ts +6 -1
- package/dist/agent-runtime/cli.js +15 -4
- package/dist/agent-runtime/compact/reviewer.js +3 -1
- package/dist/agent-runtime/config.js +16 -5
- package/dist/agent-runtime/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/efficiency-summary.js +7 -1
- package/dist/agent-runtime/engine/__fixtures__/implementation-fixture.js +1 -1
- package/dist/agent-runtime/engine/checkpoint/database.d.ts +9 -0
- package/dist/agent-runtime/engine/checkpoint/database.js +40 -3
- package/dist/agent-runtime/engine/checkpoint/ledger.d.ts +1 -0
- package/dist/agent-runtime/engine/checkpoint/ledger.js +7 -2
- package/dist/agent-runtime/engine/cli.d.ts +12 -0
- package/dist/agent-runtime/engine/cli.js +15 -4
- package/dist/agent-runtime/engine/definition-schema.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-schema.js +1 -0
- package/dist/agent-runtime/engine/definition-types.d.ts +1 -1
- package/dist/agent-runtime/engine/definition-types.js +1 -1
- package/dist/agent-runtime/engine/definition-validator.js +8 -0
- package/dist/agent-runtime/engine/efficiency-summary.d.ts +7 -0
- package/dist/agent-runtime/engine/graph-description.d.ts +3 -3
- package/dist/agent-runtime/engine/invocation-context.js +2 -0
- package/dist/agent-runtime/engine/piece-registry.d.ts +1 -1
- package/dist/agent-runtime/engine/piece-registry.js +1 -1
- package/dist/agent-runtime/engine/pieces/artifact-contract.d.ts +5 -0
- package/dist/agent-runtime/engine/pieces/artifact-contract.js +6 -2
- package/dist/agent-runtime/engine/pieces/derivation-coverage.d.ts +30 -0
- package/dist/agent-runtime/engine/pieces/derivation-coverage.js +95 -0
- package/dist/agent-runtime/engine/pieces/derive-change.d.ts +50 -0
- package/dist/agent-runtime/engine/pieces/derive-change.js +209 -0
- package/dist/agent-runtime/engine/pieces/index.js +2 -1
- package/dist/agent-runtime/engine/pieces/openspec.d.ts +16 -1
- package/dist/agent-runtime/engine/pieces/openspec.js +39 -30
- package/dist/agent-runtime/engine/pieces/role-turn.js +26 -5
- package/dist/agent-runtime/engine/preflight.d.ts +2 -1
- package/dist/agent-runtime/engine/preflight.js +3 -1
- package/dist/agent-runtime/engine/regex.d.ts +12 -2
- package/dist/agent-runtime/engine/regex.js +25 -11
- package/dist/agent-runtime/engine/run-status.d.ts +34 -0
- package/dist/agent-runtime/engine/run-status.js +7 -1
- package/dist/agent-runtime/engine/runs.d.ts +56 -0
- package/dist/agent-runtime/engine/runs.js +88 -5
- package/dist/agent-runtime/engine/steering/inbox.d.ts +15 -0
- package/dist/agent-runtime/engine/steering/inbox.js +21 -1
- package/dist/agent-runtime/engine/storage/private-path.js +7 -1
- package/dist/agent-runtime/evaluation.js +1 -1
- package/dist/agent-runtime/executor-types.d.ts +19 -0
- package/dist/agent-runtime/executor-types.js +10 -0
- package/dist/agent-runtime/failure-classification.d.ts +51 -0
- package/dist/agent-runtime/failure-classification.js +110 -0
- package/dist/agent-runtime/graph/artifacts.d.ts +3 -1
- package/dist/agent-runtime/graph/artifacts.js +5 -1
- package/dist/agent-runtime/graph/baseline.d.ts +50 -0
- package/dist/agent-runtime/graph/baseline.js +145 -0
- package/dist/agent-runtime/graph/capability-map.d.ts +29 -0
- package/dist/agent-runtime/graph/capability-map.js +65 -0
- package/dist/agent-runtime/graph/clarifications.d.ts +75 -0
- package/dist/agent-runtime/graph/clarifications.js +158 -0
- package/dist/agent-runtime/graph/nodes.js +178 -19
- package/dist/agent-runtime/graph/review-brief-record.d.ts +25 -0
- package/dist/agent-runtime/graph/review-brief-record.js +26 -0
- package/dist/agent-runtime/graph/review-brief.d.ts +73 -0
- package/dist/agent-runtime/graph/review-brief.js +127 -0
- package/dist/agent-runtime/graph/roles.js +5 -3
- package/dist/agent-runtime/graph/spec-trace-record.d.ts +33 -0
- package/dist/agent-runtime/graph/spec-trace-record.js +65 -0
- package/dist/agent-runtime/graph/state.d.ts +8 -0
- package/dist/agent-runtime/index.d.ts +7 -0
- package/dist/agent-runtime/index.js +7 -0
- package/dist/agent-runtime/openspec.d.ts +1 -0
- package/dist/agent-runtime/openspec.js +1 -1
- package/dist/agent-runtime/project-rules.d.ts +139 -0
- package/dist/agent-runtime/project-rules.js +683 -0
- package/dist/agent-runtime/prompts.d.ts +32 -1
- package/dist/agent-runtime/prompts.js +180 -7
- package/dist/agent-runtime/role-routing.d.ts +2 -1
- package/dist/agent-runtime/session/application/active-session.d.ts +43 -2
- package/dist/agent-runtime/session/application/active-session.js +163 -7
- package/dist/agent-runtime/session/application/session-service.d.ts +2 -2
- package/dist/agent-runtime/session/domain/descriptor.js +5 -0
- package/dist/agent-runtime/session/domain/interruption.js +1 -0
- package/dist/agent-runtime/session/domain/policy.js +2 -0
- package/dist/agent-runtime/session/domain/redaction.d.ts +19 -0
- package/dist/agent-runtime/session/domain/redaction.js +71 -0
- package/dist/agent-runtime/session/domain/types.d.ts +17 -1
- package/dist/agent-runtime/session/drivers/claude/argv.d.ts +14 -0
- package/dist/agent-runtime/session/drivers/claude/argv.js +19 -2
- package/dist/agent-runtime/session/drivers/claude/driver.js +11 -1
- package/dist/agent-runtime/session/drivers/claude/translator.d.ts +4 -0
- package/dist/agent-runtime/session/drivers/claude/translator.js +16 -3
- package/dist/agent-runtime/session/drivers/codex/driver.js +2 -0
- package/dist/agent-runtime/session/drivers/executor/driver.js +2 -0
- package/dist/agent-runtime/session/host/protocol.d.ts +12 -0
- package/dist/agent-runtime/session/host/protocol.js +10 -2
- package/dist/agent-runtime/session/host/server.js +6 -5
- package/dist/agent-runtime/session/index.d.ts +1 -1
- package/dist/agent-runtime/session/journal/sqlite-journal.js +6 -2
- package/dist/agent-runtime/session/ports.d.ts +6 -2
- package/dist/agent-runtime/session/testing/driver-conformance.js +16 -2
- package/dist/agent-runtime/session/testing/memory-journal.js +4 -2
- package/dist/installer/cli.js +10 -1
- package/dist/installer/commands/init.js +14 -3
- package/dist/installer/phases/scaffold.js +56 -5
- package/dist/pipeline/pipeline-state.d.ts +15 -0
- package/dist/pipeline/pipeline-state.js +36 -3
- package/dist/pipeline/spec-trace.d.ts +134 -0
- package/dist/pipeline/spec-trace.js +392 -0
- package/dist/shared/secret-redaction.d.ts +42 -0
- package/dist/shared/secret-redaction.js +184 -0
- package/integration-contract.json +177 -7
- package/package.json +7 -7
- package/schemas/agent-runtime.schema.json +3 -1
- package/schemas/workflow-definition.schema.json +1 -0
- package/templates/settings/codex-config.toml +3 -2
|
@@ -9,12 +9,13 @@ import { admitVerificationProposals } from '../../verification-proposals.js';
|
|
|
9
9
|
import { guardrailEnabled } from '../../guardrails.js';
|
|
10
10
|
import { installEnvironment, prepareEnvironment } from '../../compact/environment.js';
|
|
11
11
|
import { roleInstructions } from '../../prompts.js';
|
|
12
|
+
import { projectRulesContext } from '../../project-rules.js';
|
|
12
13
|
import { EngineError } from '../contracts.js';
|
|
13
14
|
import { boundedText, historyEntry, idSchema, json, paramsSchema, invocationTimers, stringSchema, text } from './shared.js';
|
|
14
15
|
const ajv = new Ajv2020({ allErrors: true, strict: true, validateFormats: false, ownProperties: true });
|
|
15
16
|
export function roleTurnPiece(bindings) {
|
|
16
17
|
return {
|
|
17
|
-
descriptor: { kind: 'role-turn', paramsSchema: paramsSchema({ ...invocationTimers, roleId: idSchema, prompt: { ...stringSchema, minLength: 1 }, structuredOutput: { type: 'object' }, sessionContinuity: { enum: ['run', 'none'] }, verificationProposalsFrom: { type: 'string', minLength: 1, maxLength: 128 } }, ['roleId', 'prompt']), outcomes: ['next', 'invalid', 'failed'], effect: 'derived', requiresAI: true, storeAccess: 'write' },
|
|
18
|
+
descriptor: { kind: 'role-turn', paramsSchema: paramsSchema({ ...invocationTimers, roleId: idSchema, prompt: { ...stringSchema, minLength: 1 }, structuredOutput: { type: 'object' }, sessionContinuity: { enum: ['run', 'none'] }, stance: { enum: ['fixer'] }, verificationProposalsFrom: { type: 'string', minLength: 1, maxLength: 128 } }, ['roleId', 'prompt']), outcomes: ['next', 'invalid', 'failed'], effect: 'derived', requiresAI: true, storeAccess: 'write' },
|
|
18
19
|
getOutcomes: params => params.structuredOutput ? ['next', 'invalid', 'failed'] : ['next', 'failed'],
|
|
19
20
|
getEffect: (params, roles) => {
|
|
20
21
|
const role = roles[text(params.roleId)];
|
|
@@ -28,6 +29,13 @@ export function roleTurnPiece(bindings) {
|
|
|
28
29
|
/** One invocation path for configured roles, including routing and exactly one repair. */
|
|
29
30
|
export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
30
31
|
const roleId = text(params.roleId), descriptor = resolveRoleDescriptor(deps.config, roleId);
|
|
32
|
+
// `stance: 'fixer'` makes the turn a correction round: only a write role can
|
|
33
|
+
// correct code. The built-in developer takes the FIXER instructions (and the
|
|
34
|
+
// fixer engine when one is configured); a custom role only labels its events.
|
|
35
|
+
const stance = params.stance === 'fixer' ? 'fixer' : undefined;
|
|
36
|
+
if (stance && descriptor.access !== 'write')
|
|
37
|
+
throw new EngineError('invalid_arguments', `The fixer stance requires a role with write access; ${roleId} is ${descriptor.access}`);
|
|
38
|
+
const fixerEngine = stance && roleId === 'developer' ? deps.config.fixer : undefined;
|
|
31
39
|
// Agents run sandboxed and cannot install what the repository needs (Codex
|
|
32
40
|
// cannot even write the Playwright browser cache): the host prepares it
|
|
33
41
|
// before any writer turn and says so in the task.
|
|
@@ -39,7 +47,8 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
39
47
|
const schema = params.structuredOutput;
|
|
40
48
|
const validate = schema ? ajv.compile(schema) : undefined;
|
|
41
49
|
const roleState = deps.roleState(context);
|
|
42
|
-
|
|
50
|
+
// Sessions never carry across engines: a turn on the fixer engine always starts fresh.
|
|
51
|
+
const previous = params.sessionContinuity !== 'none' && !fixerEngine ? roleState.read().sessions[roleId]?.sessionId : undefined;
|
|
43
52
|
const openspec = deps.openspec?.(context);
|
|
44
53
|
if (descriptor.openspecSkill && !openspec?.[roleId])
|
|
45
54
|
throw new EngineError('openspec_binding_required', 'Role requires a scoped OpenSpec binding: ' + roleId);
|
|
@@ -48,7 +57,7 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
48
57
|
const before = deps.executionSnapshot(context).candidate;
|
|
49
58
|
const memoryIdentity = { roleId, descriptor: json(descriptor), tier: roleState.read().routes[roleId]?.tier ?? 'base',
|
|
50
59
|
candidateHash: before?.hash ?? null, scopeId: context.frame.scope.id, nodePath: context.frame.nodePath,
|
|
51
|
-
repositories: deps.context.repositories.map(repository => ({ id: repository.id, path: repository.path })) };
|
|
60
|
+
repositories: deps.context.repositories.map(repository => ({ id: repository.id, path: repository.path })), ...(stance ? { stance } : {}) };
|
|
52
61
|
const memoryKey = contentDigest(memoryIdentity);
|
|
53
62
|
const saved = await advisoryMemory(context, async () => {
|
|
54
63
|
const memory = deps.memory(context);
|
|
@@ -59,8 +68,11 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
59
68
|
const task = text(params.prompt) + environment + (priorNote ? '\n\n## Prior project review note\nTreat this bounded note as prior evidence to check against the current task; frozen requirements remain authoritative.\n' + JSON.stringify(priorNote) : '');
|
|
60
69
|
const verification = descriptor.access === 'write' ? hostVerificationPlan(deps, context, params) : undefined;
|
|
61
70
|
const adopted = adoptedVerificationOutputs(context.state.$outputs);
|
|
62
|
-
const
|
|
71
|
+
const derivation = roleId === 'reviewer' ? committedDerivation(context.state.$outputs) : undefined;
|
|
72
|
+
const projectRules = roleId === 'architect' || roleId === 'reviewer' ? projectRulesContext(deps.context) : undefined;
|
|
73
|
+
const full = roleInstructions(descriptor, deps.context, openspec?.[roleId]?.change, { definition: stance && roleId === 'developer' ? deps.config.rolePrompts?.fixer : descriptor.prompt, ...(stance && roleId === 'developer' ? { stance } : {}), verification, ...(adopted.length ? { adoptedOutputs: adopted } : {}), ...(derivation ? { derivation } : {}), ...(projectRules ? { projectRules } : {}) }) + '\n## Current workflow task\n' + task;
|
|
63
74
|
const result = await invoke(roleId, deps.stepContext(context), { prompt: previous ? task : full, ...(previous ? { resumeSessionId: previous, fallbackPrompt: full } : {}),
|
|
75
|
+
...(stance ? { stance } : {}), ...(fixerEngine ? { agentOverride: fixerEngine } : {}),
|
|
64
76
|
structured: schema !== undefined, lenient: options.normalizeStructuredOutput !== undefined, outputSchema: schema, timeoutMs: params.timeoutMs, idleTimeoutMs: params.idleTimeoutMs }, (output, responseText) => {
|
|
65
77
|
const normalized = options.normalizeStructuredOutput ? options.normalizeStructuredOutput(output, responseText) : output;
|
|
66
78
|
if (validate && !validate(normalized))
|
|
@@ -96,7 +108,16 @@ export async function executeRoleTurn(deps, params, context, options = {}) {
|
|
|
96
108
|
text: boundedText(result.text, 4000), lastAttemptId: context.frame.attemptId });
|
|
97
109
|
});
|
|
98
110
|
return { outcome: 'next', output: { candidateHash: deps.executionSnapshot(context).candidate?.hash ?? null, text: boundedText(result.text), ...(result.value ? { structured: json(result.value) } : {}) },
|
|
99
|
-
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
111
|
+
history: [historyEntry(context, result.text)], ...(params.sessionContinuity !== 'none' && !fixerEngine && session ? { session: { sessionId: session.sessionId, identity: session.identity } } : {}) };
|
|
112
|
+
}
|
|
113
|
+
/** The coverage report a committed `derive-change` output carries (its derivation.json), for the reviewer. */
|
|
114
|
+
function committedDerivation(outputs) {
|
|
115
|
+
for (const output of Object.values(outputs)) {
|
|
116
|
+
const value = output && typeof output === 'object' && !Array.isArray(output) ? output.derivation : undefined;
|
|
117
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && Array.isArray(value.unexplainedFiles) && typeof value.files === 'number' && typeof value.explained === 'number')
|
|
118
|
+
return { ...value, unexplainedFiles: value.unexplainedFiles.filter((file) => typeof file === 'string') };
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
100
121
|
}
|
|
101
122
|
/** Files any committed `verify` output reports as host-adopted verification output (sticky for the run), so no role is told to revert them. */
|
|
102
123
|
function adoptedVerificationOutputs(outputs) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PipelineContext } from '../../pipeline/pipeline-state.js';
|
|
2
2
|
import { type ExecutorRegistry } from '../executors.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type RuntimeConfig } from '../executor-types.js';
|
|
4
4
|
import type { RoleCatalog, WorkflowDefinition } from './definition-types.js';
|
|
5
5
|
export interface DefinitionRunRequest {
|
|
6
6
|
context: PipelineContext;
|
|
@@ -14,6 +14,7 @@ export interface DefinitionRunInput {
|
|
|
14
14
|
change?: string;
|
|
15
15
|
registry?: ExecutorRegistry;
|
|
16
16
|
}
|
|
17
|
+
/** Configured roles plus Core's engine roles (deriver), which definitions may declare without configuring them. */
|
|
17
18
|
export declare function configuredRoles(config: RuntimeConfig): RoleCatalog;
|
|
18
19
|
/** Complete admission is read-only; no run, journal, provider turn or skill is created. */
|
|
19
20
|
export declare function preflightDefinition(input: DefinitionRunInput): Promise<{
|
|
@@ -2,12 +2,14 @@ import { validatePipelineContext, validateVerificationRequest } from '../../pipe
|
|
|
2
2
|
import { normalizeRuntimeConfig, resolveRoleDescriptor, roleIds } from '../config.js';
|
|
3
3
|
import { assertEffortSupported } from '../capabilities.js';
|
|
4
4
|
import { createExecutorRegistry } from '../executors.js';
|
|
5
|
+
import { ENGINE_ROLES } from '../executor-types.js';
|
|
5
6
|
import { EngineError } from './contracts.js';
|
|
6
7
|
import { intersectBudgets } from './budget.js';
|
|
7
8
|
import { validateWorkflowDefinition } from './definition-validator.js';
|
|
8
9
|
import { validationPieceRegistry } from './pieces/index.js';
|
|
10
|
+
/** Configured roles plus Core's engine roles (deriver), which definitions may declare without configuring them. */
|
|
9
11
|
export function configuredRoles(config) {
|
|
10
|
-
return Object.fromEntries(roleIds(config).map(id => [id, { access: resolveRoleDescriptor(config, id).access }]));
|
|
12
|
+
return Object.fromEntries([...roleIds(config), ...ENGINE_ROLES].map(id => [id, { access: resolveRoleDescriptor(config, id).access }]));
|
|
11
13
|
}
|
|
12
14
|
/** Complete admission is read-only; no run, journal, provider turn or skill is created. */
|
|
13
15
|
export async function preflightDefinition(input) {
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
export declare const PATTERN_TIMEOUT_MS = 50;
|
|
2
|
+
/**
|
|
3
|
+
* Attempts per match. The VM timeout is wall-clock: a garbage-collection pause or
|
|
4
|
+
* a descheduled thread on a loaded machine can exceed it while a simple pattern
|
|
5
|
+
* runs. A catastrophic pattern times out on every attempt, so one retry keeps the
|
|
6
|
+
* bound (at most 2 × 50 ms) without failing a cheap match on scheduler noise.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PATTERN_ATTEMPTS = 2;
|
|
9
|
+
type Run = (pattern: string, text: string) => RegExpExecArray | null;
|
|
10
|
+
/** Regex is data in a constant script. V8 interrupts even catastrophic matching. `run` is a test seam. */
|
|
11
|
+
export declare function capturePattern(pattern: string, text: string, run?: Run): Array<string | undefined> | null;
|
|
3
12
|
export declare function matchesPattern(pattern: string, text: string): boolean;
|
|
13
|
+
export {};
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { Script } from 'node:vm';
|
|
2
2
|
import { EngineError } from './contracts.js';
|
|
3
3
|
const matchScript = new Script('new RegExp(pattern).exec(text)', { filename: 'core-bounded-pattern' });
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
export const PATTERN_TIMEOUT_MS = 50;
|
|
5
|
+
/**
|
|
6
|
+
* Attempts per match. The VM timeout is wall-clock: a garbage-collection pause or
|
|
7
|
+
* a descheduled thread on a loaded machine can exceed it while a simple pattern
|
|
8
|
+
* runs. A catastrophic pattern times out on every attempt, so one retry keeps the
|
|
9
|
+
* bound (at most 2 × 50 ms) without failing a cheap match on scheduler noise.
|
|
10
|
+
*/
|
|
11
|
+
export const PATTERN_ATTEMPTS = 2;
|
|
12
|
+
const runInVm = (pattern, text) => matchScript.runInNewContext({ pattern, text }, {
|
|
13
|
+
timeout: PATTERN_TIMEOUT_MS, contextCodeGeneration: { strings: false, wasm: false },
|
|
14
|
+
});
|
|
15
|
+
const timedOut = (error) => error !== null && typeof error === 'object' && 'code' in error && error.code === 'ERR_SCRIPT_EXECUTION_TIMEOUT';
|
|
16
|
+
/** Regex is data in a constant script. V8 interrupts even catastrophic matching. `run` is a test seam. */
|
|
17
|
+
export function capturePattern(pattern, text, run = runInVm) {
|
|
6
18
|
if (pattern.length > 200)
|
|
7
19
|
throw new EngineError('invalid_expression', 'Pattern exceeds 200 characters');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const input = text.slice(0, 32_000);
|
|
21
|
+
for (let attempt = 1;; attempt++) {
|
|
22
|
+
try {
|
|
23
|
+
const match = run(pattern, input);
|
|
24
|
+
return match ? Array.from(match) : null;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (timedOut(error) && attempt < PATTERN_ATTEMPTS)
|
|
28
|
+
continue;
|
|
29
|
+
throw new EngineError('invalid_expression', timedOut(error) ? `Regular expression exceeded ${PATTERN_TIMEOUT_MS} ms` : 'Invalid regular expression');
|
|
30
|
+
}
|
|
17
31
|
}
|
|
18
32
|
}
|
|
19
33
|
export function matchesPattern(pattern, text) { return capturePattern(pattern, text) !== null; }
|
|
@@ -46,6 +46,11 @@ export declare function projectRunStatus(ledger: RunLedger, compact?: boolean):
|
|
|
46
46
|
error?: string;
|
|
47
47
|
nextNodePath: string;
|
|
48
48
|
nextStep: string;
|
|
49
|
+
pause?: {
|
|
50
|
+
requestId: string;
|
|
51
|
+
reason: string;
|
|
52
|
+
at: string;
|
|
53
|
+
} | undefined;
|
|
49
54
|
steps: {
|
|
50
55
|
[k: string]: {
|
|
51
56
|
kind: string;
|
|
@@ -142,6 +147,11 @@ export declare function projectRunStatus(ledger: RunLedger, compact?: boolean):
|
|
|
142
147
|
} | undefined;
|
|
143
148
|
nextNodePath: string;
|
|
144
149
|
nextStep: string;
|
|
150
|
+
pause?: {
|
|
151
|
+
requestId: string;
|
|
152
|
+
reason: string;
|
|
153
|
+
at: string;
|
|
154
|
+
} | undefined;
|
|
145
155
|
steps: {
|
|
146
156
|
[k: string]: {
|
|
147
157
|
kind: string;
|
|
@@ -251,6 +261,13 @@ export declare function projectRunStatus(ledger: RunLedger, compact?: boolean):
|
|
|
251
261
|
fullContexts: number;
|
|
252
262
|
incrementalContexts: number;
|
|
253
263
|
};
|
|
264
|
+
baseline: {
|
|
265
|
+
invocations: number;
|
|
266
|
+
roles: string[];
|
|
267
|
+
costUsd: number | null;
|
|
268
|
+
inputTokens: number | null;
|
|
269
|
+
outputTokens: number | null;
|
|
270
|
+
};
|
|
254
271
|
escalations: {
|
|
255
272
|
role: string;
|
|
256
273
|
attemptId: string;
|
|
@@ -326,6 +343,11 @@ export declare function statusRun(directory: string, compact?: boolean): Promise
|
|
|
326
343
|
error?: string;
|
|
327
344
|
nextNodePath: string;
|
|
328
345
|
nextStep: string;
|
|
346
|
+
pause?: {
|
|
347
|
+
requestId: string;
|
|
348
|
+
reason: string;
|
|
349
|
+
at: string;
|
|
350
|
+
} | undefined;
|
|
329
351
|
steps: {
|
|
330
352
|
[k: string]: {
|
|
331
353
|
kind: string;
|
|
@@ -422,6 +444,11 @@ export declare function statusRun(directory: string, compact?: boolean): Promise
|
|
|
422
444
|
} | undefined;
|
|
423
445
|
nextNodePath: string;
|
|
424
446
|
nextStep: string;
|
|
447
|
+
pause?: {
|
|
448
|
+
requestId: string;
|
|
449
|
+
reason: string;
|
|
450
|
+
at: string;
|
|
451
|
+
} | undefined;
|
|
425
452
|
steps: {
|
|
426
453
|
[k: string]: {
|
|
427
454
|
kind: string;
|
|
@@ -531,6 +558,13 @@ export declare function statusRun(directory: string, compact?: boolean): Promise
|
|
|
531
558
|
fullContexts: number;
|
|
532
559
|
incrementalContexts: number;
|
|
533
560
|
};
|
|
561
|
+
baseline: {
|
|
562
|
+
invocations: number;
|
|
563
|
+
roles: string[];
|
|
564
|
+
costUsd: number | null;
|
|
565
|
+
inputTokens: number | null;
|
|
566
|
+
outputTokens: number | null;
|
|
567
|
+
};
|
|
534
568
|
escalations: {
|
|
535
569
|
role: string;
|
|
536
570
|
attemptId: string;
|
|
@@ -23,10 +23,16 @@ export function projectRunStatus(ledger, compact = true) {
|
|
|
23
23
|
const failures = ledger.db.sqlite.prepare("SELECT node_path,scope_id,branch,status,attempt,visit,ended_at,error_code,error_message FROM attempts WHERE run_id=? AND status IN ('failed','interrupted','blocked') ORDER BY started_at DESC LIMIT 8").all(ledger.runId);
|
|
24
24
|
const lease = ledger.lease.current();
|
|
25
25
|
const recoverable = ledger.db.sqlite.prepare("SELECT a.node_path,a.scope_id,a.attempt_id,a.status,v.effect FROM attempts a JOIN visits v ON a.visit_id=v.visit_id JOIN steps s ON s.last_attempt_id=a.attempt_id WHERE a.run_id=? AND a.terminal_digest IS NULL AND a.status IN ('running','interrupted') AND v.effect='write'").all(ledger.runId);
|
|
26
|
+
// A requested pause (runtime pause) records its reason on the workflow_paused event.
|
|
27
|
+
const pauseEvent = row.status === 'paused' && !pending.length
|
|
28
|
+
? ledger.db.sqlite.prepare("SELECT at,payload_json FROM events WHERE run_id=? AND type='workflow_paused' ORDER BY sequence DESC LIMIT 1").get(ledger.runId) : undefined;
|
|
29
|
+
const pausePayload = pauseEvent ? JSON.parse(String(pauseEvent.payload_json)).payload : undefined;
|
|
30
|
+
const pause = pausePayload && typeof pausePayload.reason === 'string' && typeof pausePayload.requestId === 'string'
|
|
31
|
+
? { requestId: pausePayload.requestId, reason: pausePayload.reason, at: String(pauseEvent.at) } : undefined;
|
|
26
32
|
const next = pending[0]?.nodePath ?? (row.status === 'succeeded' || row.status === 'cancelled' ? null : base.nextStep);
|
|
27
33
|
const state = { ...(compact ? { runId: base.runId, traceId: base.traceId, status: base.status, updatedAt: base.updatedAt, usage: base.usage,
|
|
28
34
|
...(base.pendingApproval ? { pendingApproval: base.pendingApproval } : {}), ...(base.pendingQuestion ? { pendingQuestion: base.pendingQuestion } : {}) } : base),
|
|
29
|
-
nextNodePath: next, nextStep: next,
|
|
35
|
+
nextNodePath: next, nextStep: next, ...(pause ? { pause } : {}),
|
|
30
36
|
steps: Object.fromEntries(steps.map(step => [String(step.node_path), { kind: String(step.kind), status: String(step.status), visits: Number(step.visits) }])),
|
|
31
37
|
scopes: steps.map(step => {
|
|
32
38
|
// Read only the current committed attempt. A previous successful visit is
|
|
@@ -80,6 +80,11 @@ export declare function createRun(input: DefinitionRunInput & RunObservers): Pro
|
|
|
80
80
|
error?: string;
|
|
81
81
|
nextNodePath: string;
|
|
82
82
|
nextStep: string;
|
|
83
|
+
pause?: {
|
|
84
|
+
requestId: string;
|
|
85
|
+
reason: string;
|
|
86
|
+
at: string;
|
|
87
|
+
} | undefined;
|
|
83
88
|
steps: {
|
|
84
89
|
[k: string]: {
|
|
85
90
|
kind: string;
|
|
@@ -176,6 +181,11 @@ export declare function createRun(input: DefinitionRunInput & RunObservers): Pro
|
|
|
176
181
|
} | undefined;
|
|
177
182
|
nextNodePath: string;
|
|
178
183
|
nextStep: string;
|
|
184
|
+
pause?: {
|
|
185
|
+
requestId: string;
|
|
186
|
+
reason: string;
|
|
187
|
+
at: string;
|
|
188
|
+
} | undefined;
|
|
179
189
|
steps: {
|
|
180
190
|
[k: string]: {
|
|
181
191
|
kind: string;
|
|
@@ -285,6 +295,13 @@ export declare function createRun(input: DefinitionRunInput & RunObservers): Pro
|
|
|
285
295
|
fullContexts: number;
|
|
286
296
|
incrementalContexts: number;
|
|
287
297
|
};
|
|
298
|
+
baseline: {
|
|
299
|
+
invocations: number;
|
|
300
|
+
roles: string[];
|
|
301
|
+
costUsd: number | null;
|
|
302
|
+
inputTokens: number | null;
|
|
303
|
+
outputTokens: number | null;
|
|
304
|
+
};
|
|
288
305
|
escalations: {
|
|
289
306
|
role: string;
|
|
290
307
|
attemptId: string;
|
|
@@ -363,6 +380,11 @@ export declare function resumeRun(directory: string, options?: ResumeDefinitionO
|
|
|
363
380
|
error?: string;
|
|
364
381
|
nextNodePath: string;
|
|
365
382
|
nextStep: string;
|
|
383
|
+
pause?: {
|
|
384
|
+
requestId: string;
|
|
385
|
+
reason: string;
|
|
386
|
+
at: string;
|
|
387
|
+
} | undefined;
|
|
366
388
|
steps: {
|
|
367
389
|
[k: string]: {
|
|
368
390
|
kind: string;
|
|
@@ -459,6 +481,11 @@ export declare function resumeRun(directory: string, options?: ResumeDefinitionO
|
|
|
459
481
|
} | undefined;
|
|
460
482
|
nextNodePath: string;
|
|
461
483
|
nextStep: string;
|
|
484
|
+
pause?: {
|
|
485
|
+
requestId: string;
|
|
486
|
+
reason: string;
|
|
487
|
+
at: string;
|
|
488
|
+
} | undefined;
|
|
462
489
|
steps: {
|
|
463
490
|
[k: string]: {
|
|
464
491
|
kind: string;
|
|
@@ -568,6 +595,13 @@ export declare function resumeRun(directory: string, options?: ResumeDefinitionO
|
|
|
568
595
|
fullContexts: number;
|
|
569
596
|
incrementalContexts: number;
|
|
570
597
|
};
|
|
598
|
+
baseline: {
|
|
599
|
+
invocations: number;
|
|
600
|
+
roles: string[];
|
|
601
|
+
costUsd: number | null;
|
|
602
|
+
inputTokens: number | null;
|
|
603
|
+
outputTokens: number | null;
|
|
604
|
+
};
|
|
571
605
|
escalations: {
|
|
572
606
|
role: string;
|
|
573
607
|
attemptId: string;
|
|
@@ -603,3 +637,25 @@ export declare function cancelRun(directory: string, requestId?: string): Promis
|
|
|
603
637
|
requestId: string;
|
|
604
638
|
createdAt: string;
|
|
605
639
|
}>;
|
|
640
|
+
export declare const PAUSE_OUTCOMES: readonly ['requested', 'paused', 'already-paused', 'not-running', 'terminal'];
|
|
641
|
+
export type PauseOutcome = typeof PAUSE_OUTCOMES[number];
|
|
642
|
+
export interface PauseResult {
|
|
643
|
+
runId: string;
|
|
644
|
+
requestId: string;
|
|
645
|
+
reason: string;
|
|
646
|
+
outcome: PauseOutcome;
|
|
647
|
+
status: string;
|
|
648
|
+
createdAt?: string;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Cooperative pause. A run executing under a live lease receives an idempotent
|
|
652
|
+
* inbox request: its in-flight attempts finish and commit, the next admission is
|
|
653
|
+
* refused, and the owner settles `paused` with the reason and releases its lease.
|
|
654
|
+
* A run whose owner is gone while still marked running is settled `paused`
|
|
655
|
+
* directly. Terminal, already paused or otherwise stopped runs are left
|
|
656
|
+
* unchanged and reported as such. `runtime resume` continues every paused run.
|
|
657
|
+
*/
|
|
658
|
+
export declare function pauseRun(directory: string, options?: {
|
|
659
|
+
requestId?: string;
|
|
660
|
+
reason?: string;
|
|
661
|
+
}): Promise<PauseResult>;
|
|
@@ -13,7 +13,7 @@ import { EngineEventStream } from './events.js';
|
|
|
13
13
|
import { initialDefinitionState } from './state.js';
|
|
14
14
|
import { preflightDefinition } from './preflight.js';
|
|
15
15
|
import { observeLedger, projectRunStatus } from './run-status.js';
|
|
16
|
-
import { ControlInbox } from './steering/inbox.js';
|
|
16
|
+
import { ControlInbox, PAUSE_REASON } from './steering/inbox.js';
|
|
17
17
|
import { ledgerBudget } from './invocation-context.js';
|
|
18
18
|
import { composeDefinitionRuntime } from './composition.js';
|
|
19
19
|
import { createEngineTelemetry } from './otel.js';
|
|
@@ -145,6 +145,8 @@ async function execute(database, admitted, options, resume) {
|
|
|
145
145
|
if (resume)
|
|
146
146
|
database.transaction('workflow-resumed', () => {
|
|
147
147
|
lease.assert(token);
|
|
148
|
+
// An explicit resume supersedes pause requests that arrived before it.
|
|
149
|
+
inbox.consumePauses();
|
|
148
150
|
appendRunEvent(database, runId, 'workflow_resumed', {});
|
|
149
151
|
});
|
|
150
152
|
stream.flush();
|
|
@@ -168,21 +170,30 @@ async function execute(database, admitted, options, resume) {
|
|
|
168
170
|
if (controller.signal.reason instanceof EngineError && controller.signal.reason.code === 'lease_lost')
|
|
169
171
|
throw controller.signal.reason;
|
|
170
172
|
const reason = controller.signal.aborted ? controller.signal.reason : error;
|
|
171
|
-
const
|
|
173
|
+
const pause = controller.signal.aborted ? undefined : pauseOf(error);
|
|
174
|
+
const code = pause ? 'paused' : reason instanceof EngineError || reason && typeof reason === 'object' && 'code' in reason ? String(reason.code) : 'execution_failed';
|
|
172
175
|
const message = reason instanceof Error ? reason.message : String(reason);
|
|
176
|
+
let settled;
|
|
173
177
|
database.transaction('execution-stopped', () => {
|
|
174
178
|
lease.assert(token);
|
|
175
179
|
const row = ledger.run();
|
|
176
180
|
if (row.completion_json)
|
|
177
181
|
return;
|
|
178
|
-
const status = code === 'aborted' ? 'cancelled' : row.status === 'interrupted' || code === 'recover_required' ? 'interrupted' : 'failed';
|
|
182
|
+
const status = code === 'aborted' ? 'cancelled' : pause ? 'paused' : row.status === 'interrupted' || code === 'recover_required' ? 'interrupted' : 'failed';
|
|
183
|
+
settled = status;
|
|
179
184
|
database.put('runs', { ...row, status, active_duration_ms: ledger.activeDurationMs(), active_started_at: null, updated_at: new Date().toISOString() });
|
|
180
|
-
|
|
185
|
+
if (pause) {
|
|
186
|
+
inbox.consumePauses();
|
|
187
|
+
appendRunEvent(database, runId, 'workflow_paused', { requestId: pause.requestId, reason: pause.reason });
|
|
188
|
+
}
|
|
189
|
+
else
|
|
190
|
+
appendRunEvent(database, runId, status === 'cancelled' ? 'workflow_cancelled' : 'workflow_failed', { error: { code, message } });
|
|
181
191
|
});
|
|
182
192
|
if (inbox.cancellationRequested())
|
|
183
193
|
inbox.acknowledgeCancellation(token);
|
|
184
194
|
const status = projectRunStatus(ledger);
|
|
185
|
-
|
|
195
|
+
// A requested pause is a resumable stop, not a failure.
|
|
196
|
+
return settled === 'paused' ? status : { ...status, error: { code, message } };
|
|
186
197
|
}
|
|
187
198
|
finally {
|
|
188
199
|
clearInterval(controlTimer);
|
|
@@ -194,6 +205,18 @@ async function execute(database, admitted, options, resume) {
|
|
|
194
205
|
await telemetry?.close();
|
|
195
206
|
}
|
|
196
207
|
}
|
|
208
|
+
/** The pause request carried by an admission refusal, also when several branches refused together. */
|
|
209
|
+
function pauseOf(error) {
|
|
210
|
+
const errors = error instanceof AggregateError ? error.errors : [error];
|
|
211
|
+
for (const value of errors) {
|
|
212
|
+
if (value instanceof EngineError && value.code === 'paused' && value.details && typeof value.details === 'object' && !Array.isArray(value.details)) {
|
|
213
|
+
const details = value.details;
|
|
214
|
+
if (typeof details.requestId === 'string' && typeof details.reason === 'string')
|
|
215
|
+
return { requestId: details.requestId, reason: details.reason };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
197
220
|
function validateAnswers(ledger, answers) {
|
|
198
221
|
const known = new Map(ledger.interrupts().map(value => [value.id, value]));
|
|
199
222
|
for (const [id, value] of Object.entries(answers)) {
|
|
@@ -245,3 +268,63 @@ export async function cancelRun(directory, requestId) {
|
|
|
245
268
|
database.close();
|
|
246
269
|
}
|
|
247
270
|
}
|
|
271
|
+
export const PAUSE_OUTCOMES = ['requested', 'paused', 'already-paused', 'not-running', 'terminal'];
|
|
272
|
+
/**
|
|
273
|
+
* Cooperative pause. A run executing under a live lease receives an idempotent
|
|
274
|
+
* inbox request: its in-flight attempts finish and commit, the next admission is
|
|
275
|
+
* refused, and the owner settles `paused` with the reason and releases its lease.
|
|
276
|
+
* A run whose owner is gone while still marked running is settled `paused`
|
|
277
|
+
* directly. Terminal, already paused or otherwise stopped runs are left
|
|
278
|
+
* unchanged and reported as such. `runtime resume` continues every paused run.
|
|
279
|
+
*/
|
|
280
|
+
export async function pauseRun(directory, options = {}) {
|
|
281
|
+
const reason = options.reason ?? 'requested', requestId = options.requestId ?? randomUUID();
|
|
282
|
+
if (!PAUSE_REASON.test(reason))
|
|
283
|
+
throw new EngineError('invalid_arguments', 'Pause reason must be a lowercase token of 1–64 characters (for example requested or budget)');
|
|
284
|
+
if (!requestId || requestId.length > 256)
|
|
285
|
+
throw new EngineError('invalid_arguments', 'Control request ID must contain 1–256 characters');
|
|
286
|
+
const database = await RunDatabase.open(path.join(directory, 'run.sqlite'));
|
|
287
|
+
try {
|
|
288
|
+
const view = observeLedger(database), runId = view.runId;
|
|
289
|
+
const result = (outcome, createdAt) => ({ runId, requestId, reason, outcome, status: String(view.run().status), ...(createdAt ? { createdAt } : {}) });
|
|
290
|
+
const terminal = () => { const row = view.run(); return !!row.completion_json || row.status === 'succeeded' || row.status === 'cancelled'; };
|
|
291
|
+
if (terminal())
|
|
292
|
+
return result('terminal');
|
|
293
|
+
// Only this exact package can resume the run, so only it may pause it.
|
|
294
|
+
if (!sameRuntimeIdentity(JSON.parse(String(view.run().runtime_identity_json)), coreRuntimeIdentity()))
|
|
295
|
+
throw new EngineError('resume_incompatible', 'Pause requires the retained original Core package');
|
|
296
|
+
const lease = new RunLease(database, runId);
|
|
297
|
+
if ((lease.current()?.expiresAt ?? 0) > Date.now()) {
|
|
298
|
+
database.upgradeSchema();
|
|
299
|
+
const accepted = new ControlInbox(database, runId).pause(reason, requestId);
|
|
300
|
+
return result('requested', accepted.createdAt);
|
|
301
|
+
}
|
|
302
|
+
if (view.run().status === 'paused')
|
|
303
|
+
return result('already-paused');
|
|
304
|
+
if (view.run().status !== 'running')
|
|
305
|
+
return result('not-running');
|
|
306
|
+
const token = lease.acquire(randomUUID());
|
|
307
|
+
let outcome = 'paused';
|
|
308
|
+
try {
|
|
309
|
+
const ledger = new RunLedger(database, token, { maxTransitions: 1 });
|
|
310
|
+
database.transaction('workflow-paused', () => {
|
|
311
|
+
lease.assert(token);
|
|
312
|
+
const run = ledger.run();
|
|
313
|
+
// The owner may have settled between the observation and the lease.
|
|
314
|
+
if (run.completion_json || run.status !== 'running') {
|
|
315
|
+
outcome = run.status === 'paused' ? 'already-paused' : run.completion_json || run.status === 'succeeded' || run.status === 'cancelled' ? 'terminal' : 'not-running';
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
database.put('runs', { ...run, status: 'paused', active_duration_ms: ledger.activeDurationMs(), active_started_at: null, updated_at: new Date().toISOString() });
|
|
319
|
+
appendRunEvent(database, runId, 'workflow_paused', { requestId, reason });
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
lease.release(token);
|
|
324
|
+
}
|
|
325
|
+
return result(outcome);
|
|
326
|
+
}
|
|
327
|
+
finally {
|
|
328
|
+
database.close();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -4,6 +4,7 @@ export declare const MAX_STEERING_CHARACTERS = 20000;
|
|
|
4
4
|
export declare const MAX_STEERING_BYTES = 80000;
|
|
5
5
|
export declare const MAX_PENDING_STEERING = 128;
|
|
6
6
|
export declare const MAX_PENDING_STEERING_BYTES: number;
|
|
7
|
+
export declare const PAUSE_REASON: RegExp;
|
|
7
8
|
export interface SteeringMessage {
|
|
8
9
|
id: string;
|
|
9
10
|
text: string;
|
|
@@ -34,6 +35,12 @@ export declare class ControlInbox {
|
|
|
34
35
|
requestId: string;
|
|
35
36
|
createdAt: string;
|
|
36
37
|
};
|
|
38
|
+
/** Cooperative pause: the owner stops before admitting its next attempt and settles `paused`. */
|
|
39
|
+
pause(reason: string, requestId?: string): {
|
|
40
|
+
requestId: string;
|
|
41
|
+
createdAt: string;
|
|
42
|
+
reason: string;
|
|
43
|
+
};
|
|
37
44
|
private appendControl;
|
|
38
45
|
pending(): SteeringMessage[];
|
|
39
46
|
/** Bounded read-only projection. Counts cover the whole inbox, previews never expose full prompts. */
|
|
@@ -48,6 +55,14 @@ export declare class ControlInbox {
|
|
|
48
55
|
/** Called inside attempt admission, after the attempt row exists and the lease was fenced. */
|
|
49
56
|
claimForAttempt(frame: AttemptFrame): SteeringMessage[];
|
|
50
57
|
cancellationRequested(): boolean;
|
|
58
|
+
/** The oldest unconsumed pause request, if any. */
|
|
59
|
+
pauseRequested(): {
|
|
60
|
+
requestId: string;
|
|
61
|
+
reason: string;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
} | undefined;
|
|
64
|
+
/** Marks every pending pause request consumed. Callers hold the lease inside their own transaction. */
|
|
65
|
+
consumePauses(): void;
|
|
51
66
|
acknowledgeCancellation(token: ExecutionLease): void;
|
|
52
67
|
}
|
|
53
68
|
/** Operator text is delimited and cannot replace frozen acceptance obligations/instructions. */
|
|
@@ -7,6 +7,7 @@ export const MAX_STEERING_CHARACTERS = 20_000;
|
|
|
7
7
|
export const MAX_STEERING_BYTES = 80_000;
|
|
8
8
|
export const MAX_PENDING_STEERING = 128;
|
|
9
9
|
export const MAX_PENDING_STEERING_BYTES = 512 * 1024;
|
|
10
|
+
export const PAUSE_REASON = /^[a-z][a-z0-9-]{0,63}$/;
|
|
10
11
|
const project = (row) => {
|
|
11
12
|
const value = JSON.parse(String(row.payload_json));
|
|
12
13
|
return { id: String(row.request_id), ...value, createdAt: String(row.created_at),
|
|
@@ -33,6 +34,13 @@ export class ControlInbox {
|
|
|
33
34
|
const row = this.appendControl('cancel', {}, requestId);
|
|
34
35
|
return { requestId: String(row.request_id), createdAt: String(row.created_at) };
|
|
35
36
|
}
|
|
37
|
+
/** Cooperative pause: the owner stops before admitting its next attempt and settles `paused`. */
|
|
38
|
+
pause(reason, requestId = randomUUID()) {
|
|
39
|
+
if (!PAUSE_REASON.test(reason))
|
|
40
|
+
throw new EngineError('invalid_arguments', 'Pause reason must be a lowercase token of 1–64 characters (for example requested or budget)');
|
|
41
|
+
const row = this.appendControl('pause', { reason }, requestId);
|
|
42
|
+
return { requestId: String(row.request_id), createdAt: String(row.created_at), reason: JSON.parse(String(row.payload_json)).reason };
|
|
43
|
+
}
|
|
36
44
|
appendControl(kind, payload, requestId) {
|
|
37
45
|
if (!requestId || requestId.length > 256)
|
|
38
46
|
throw new EngineError('invalid_arguments', 'Control request ID must contain 1–256 characters');
|
|
@@ -56,7 +64,7 @@ export class ControlInbox {
|
|
|
56
64
|
}
|
|
57
65
|
const at = new Date().toISOString();
|
|
58
66
|
this.database.put('control_inbox', { request_id: requestId, run_id: this.runId, kind, payload_json: encoded, created_at: at, received_sequence: run.next_event_sequence });
|
|
59
|
-
appendRunEvent(this.database, this.runId, kind === 'steer' ? 'steering_received' : 'cancellation_requested', { requestId }, {}, at);
|
|
67
|
+
appendRunEvent(this.database, this.runId, kind === 'steer' ? 'steering_received' : kind === 'pause' ? 'pause_requested' : 'cancellation_requested', kind === 'pause' ? { requestId, reason: payload.reason } : { requestId }, {}, at);
|
|
60
68
|
return this.database.get('control_inbox', { request_id: requestId });
|
|
61
69
|
});
|
|
62
70
|
}
|
|
@@ -95,6 +103,18 @@ export class ControlInbox {
|
|
|
95
103
|
cancellationRequested() {
|
|
96
104
|
return !!this.database.sqlite.prepare("SELECT 1 FROM control_inbox WHERE run_id=? AND kind='cancel' AND consumed_at IS NULL").get(this.runId);
|
|
97
105
|
}
|
|
106
|
+
/** The oldest unconsumed pause request, if any. */
|
|
107
|
+
pauseRequested() {
|
|
108
|
+
const row = this.database.sqlite.prepare("SELECT * FROM control_inbox WHERE run_id=? AND kind='pause' AND consumed_at IS NULL ORDER BY received_sequence,request_id LIMIT 1").get(this.runId);
|
|
109
|
+
return row ? { requestId: String(row.request_id), reason: JSON.parse(String(row.payload_json)).reason, createdAt: String(row.created_at) } : undefined;
|
|
110
|
+
}
|
|
111
|
+
/** Marks every pending pause request consumed. Callers hold the lease inside their own transaction. */
|
|
112
|
+
consumePauses() {
|
|
113
|
+
const at = new Date().toISOString();
|
|
114
|
+
for (const row of this.database.sqlite.prepare("SELECT * FROM control_inbox WHERE run_id=? AND kind='pause' AND consumed_at IS NULL").all(this.runId)) {
|
|
115
|
+
this.database.put('control_inbox', { ...row, consumed_at: at });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
98
118
|
acknowledgeCancellation(token) {
|
|
99
119
|
this.database.transaction('cancellation-consumed', () => {
|
|
100
120
|
new RunLease(this.database, this.runId).assert(token);
|
|
@@ -19,12 +19,18 @@ if($actual.GetOwner([System.Security.Principal.SecurityIdentifier]).Value -ne $i
|
|
|
19
19
|
foreach($entry in $actual.GetAccessRules($true,$true,[System.Security.Principal.SecurityIdentifier])) {
|
|
20
20
|
if($entry.IdentityReference.Value -notin @($identity.User.Value,'S-1-5-18') -or $entry.AccessControlType -ne 'Allow'){throw 'Private directory grants broad access'}
|
|
21
21
|
}`;
|
|
22
|
+
// A cold `powershell.exe` that loads the .NET AccessControl types has taken
|
|
23
|
+
// more than 30 s on a loaded windows-latest runner (run 39675761584: the first
|
|
24
|
+
// RunDatabase.open of a file failed with the total execution limit). The bound
|
|
25
|
+
// only protects against a hung PowerShell; a slow one must not fail closed
|
|
26
|
+
// storage creation, so it gets the same headroom as other Windows cold starts.
|
|
27
|
+
const WINDOWS_ACL_TIMEOUT_MS = 120_000;
|
|
22
28
|
async function setPrivateWindowsAcl(target, cwd) {
|
|
23
29
|
const env = { ...process.env, SPECRAILS_PRIVATE_PATH: target };
|
|
24
30
|
for (const key of Object.keys(env))
|
|
25
31
|
if (key.toLowerCase() === 'psmodulepath')
|
|
26
32
|
delete env[key];
|
|
27
|
-
const result = await runCliProcess({ command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(aclScript, 'utf16le').toString('base64')] }, { cwd, timeoutMs:
|
|
33
|
+
const result = await runCliProcess({ command: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-EncodedCommand', Buffer.from(aclScript, 'utf16le').toString('base64')] }, { cwd, timeoutMs: WINDOWS_ACL_TIMEOUT_MS, env });
|
|
28
34
|
if (result.exitCode !== 0)
|
|
29
35
|
throw new PersistenceError('private_storage_unavailable', `Cannot protect engine storage: ${result.stderr.slice(0, 2000)}`);
|
|
30
36
|
}
|
|
@@ -53,7 +53,7 @@ function fixtureRegistry(test, context, onPrompt) {
|
|
|
53
53
|
await tools.execute({ action: 'new' });
|
|
54
54
|
const artifacts = [
|
|
55
55
|
['proposal', 'proposal.md', `## Why\n${test.description}\n## What Changes\n- Implement the requested behavior.\n## Capabilities\n### New Capabilities\n- \`fixture-behavior\`: ${test.description}\n### Modified Capabilities\nNone.\n## Impact\nSelected repositories.\n`],
|
|
56
|
-
['design', 'design.md', `## Context\n${test.description}\n## Decisions\nUse the existing implementation.cjs pattern and keep the public function boundary.`],
|
|
56
|
+
['design', 'design.md', `## Context\n${test.description}\n## Decisions\nUse the existing implementation.cjs pattern and keep the public function boundary.\n## Non-Goals\n- No change outside implementation.cjs.`],
|
|
57
57
|
['specs', 'specs/fixture-behavior/spec.md', `## ADDED Requirements\n### Requirement: Requested behavior\nThe implementation SHALL satisfy the frozen behavior.\n#### Scenario: Supported input\n- **WHEN** a supported input is supplied\n- **THEN** the expected value is returned without losing required ordering or boundary behavior\n`],
|
|
58
58
|
['tasks', 'tasks.md', '## 1. Implementation\n- [ ] 1.1 Implement and test the requested behavior\n'],
|
|
59
59
|
];
|