@tacuchi/agent-workflow-cli 21.11.0 → 21.12.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/application/flow/advance.js +132 -18
- package/dist/application/flow/advance.js.map +1 -1
- package/dist/application/flow/flow-service.js +100 -2
- package/dist/application/flow/flow-service.js.map +1 -1
- package/dist/application/flow/internal-drive.js +16 -13
- package/dist/application/flow/internal-drive.js.map +1 -1
- package/dist/application/flow/run-state-service.js +236 -5
- package/dist/application/flow/run-state-service.js.map +1 -1
- package/dist/application/flow/submit.js +90 -35
- package/dist/application/flow/submit.js.map +1 -1
- package/dist/application/paths-service.js +17 -0
- package/dist/application/paths-service.js.map +1 -1
- package/dist/application/session-narrative.js +7 -3
- package/dist/application/session-narrative.js.map +1 -1
- package/dist/application/session-resolver.js +12 -2
- package/dist/application/session-resolver.js.map +1 -1
- package/dist/application/session-resume-service.js +7 -0
- package/dist/application/session-resume-service.js.map +1 -1
- package/dist/cli/commands/flow.js +52 -5
- package/dist/cli/commands/flow.js.map +1 -1
- package/dist/domain/capability/effects.js +13 -0
- package/dist/domain/capability/effects.js.map +1 -1
- package/dist/domain/flow/answer.js +105 -0
- package/dist/domain/flow/answer.js.map +1 -1
- package/dist/domain/flow/run-state.js +215 -11
- package/dist/domain/flow/run-state.js.map +1 -1
- package/package.json +1 -1
- package/skills/w/loops/CHASSIS.md +5 -7
|
@@ -12,15 +12,45 @@
|
|
|
12
12
|
* before the single write happens, so a failure halfway through leaves the
|
|
13
13
|
* previous state exactly as it was rather than a half-applied one.
|
|
14
14
|
*/
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import { FLOW_RUN_STATE_FILE, parseRunState, serializeRunState, } from "../../domain/flow/run-state.js";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { FLOW_RUN_STATE_FILE, atCurrentVersion, parseRunState, serializeRunState, withAttemptCounters, } from "../../domain/flow/run-state.js";
|
|
17
17
|
import { LockBusyError, acquireLock } from "../lock-service.js";
|
|
18
|
+
import { semanticDigest } from "../semantic-operation/protocol.js";
|
|
19
|
+
/**
|
|
20
|
+
* The attempt counter: outside the run state's seal, and outside its FOLDER.
|
|
21
|
+
*
|
|
22
|
+
* Two evasions, and the file's location answers the second one. The seal proved
|
|
23
|
+
* it detects an edit and does not detect a restore: copying an earlier
|
|
24
|
+
* `.flow-run.json` back over the current one is accepted — the file is
|
|
25
|
+
* internally consistent, because it really was written by this CLI — and it
|
|
26
|
+
* takes the attempt ledger back with it. So the count lives in a second file
|
|
27
|
+
* that only ever grows. But while that file sat NEXT to the state, inside the
|
|
28
|
+
* session folder, a `cp -r` of the folder carried both away and back, and
|
|
29
|
+
* deleting it reset the cap: the counter defended against restoring a file and
|
|
30
|
+
* fell to restoring the directory that contained it, which is the same move one
|
|
31
|
+
* level up.
|
|
32
|
+
*
|
|
33
|
+
* Now it is workspace runtime — `.<ns>/sessions/.flow-attempts/<folder>.json`,
|
|
34
|
+
* keyed by the session it counts, dot-prefixed so the session listing skips it
|
|
35
|
+
* and already inside the gitignore the CLI manages. Restoring a session folder
|
|
36
|
+
* cannot lower it, deleting the folder cannot delete it, and a recovery does not
|
|
37
|
+
* delete it either: it records how many attempts were given back. See
|
|
38
|
+
* `attemptsAt`.
|
|
39
|
+
*
|
|
40
|
+
* One file per run rather than one registry for the workspace, because two runs
|
|
41
|
+
* advance concurrently under two DIFFERENT locks: a shared registry would need a
|
|
42
|
+
* lock of its own, and losing that race would silently drop a run's floor —
|
|
43
|
+
* rebuilding the very hole the file exists to close.
|
|
44
|
+
*/
|
|
45
|
+
const COUNTER_VERSION = 2;
|
|
46
|
+
const NO_COUNTERS = { attempts: {}, granted: {} };
|
|
18
47
|
export function locateRun(paths, session) {
|
|
19
48
|
const dir = join(paths.cwdSessionsDir(), session);
|
|
20
49
|
return {
|
|
21
50
|
session,
|
|
22
51
|
dir,
|
|
23
52
|
statePath: join(dir, FLOW_RUN_STATE_FILE),
|
|
53
|
+
countersPath: paths.cwdFlowAttemptsFile(session),
|
|
24
54
|
lockPath: join(dir, `${FLOW_RUN_STATE_FILE}.lock`),
|
|
25
55
|
};
|
|
26
56
|
}
|
|
@@ -43,7 +73,187 @@ export async function readRun(fs, location) {
|
|
|
43
73
|
},
|
|
44
74
|
};
|
|
45
75
|
}
|
|
46
|
-
|
|
76
|
+
const parsed = parseRunState(await fs.readText(location.statePath));
|
|
77
|
+
if (!parsed.ok)
|
|
78
|
+
return parsed;
|
|
79
|
+
// Reconciled on the way OUT, never on the way in: whoever restored an older
|
|
80
|
+
// state has already handed it to us, and raising the floor here is what makes
|
|
81
|
+
// the restore worthless. Every reader goes through this function, so no surface
|
|
82
|
+
// of the CLI can see a run with attempts the counter says were already spent.
|
|
83
|
+
const counters = await readCounters(fs, location);
|
|
84
|
+
if (!counters.ok)
|
|
85
|
+
return counters;
|
|
86
|
+
const rolledBack = checkAgainstSealedFloor(parsed.state, counters.value);
|
|
87
|
+
if (rolledBack !== null)
|
|
88
|
+
return { ok: false, failure: rolledBack };
|
|
89
|
+
return {
|
|
90
|
+
ok: true,
|
|
91
|
+
state: withAttemptCounters(atCurrentVersion(parsed.state), {
|
|
92
|
+
floor: counters.value.attempts,
|
|
93
|
+
grants: counters.value.granted,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Whether the counter came back BEHIND the floor the state itself carries sealed.
|
|
99
|
+
*
|
|
100
|
+
* The write order is the counter first and the state second, so the counter is
|
|
101
|
+
* never behind: a process that dies between the two leaves it AHEAD, which costs
|
|
102
|
+
* an attempt nobody spent, and that is the only one of the two errors that is
|
|
103
|
+
* safe to make. The reverse cannot happen by running this CLI. It happens when
|
|
104
|
+
* the counter file is deleted, truncated or replaced by an older copy while the
|
|
105
|
+
* state stays — surgery on the accounting, and the only evidence of it that
|
|
106
|
+
* survives inside the seal.
|
|
107
|
+
*
|
|
108
|
+
* Refused rather than rebuilt. Rebuilding from the state is precisely what the
|
|
109
|
+
* counter exists to not do: the state is the file a restore rolls back, and
|
|
110
|
+
* seeding the floor from it would hand the evader the reset they came for while
|
|
111
|
+
* calling it a repair.
|
|
112
|
+
*/
|
|
113
|
+
function checkAgainstSealedFloor(state, counters) {
|
|
114
|
+
const behind = (sealed, live) => Object.entries(sealed ?? {}).find(([transition, count]) => count > (live[transition] ?? 0));
|
|
115
|
+
const lost = behind(state.attempt_floor, counters.attempts) ??
|
|
116
|
+
behind(state.attempt_grants, counters.granted);
|
|
117
|
+
if (lost === undefined)
|
|
118
|
+
return null;
|
|
119
|
+
return {
|
|
120
|
+
code: "FLOW_RUN_COUNTER_ROLLED_BACK",
|
|
121
|
+
message: `el contador de intentos quedó detrás del estado sellado: '${lost[0]}' declara ${lost[1]} y el contador no los tiene`,
|
|
122
|
+
action: "el contador se borró o se restauró una copia anterior: restaurá el archivo de intentos de la corrida, o descartá la corrida entera y re-adoptá la sesión con 'aw flow advance --flow <flow> --adopt' — no se reconstruye desde el estado, que es justo lo que una restauración rebobina",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The counter as it stands, or the conservative reading when there is none.
|
|
127
|
+
*
|
|
128
|
+
* An ABSENT file is the normal state of every run that started before the counter
|
|
129
|
+
* existed, and of every run that has not spent an attempt yet: it reads as "no
|
|
130
|
+
* floor beyond the ledger", the run keeps walking, and the next write seeds the
|
|
131
|
+
* file from the ledger it already has. Absence is not a hole — a state that DID
|
|
132
|
+
* spend attempts carries their floor inside its own seal, so an absent counter
|
|
133
|
+
* under such a state is caught by {@link checkAgainstSealedFloor} rather than
|
|
134
|
+
* read as zero.
|
|
135
|
+
*
|
|
136
|
+
* Everything else is refused with a cause, and the checks are the state file's
|
|
137
|
+
* own, in the same order: shape, then version, then the SEAL, then coherence.
|
|
138
|
+
* This file is read fail-closed for exactly the reason the state is — it was the
|
|
139
|
+
* surface added to resist manipulation, and while it was the only unsealed file
|
|
140
|
+
* of the run it was also the easiest one to manipulate: writing `granted: 99`
|
|
141
|
+
* into it turned the cap off for good.
|
|
142
|
+
*/
|
|
143
|
+
async function readCounters(fs, location) {
|
|
144
|
+
if (!(await fs.exists(location.countersPath)))
|
|
145
|
+
return { ok: true, value: NO_COUNTERS };
|
|
146
|
+
const refuse = (why) => ({
|
|
147
|
+
ok: false,
|
|
148
|
+
failure: {
|
|
149
|
+
code: "FLOW_RUN_COUNTER_INVALID",
|
|
150
|
+
message: `el contador de intentos de la corrida ${why}`,
|
|
151
|
+
action: "no se avanza con una contabilidad de intentos ilegible ni con una que fue editada fuera del CLI: restaurá el archivo, o descartá la corrida entera y re-adoptá la sesión con 'aw flow advance --flow <flow> --adopt'",
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = JSON.parse(await fs.readText(location.countersPath));
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return refuse("no es JSON válido");
|
|
160
|
+
}
|
|
161
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
162
|
+
return refuse("no es un objeto JSON");
|
|
163
|
+
}
|
|
164
|
+
const record = parsed;
|
|
165
|
+
if (record.version !== COUNTER_VERSION) {
|
|
166
|
+
return refuse(`declara una versión que este CLI no lee: ${String(record.version)}`);
|
|
167
|
+
}
|
|
168
|
+
const attempts = readCounterMap(record.attempts);
|
|
169
|
+
const granted = readCounterMap(record.granted);
|
|
170
|
+
if (attempts === null || granted === null)
|
|
171
|
+
return refuse("no es un contador por transición");
|
|
172
|
+
const value = { attempts, granted };
|
|
173
|
+
if (record.digest !== counterDigest(location.session, value)) {
|
|
174
|
+
return refuse("no coincide con su propio sello");
|
|
175
|
+
}
|
|
176
|
+
// A grant is what a recovery forgave, and forgiving more than was ever spent
|
|
177
|
+
// is not a state this CLI can produce: `raiseCounters` only ever copies the
|
|
178
|
+
// grants the state recorded, and a recovery grants exactly what it found
|
|
179
|
+
// spent. A file that says otherwise is asking for a cap that never fires.
|
|
180
|
+
const forgiven = Object.entries(granted).find(([id, count]) => count > (attempts[id] ?? 0));
|
|
181
|
+
if (forgiven !== undefined) {
|
|
182
|
+
return refuse(`perdona ${forgiven[1]} intentos de '${forgiven[0]}' y solo registra ${attempts[forgiven[0]] ?? 0}`);
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, value };
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The counter's seal — the same canonicalization the rest of the protocol uses.
|
|
188
|
+
*
|
|
189
|
+
* The SESSION is inside it, and not as decoration: the file lives in a shared
|
|
190
|
+
* runtime folder keyed by folder name, so sealing the key is what makes one run's
|
|
191
|
+
* counter unusable as another's.
|
|
192
|
+
*/
|
|
193
|
+
function counterDigest(session, counters) {
|
|
194
|
+
return semanticDigest({ version: COUNTER_VERSION, session, ...counters });
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Raise the counter to what this state knows, and hand the state back reconciled.
|
|
198
|
+
*
|
|
199
|
+
* Monotone by construction on both halves: the attempts seen can only go up —
|
|
200
|
+
* `Math.max` against what the file already said — and the grants a recovery
|
|
201
|
+
* recorded come from the state, which only ever adds to them. A state written
|
|
202
|
+
* from a restored ledger therefore raises nothing and gets the live floor back.
|
|
203
|
+
*/
|
|
204
|
+
async function raiseCounters(fs, location, state) {
|
|
205
|
+
const current = await readCounters(fs, location);
|
|
206
|
+
if (!current.ok)
|
|
207
|
+
return current;
|
|
208
|
+
const spent = new Map();
|
|
209
|
+
for (const attempt of state.attempts) {
|
|
210
|
+
spent.set(attempt.transition, (spent.get(attempt.transition) ?? 0) + 1);
|
|
211
|
+
}
|
|
212
|
+
const attempts = { ...current.value.attempts };
|
|
213
|
+
for (const [transition, count] of spent) {
|
|
214
|
+
attempts[transition] = Math.max(attempts[transition] ?? 0, count);
|
|
215
|
+
}
|
|
216
|
+
const granted = { ...current.value.granted };
|
|
217
|
+
for (const [transition, forgiven] of Object.entries(state.attempt_grants ?? {})) {
|
|
218
|
+
granted[transition] = Math.max(granted[transition] ?? 0, forgiven);
|
|
219
|
+
}
|
|
220
|
+
const next = { attempts, granted };
|
|
221
|
+
if (changed(current.value, next))
|
|
222
|
+
await writeCounters(fs, location, next);
|
|
223
|
+
return { ok: true, state: withAttemptCounters(state, { floor: attempts, grants: granted }) };
|
|
224
|
+
}
|
|
225
|
+
async function writeCounters(fs, location, counters) {
|
|
226
|
+
await fs.mkdirp(dirname(location.countersPath));
|
|
227
|
+
await fs.writeText(location.countersPath, `${JSON.stringify({
|
|
228
|
+
version: COUNTER_VERSION,
|
|
229
|
+
session: location.session,
|
|
230
|
+
...counters,
|
|
231
|
+
digest: counterDigest(location.session, counters),
|
|
232
|
+
}, null, 2)}\n`);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Start the counter over for a run that is being created.
|
|
236
|
+
*
|
|
237
|
+
* The file goes rather than being zeroed, so an adopted run is byte-identical to
|
|
238
|
+
* one that never had a counter: the next attempt seeds it again from the ledger.
|
|
239
|
+
*/
|
|
240
|
+
async function resetCounters(fs, location, state) {
|
|
241
|
+
await fs.remove(location.countersPath);
|
|
242
|
+
return { ok: true, state: withAttemptCounters(state, { floor: {}, grants: {} }) };
|
|
243
|
+
}
|
|
244
|
+
function changed(before, after) {
|
|
245
|
+
return (JSON.stringify(before.attempts) !== JSON.stringify(after.attempts) ||
|
|
246
|
+
JSON.stringify(before.granted) !== JSON.stringify(after.granted));
|
|
247
|
+
}
|
|
248
|
+
function readCounterMap(value) {
|
|
249
|
+
if (value === undefined)
|
|
250
|
+
return {};
|
|
251
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
252
|
+
return null;
|
|
253
|
+
const entries = Object.entries(value);
|
|
254
|
+
if (!entries.every(([, count]) => Number.isInteger(count) && count >= 0))
|
|
255
|
+
return null;
|
|
256
|
+
return Object.fromEntries(entries);
|
|
47
257
|
}
|
|
48
258
|
/**
|
|
49
259
|
* Run one mutation under the run's lock and persist its result.
|
|
@@ -95,10 +305,31 @@ export async function applyUnderLock(fs, location, mutate, options = {}) {
|
|
|
95
305
|
return result;
|
|
96
306
|
if (result.persist === false)
|
|
97
307
|
return result;
|
|
308
|
+
// The counter first, the state second, and the order is the contract. If the
|
|
309
|
+
// process dies between them the counter is AHEAD of the ledger, which costs
|
|
310
|
+
// an attempt nobody spent; the other order would leave it BEHIND, which is an
|
|
311
|
+
// attempt somebody spent and can spend again. Only one of those two errors is
|
|
312
|
+
// safe to make.
|
|
313
|
+
//
|
|
314
|
+
// An ADOPTION is the one case that starts the counter over, and it is not a
|
|
315
|
+
// loophole in the monotonicity: a state that was absent means this run is
|
|
316
|
+
// being created now, and the only way to get there deliberately is to throw
|
|
317
|
+
// the previous run away whole — every applied transition with it. What the
|
|
318
|
+
// counter defends against is keeping the position and losing the attempts;
|
|
319
|
+
// paying for a reset with the entire run is not that trade. Keeping the old
|
|
320
|
+
// count would be worse than useless: re-adopting after a corrupt state — the
|
|
321
|
+
// repair this CLI prints — would come back pre-exhausted at its first gate.
|
|
322
|
+
const raised = state === null
|
|
323
|
+
? await resetCounters(fs, location, result.state)
|
|
324
|
+
: await raiseCounters(fs, location, result.state);
|
|
325
|
+
if (!raised.ok)
|
|
326
|
+
return raised;
|
|
98
327
|
// One write, after the next state is complete and sealed: nothing partial
|
|
99
328
|
// can be observed because nothing partial is ever written.
|
|
100
|
-
await fs.writeText(location.statePath, serializeRunState(
|
|
101
|
-
|
|
329
|
+
await fs.writeText(location.statePath, serializeRunState(raised.state));
|
|
330
|
+
// The reconciled state is what the caller gets back, because the digest it
|
|
331
|
+
// holds is the one the next compare-and-swap will be judged against.
|
|
332
|
+
return { ...result, state: raised.state };
|
|
102
333
|
}
|
|
103
334
|
finally {
|
|
104
335
|
await lock.release();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-state-service.js","sourceRoot":"","sources":["../../../src/application/flow/run-state-service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"run-state-service.js","sourceRoot":"","sources":["../../../src/application/flow/run-state-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
* `renderHuman`, and a boundary nobody can see is a boundary that did not happen.
|
|
22
22
|
*/
|
|
23
23
|
import { join } from "node:path";
|
|
24
|
-
import { authorizeEffects } from "../../domain/capability/effects.js";
|
|
24
|
+
import { authorizeEffects, touchesTheWorld, } from "../../domain/capability/effects.js";
|
|
25
25
|
import { AttemptLedger } from "../../domain/capability/protocol.js";
|
|
26
|
-
import { claimedSeal, parseFlowAnswer } from "../../domain/flow/answer.js";
|
|
26
|
+
import { claimedSeal, parseFlowAnswer, spendsAttempt, } from "../../domain/flow/answer.js";
|
|
27
27
|
import { actionOf, approvalGrantOf, effectsOf, internalActionOf, journeyOfFlow, proposalContractOf, publishApprovalOf, scopesSources, } from "../../domain/flow/authority.js";
|
|
28
28
|
import { effectApprovalDigest } from "../../domain/flow/authorization.js";
|
|
29
29
|
import { PAUSE_LABEL, STOP_LABEL, stepOf, } from "../../domain/flow/directive.js";
|
|
30
30
|
import { executionVerdict } from "../../domain/flow/execution-result.js";
|
|
31
|
-
import { applyTransition, checkAgainstJourney, withApproval, withAttempt, withBoundary, withObservation, withProposal, withScope, } from "../../domain/flow/run-state.js";
|
|
31
|
+
import { applyTransition, checkAgainstJourney, restatesLastEvent, withApproval, withAttempt, withBoundary, withEvent, withObservation, withProposal, withScope, } from "../../domain/flow/run-state.js";
|
|
32
32
|
import { destinationsOf, sealProposal } from "../../domain/proposal.js";
|
|
33
33
|
import { baseDigest } from "../../domain/proposal.js";
|
|
34
34
|
import { reservationMarker } from "../../domain/reservation.js";
|
|
@@ -211,8 +211,9 @@ function decide(state, input, snapshot) {
|
|
|
211
211
|
// nothing — matching a recorded attempt requires resending exactly what already
|
|
212
212
|
// ran, and a retry applies nothing.
|
|
213
213
|
const identity = attemptIdentity(state, input, claimedSeal(input.raw) ?? resolved.seal, resolved.stopped.id);
|
|
214
|
+
const cost = { journey, identity };
|
|
214
215
|
// 1 · Resend, before anything else.
|
|
215
|
-
const resend = resendCheck(state, resolved,
|
|
216
|
+
const resend = resendCheck(state, resolved, cost);
|
|
216
217
|
if (resend !== null)
|
|
217
218
|
return resend;
|
|
218
219
|
// The action the run is waiting on is the one that was EMITTED, and the seal
|
|
@@ -220,11 +221,11 @@ function decide(state, input, snapshot) {
|
|
|
220
221
|
// is the diagnosis: when the two differ, the invocation changed underneath a run
|
|
221
222
|
// in flight (a CLI upgraded mid-run), which is a different problem from a state
|
|
222
223
|
// that moved — and it has a different answer.
|
|
223
|
-
const drifted = actionDrift(state, resolved);
|
|
224
|
+
const drifted = actionDrift(state, resolved, cost);
|
|
224
225
|
if (drifted !== null)
|
|
225
226
|
return drifted;
|
|
226
227
|
// 2 · The boundary in force decides what is admissible.
|
|
227
|
-
const admissible = admit(state, resolved, resolved.stopped, input,
|
|
228
|
+
const admissible = admit(state, resolved, resolved.stopped, input, cost);
|
|
228
229
|
if ("decision" in admissible)
|
|
229
230
|
return admissible.decision;
|
|
230
231
|
const parsed = admissible;
|
|
@@ -237,17 +238,11 @@ function decide(state, input, snapshot) {
|
|
|
237
238
|
// preview showed. Neither ever applies the step by itself.
|
|
238
239
|
const scoped = scopeFrom(state, resolved.stopped, parsed.answer, snapshot.scope);
|
|
239
240
|
if ("failure" in scoped) {
|
|
240
|
-
return reject(state, resolved, scoped.failure.message, {
|
|
241
|
-
code: scoped.failure.code,
|
|
242
|
-
action: scoped.failure.action,
|
|
243
|
-
});
|
|
241
|
+
return reject(state, resolved, scoped.failure.message, { code: scoped.failure.code, action: scoped.failure.action }, cost);
|
|
244
242
|
}
|
|
245
243
|
const sealed = sealFrom(scoped.state, resolved.stopped, parsed.answer, snapshot.destinations);
|
|
246
244
|
if ("failure" in sealed) {
|
|
247
|
-
return reject(state, resolved, sealed.failure.message, {
|
|
248
|
-
code: sealed.failure.code,
|
|
249
|
-
action: sealed.failure.action,
|
|
250
|
-
});
|
|
245
|
+
return reject(state, resolved, sealed.failure.message, { code: sealed.failure.code, action: sealed.failure.action }, cost);
|
|
251
246
|
}
|
|
252
247
|
const granted = resolved.kind === "authorization" ? (resolved.authorization?.planned ?? []) : [];
|
|
253
248
|
const approved = grantOf(sealed.state, resolved, parsed.answer, granted, journey);
|
|
@@ -336,8 +331,16 @@ function scopeFrom(state, stopped, answer, snapshot) {
|
|
|
336
331
|
}
|
|
337
332
|
return { state: withScope(state, { plan, sources }) };
|
|
338
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* A scope answer the boundary read and refused — under its OWN code.
|
|
336
|
+
*
|
|
337
|
+
* It reported `FLOW_ANSWER_INVALID` for a while, and that code means "the
|
|
338
|
+
* envelope could not be read as an answer", so the same string stood for two
|
|
339
|
+
* opposite facts: one that must not cost an attempt and one that must. A reader
|
|
340
|
+
* cannot tell them apart, and neither could the table that decides the charge.
|
|
341
|
+
*/
|
|
339
342
|
function invalidScope(message, action) {
|
|
340
|
-
return { code: "
|
|
343
|
+
return { code: "FLOW_SCOPE_INVALID", message, action };
|
|
341
344
|
}
|
|
342
345
|
/**
|
|
343
346
|
* Seat the proposal an authoring answer just handed over — sealed, not believed.
|
|
@@ -498,7 +501,7 @@ function grantOf(state, resolved, answer, granted, journey) {
|
|
|
498
501
|
* a payload the boundary's own contract rejects, a decline (a real answer that
|
|
499
502
|
* applies nothing), and an execution result that did not earn its transition.
|
|
500
503
|
*/
|
|
501
|
-
function admit(state, resolved, stopped, input,
|
|
504
|
+
function admit(state, resolved, stopped, input, cost) {
|
|
502
505
|
const expectedApproval = resolved.kind === "authorization"
|
|
503
506
|
? effectApprovalDigest(stopped.id, resolved.authorization?.planned ?? [])
|
|
504
507
|
: null;
|
|
@@ -514,11 +517,13 @@ function admit(state, resolved, stopped, input, spent) {
|
|
|
514
517
|
request: resolved.request,
|
|
515
518
|
});
|
|
516
519
|
if (!parsed.ok) {
|
|
517
|
-
// An answer the boundary refused
|
|
518
|
-
// the chassis' cap counts.
|
|
519
|
-
//
|
|
520
|
+
// An answer the boundary EVALUATED and refused is an attempt spent: it is the
|
|
521
|
+
// exact event the chassis' cap counts. A payload it could not read as an
|
|
522
|
+
// answer at all is not — see {@link FLOW_ANSWER_REJECTIONS}, which `reject`
|
|
523
|
+
// consults. A resend, a decline and a pause are not failed tries either, and
|
|
524
|
+
// they are classified there rather than being exempted here.
|
|
520
525
|
return {
|
|
521
|
-
decision: reject(state, resolved, parsed.failure.message, { code: parsed.failure.code, action: parsed.failure.action },
|
|
526
|
+
decision: reject(state, resolved, parsed.failure.message, { code: parsed.failure.code, action: parsed.failure.action }, cost),
|
|
522
527
|
};
|
|
523
528
|
}
|
|
524
529
|
// The flow control is a real answer, and neither half applies anything. They
|
|
@@ -532,7 +537,7 @@ function admit(state, resolved, stopped, input, spent) {
|
|
|
532
537
|
code: "FLOW_BOUNDARY_PAUSED",
|
|
533
538
|
action: "escribí el CHECKPOINT con 'aw checkpoint-write', compactá, y volvé con 'aw flow advance' a esta misma frontera",
|
|
534
539
|
outcome: "needs_input",
|
|
535
|
-
}),
|
|
540
|
+
}, cost),
|
|
536
541
|
};
|
|
537
542
|
}
|
|
538
543
|
if (parsed.answer.choice === STOP_LABEL) {
|
|
@@ -541,7 +546,7 @@ function admit(state, resolved, stopped, input, spent) {
|
|
|
541
546
|
code: "FLOW_BOUNDARY_DECLINED",
|
|
542
547
|
action: "reanudá con 'aw flow advance' cuando quieras retomar esta frontera",
|
|
543
548
|
outcome: "cancelled",
|
|
544
|
-
}),
|
|
549
|
+
}, cost),
|
|
545
550
|
};
|
|
546
551
|
}
|
|
547
552
|
// An execution result has to EARN the transition. Anything short of a completed
|
|
@@ -553,11 +558,49 @@ function admit(state, resolved, stopped, input, spent) {
|
|
|
553
558
|
// would leave the transition pending for an effect nobody could produce.
|
|
554
559
|
const verdict = executionVerdict(parsed.answer.result, resolved.action, effectsOfTransition(state, stopped));
|
|
555
560
|
if (verdict !== null) {
|
|
556
|
-
|
|
561
|
+
const trace = declaredTrace(stopped, resolved, parsed.answer, verdict);
|
|
562
|
+
return {
|
|
563
|
+
decision: reject(state, resolved, verdict.message, verdict.detail, {
|
|
564
|
+
...cost,
|
|
565
|
+
...(trace === null ? {} : { trace }),
|
|
566
|
+
}),
|
|
567
|
+
};
|
|
557
568
|
}
|
|
558
569
|
}
|
|
559
570
|
return parsed;
|
|
560
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* The material fact a REFUSED external result still leaves behind — or `null`.
|
|
574
|
+
*
|
|
575
|
+
* A rejected result is not the same as a result about nothing. The executor may
|
|
576
|
+
* have run the command, written the file and come back with evidence the
|
|
577
|
+
* boundary did not accept: the effect reached the world regardless of what the
|
|
578
|
+
* verdict decided about it. Nothing else in the run records that — the trace is
|
|
579
|
+
* only written by the internal driver — so an external execution could declare
|
|
580
|
+
* `applied` classes, be refused, exhaust the boundary, and then be handed back as
|
|
581
|
+
* answerable by a recovery whose whole job is to refuse exactly that.
|
|
582
|
+
*
|
|
583
|
+
* Recorded when the result declares an effect past `read_only`, and when the
|
|
584
|
+
* verdict is a PARTIAL one: "it finished and did not apply everything" is the
|
|
585
|
+
* case where what did reach the world is precisely the unknown.
|
|
586
|
+
*/
|
|
587
|
+
function declaredTrace(stopped, resolved, answer, verdict) {
|
|
588
|
+
const applied = answer.result?.effects.applied ?? [];
|
|
589
|
+
if (!touchesTheWorld(applied) && verdict.detail.code !== "FLOW_EFFECT_PARTIAL")
|
|
590
|
+
return null;
|
|
591
|
+
const invocation = resolved.action?.invocation;
|
|
592
|
+
return {
|
|
593
|
+
kind: "failed",
|
|
594
|
+
transition: stopped.id,
|
|
595
|
+
// What was really run, named the way whoever ran it would recognize it: an
|
|
596
|
+
// external execution has no internal operation id to quote.
|
|
597
|
+
operation: invocation === undefined ? stopped.id : [invocation.program, ...invocation.args].join(" "),
|
|
598
|
+
code: verdict.detail.code,
|
|
599
|
+
message: verdict.message,
|
|
600
|
+
recovery: verdict.detail.action,
|
|
601
|
+
effects: [...applied],
|
|
602
|
+
};
|
|
603
|
+
}
|
|
561
604
|
/**
|
|
562
605
|
* Whether the sealed action changed underneath a run that is standing on it.
|
|
563
606
|
*
|
|
@@ -566,7 +609,7 @@ function admit(state, resolved, stopped, input, spent) {
|
|
|
566
609
|
* staleness the seal would otherwise report, because the fix is different: nothing
|
|
567
610
|
* is wrong with the state, the invocation is simply no longer the one that ran.
|
|
568
611
|
*/
|
|
569
|
-
function actionDrift(state, resolved) {
|
|
612
|
+
function actionDrift(state, resolved, cost) {
|
|
570
613
|
const pending = state.pending_action;
|
|
571
614
|
if (pending === null || resolved.action === null)
|
|
572
615
|
return null;
|
|
@@ -575,7 +618,7 @@ function actionDrift(state, resolved) {
|
|
|
575
618
|
return reject(state, resolved, "la acción de esta frontera cambió después de emitirse: el resultado corresponde a otra invocación", {
|
|
576
619
|
code: "FLOW_ACTION_CHANGED",
|
|
577
620
|
action: "volvé a correr 'aw flow advance' para recibir la acción vigente y ejecutá esa antes de responder",
|
|
578
|
-
});
|
|
621
|
+
}, cost);
|
|
579
622
|
}
|
|
580
623
|
/**
|
|
581
624
|
* Approving an effect is NOT deciding the step, and never executing it.
|
|
@@ -651,7 +694,8 @@ function applyAndAdvance(approved, journey, stopped, identity, answer) {
|
|
|
651
694
|
* for the reason the registry gives everywhere it derives: two sources for one
|
|
652
695
|
* fact disagree eventually, and then the state is unusable.
|
|
653
696
|
*/
|
|
654
|
-
function resendCheck(state, resolved,
|
|
697
|
+
function resendCheck(state, resolved, cost) {
|
|
698
|
+
const identity = cost.identity;
|
|
655
699
|
const ledger = new AttemptLedger();
|
|
656
700
|
for (const past of state.attempts) {
|
|
657
701
|
const replay = ledger.record(past);
|
|
@@ -677,24 +721,32 @@ function resendCheck(state, resolved, identity) {
|
|
|
677
721
|
code: "FLOW_ANSWER_RESENT",
|
|
678
722
|
action: "la frontera vigente es la que devuelve esta directiva; contestá esa",
|
|
679
723
|
outcome: "completed",
|
|
680
|
-
});
|
|
724
|
+
}, cost);
|
|
681
725
|
}
|
|
682
726
|
/**
|
|
683
727
|
* A rejection, expressed as the recalculated directive.
|
|
684
728
|
*
|
|
685
|
-
*
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
729
|
+
* Whether it spends an attempt is decided HERE, from the code, and never by the
|
|
730
|
+
* call site. It used to be the call site's business, and six refusals of this
|
|
731
|
+
* file simply forgot to pass the parameter: a scope answered with an alias the
|
|
732
|
+
* plan does not name never exhausted, never degraded and never became
|
|
733
|
+
* recoverable — an unbounded loop inside the very mechanism that exists to bound
|
|
734
|
+
* one. One place decides, one table says which codes count.
|
|
735
|
+
*
|
|
736
|
+
* `cost` is absent only where there is no boundary to charge: a run that already
|
|
737
|
+
* finished, or one that never stopped at anything.
|
|
689
738
|
*/
|
|
690
|
-
function reject(state, resolved, message, detail,
|
|
739
|
+
function reject(state, resolved, message, detail, cost) {
|
|
691
740
|
// A refused answer is the one rejection that CHANGES the run: the attempt is
|
|
692
741
|
// recorded, so the boundary that has been tried to its cap degrades instead of
|
|
693
742
|
// being emitted again. The boundary is recalculated over that state, which is
|
|
694
743
|
// what turns the last refusal into the degradation rather than into an
|
|
695
744
|
// identical question with a different error string.
|
|
696
|
-
const
|
|
697
|
-
|
|
745
|
+
const traced = cost?.trace === undefined || restatesLastEvent(state, cost.trace)
|
|
746
|
+
? state
|
|
747
|
+
: withEvent(state, cost.trace);
|
|
748
|
+
const after = cost !== undefined && spendsAttempt(detail.code) ? withAttempt(traced, cost.identity) : traced;
|
|
749
|
+
const now = cost === undefined || after === state ? resolved : resolveBoundary(after, cost.journey);
|
|
698
750
|
const built = directiveFor(after, now, [], {
|
|
699
751
|
// No outcome named ⇒ the boundary decides it: a finished journey reports
|
|
700
752
|
// `completed`, an open one `needs_input`. Hardcoding one here would let a
|
|
@@ -716,7 +768,10 @@ function reject(state, resolved, message, detail, spent) {
|
|
|
716
768
|
ok: true,
|
|
717
769
|
state: after,
|
|
718
770
|
value: { directive, advanced: false },
|
|
719
|
-
|
|
771
|
+
// Persisted exactly when something really changed, whichever half changed it.
|
|
772
|
+
// Tying this to the attempt alone would drop a material trace that a refusal
|
|
773
|
+
// produced without charging for it.
|
|
774
|
+
persist: after !== state,
|
|
720
775
|
};
|
|
721
776
|
}
|
|
722
777
|
/**
|