@trydreamstate/cli 1.3.3 → 1.3.5
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/README.md +14 -7
- package/agent-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/hooks/approve-readonly.sh +2 -2
- package/agent-plugin/hooks/hooks.json +1 -1
- package/agent-plugin/skills/crm/SKILL.md +2 -2
- package/agent-plugin/skills/diagnostics/SKILL.md +3 -1
- package/agent-plugin/skills/dreamstate/SKILL.md +5 -3
- package/agent-plugin/skills/first-win/SKILL.md +2 -2
- package/agent-plugin/skills/inbox-replies/SKILL.md +3 -1
- package/agent-plugin/skills/knowledge/SKILL.md +3 -5
- package/agent-plugin/skills/linkedin-posts/SKILL.md +3 -1
- package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +3 -3
- package/agent-plugin/skills/list-building/SKILL.md +4 -9
- package/agent-plugin/skills/list-building/column-authoring.md +3 -2
- package/agent-plugin/skills/list-building/sourcing.md +2 -2
- package/agent-plugin/skills/outreach/SKILL.md +3 -3
- package/agent-plugin/skills/plays/SKILL.md +3 -3
- package/agent-plugin/skills/reddit-posts/SKILL.md +3 -1
- package/agent-plugin/skills/skill-authoring/SKILL.md +3 -1
- package/agent-plugin/skills/social/SKILL.md +3 -5
- package/agent-plugin/skills/visibility/SKILL.md +2 -6
- package/agent-plugin/skills/workflows/SKILL.md +2 -2
- package/agent-plugin/skills/workflows/lifecycle-and-enrollment.md +1 -1
- package/agent-plugin/skills/worksheet-tidy/SKILL.md +3 -1
- package/agent-plugin/skills/writing/SKILL.md +3 -5
- package/agent-plugin/skills/x-posts/SKILL.md +3 -1
- package/dist/agentGuidance.generated.json +4 -4
- package/dist/capabilityPosture.d.ts +12 -0
- package/dist/capabilityPosture.d.ts.map +1 -0
- package/dist/capabilityPosture.js +39 -0
- package/dist/client.d.ts +9 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +110 -63
- package/dist/commands/sequences.d.ts +3 -2
- package/dist/commands/sequences.d.ts.map +1 -1
- package/dist/commands/sequences.js +19 -37
- package/dist/commands/shared.d.ts.map +1 -1
- package/dist/commands/shared.js +22 -45
- package/dist/commands/workflows.d.ts +3 -2
- package/dist/commands/workflows.d.ts.map +1 -1
- package/dist/commands/workflows.js +19 -35
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -2
- package/dist/errors.d.ts +2 -6
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -43
- package/dist/gatedRun.d.ts +2 -2
- package/dist/gatedRun.d.ts.map +1 -1
- package/dist/gatedRun.js +157 -60
- package/dist/generated/packVerbs.generated.json +492 -0
- package/dist/generated/runContract.generated.d.ts +4 -0
- package/dist/generated/runContract.generated.d.ts.map +1 -0
- package/dist/generated/runContract.generated.js +7 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -59
- package/dist/money.d.ts +10 -0
- package/dist/money.d.ts.map +1 -0
- package/dist/money.js +75 -0
- package/dist/registryManifest.d.ts +6 -0
- package/dist/registryManifest.d.ts.map +1 -0
- package/dist/registryManifest.js +66 -0
- package/dist/runContract.d.ts +10 -0
- package/dist/runContract.d.ts.map +1 -0
- package/dist/runContract.js +13 -0
- package/dist/runWait.d.ts +0 -1
- package/dist/runWait.d.ts.map +1 -1
- package/dist/runWait.js +2 -13
- package/dist/turnCache.d.ts +3 -1
- package/dist/turnCache.d.ts.map +1 -1
- package/dist/turnCache.js +6 -2
- package/package.json +4 -2
package/dist/gatedRun.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.code = code;
|
|
4
|
-
exports.toolTurnToken = toolTurnToken;
|
|
5
|
-
exports.omitsToolTurnForInlineTableRead = omitsToolTurnForInlineTableRead;
|
|
6
4
|
exports.capabilityRecord = capabilityRecord;
|
|
7
5
|
exports.capabilityIdempotencyKey = capabilityIdempotencyKey;
|
|
8
6
|
exports.parameterIdempotencyKey = parameterIdempotencyKey;
|
|
9
7
|
exports.cliSurfaceRefusal = cliSurfaceRefusal;
|
|
10
8
|
exports.planFingerprint = planFingerprint;
|
|
11
9
|
exports.runGated = runGated;
|
|
10
|
+
exports.refusalFromRunResult = refusalFromRunResult;
|
|
12
11
|
exports.readCapability = readCapability;
|
|
13
12
|
const node_crypto_1 = require("node:crypto");
|
|
14
13
|
const client_1 = require("./client");
|
|
@@ -17,6 +16,9 @@ const idempotency_1 = require("./idempotency");
|
|
|
17
16
|
const schemaCoercion_1 = require("./schemaCoercion");
|
|
18
17
|
const runWait_1 = require("./runWait");
|
|
19
18
|
const turnCache_1 = require("./turnCache");
|
|
19
|
+
const capabilityPosture_1 = require("./capabilityPosture");
|
|
20
|
+
const registryManifest_1 = require("./registryManifest");
|
|
21
|
+
const runContract_1 = require("./runContract");
|
|
20
22
|
/**
|
|
21
23
|
* The one path every capability run takes: `tools run` and every pack verb
|
|
22
24
|
* (`sequences ...`, `workflows ...`) call runGated. Gating lives here and only
|
|
@@ -32,43 +34,12 @@ function code(error) {
|
|
|
32
34
|
? error.code
|
|
33
35
|
: undefined;
|
|
34
36
|
}
|
|
35
|
-
function
|
|
37
|
+
function optionalToolTurnToken(value) {
|
|
36
38
|
const root = value && typeof value === 'object' ? value : {};
|
|
37
39
|
const data = root.data && typeof root.data === 'object' ? root.data : {};
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Free sync reads. The server runs these on the inline read path when
|
|
44
|
-
* the body has no tool_turn_token and the request has no Idempotency-Key
|
|
45
|
-
* (isInlineReadEligible). A tools get before the run is a second HTTP the
|
|
46
|
-
* read does not need. Measured 2026-10-04: tables.workbooks.get full live CLI
|
|
47
|
-
* 0.726s against whoami 0.455s, about 0.27s of tools get per harness read-back.
|
|
48
|
-
* knowledge.brain.context.get is the same class (catalog: credit_class free,
|
|
49
|
-
* async_policy.mode sync, mutates not set). Live 2026-10-05 a standalone
|
|
50
|
-
* tools get of that id was 0.400s including node startup, next to whoami
|
|
51
|
-
* 0.422s, so the same-process skip is one HTTP, not a second process.
|
|
52
|
-
* The set name is historical. Writes are not in this set: they stay fetch-first.
|
|
53
|
-
*/
|
|
54
|
-
const INLINE_READ_CAPABILITIES = new Set([
|
|
55
|
-
'tables.workbooks.get',
|
|
56
|
-
'tables.workbooks.overview',
|
|
57
|
-
'tables.workbooks.list',
|
|
58
|
-
'tables.columns.list',
|
|
59
|
-
'tables.columns.stats',
|
|
60
|
-
'tables.rows.query',
|
|
61
|
-
'tables.rows.get',
|
|
62
|
-
'tables.rows.count',
|
|
63
|
-
'knowledge.brain.context.get',
|
|
64
|
-
// Both are stored, free, synchronous inbox reads. Keeping either behind a
|
|
65
|
-
// tools_get turns one list or thread open into two HTTP requests, while the
|
|
66
|
-
// canonical inline-read gate already validates their read-only contract.
|
|
67
|
-
'unibox.inbox.unified_list',
|
|
68
|
-
'unibox.inbox.conversation_get',
|
|
69
|
-
]);
|
|
70
|
-
function omitsToolTurnForInlineTableRead(capabilityId) {
|
|
71
|
-
return INLINE_READ_CAPABILITIES.has(capabilityId);
|
|
40
|
+
return typeof data.tool_turn_token === 'string' && data.tool_turn_token.trim()
|
|
41
|
+
? data.tool_turn_token
|
|
42
|
+
: undefined;
|
|
72
43
|
}
|
|
73
44
|
/** The capability record inside a tools get response, or null when the shape is unknown. */
|
|
74
45
|
function capabilityRecord(fetched) {
|
|
@@ -138,7 +109,6 @@ function cliSurfaceRefusal(capabilityId, fetched) {
|
|
|
138
109
|
return null;
|
|
139
110
|
return new client_1.CliError(`${capabilityId} is not available on the CLI surface. Use the web app, MCP or the Architect for it.`, 'unsupported_capability', 0, { details: { capability_id: capabilityId, surface: 'cli' }, retryable: false });
|
|
140
111
|
}
|
|
141
|
-
const QUOTE_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
142
112
|
/**
|
|
143
113
|
* What a quote prices: the params plus the ceiling. Keys that say how a call is
|
|
144
114
|
* made, not what it does, are left out, like the server's own plan digest.
|
|
@@ -164,7 +134,7 @@ function quoteFromResult(value, depth = 0) {
|
|
|
164
134
|
return null;
|
|
165
135
|
}
|
|
166
136
|
const entry = value;
|
|
167
|
-
if (typeof entry.quote_hash === 'string' && QUOTE_HASH_PATTERN.test(entry.quote_hash)) {
|
|
137
|
+
if (typeof entry.quote_hash === 'string' && runContract_1.QUOTE_HASH_PATTERN.test(entry.quote_hash)) {
|
|
168
138
|
const estimate = entry.estimate && typeof entry.estimate === 'object' ? entry.estimate : null;
|
|
169
139
|
return {
|
|
170
140
|
quote_hash: entry.quote_hash,
|
|
@@ -178,11 +148,103 @@ function quoteFromResult(value, depth = 0) {
|
|
|
178
148
|
}
|
|
179
149
|
return null;
|
|
180
150
|
}
|
|
181
|
-
function
|
|
151
|
+
function quotePriced(capabilityId, body, quote) {
|
|
182
152
|
const params = body.params && typeof body.params === 'object' ? body.params : {};
|
|
183
153
|
const rows = typeof params.max_rows === 'number' ? `${params.max_rows} rows` : 'rows not stated';
|
|
184
154
|
const credits = quote.maximum_credits === null ? 'credits not stated' : `up to ${quote.maximum_credits} credits`;
|
|
185
|
-
return `Dry run of ${capabilityId} priced: ${rows}, ${credits}
|
|
155
|
+
return `Dry run of ${capabilityId} priced: ${rows}, ${credits}.`;
|
|
156
|
+
}
|
|
157
|
+
/** A known price of 0 spends nothing. An unknown price or a positive price is sent only after the caller passes the approval flag. */
|
|
158
|
+
function spendNeedsApproval(maximum, approved) {
|
|
159
|
+
if (approved)
|
|
160
|
+
return false;
|
|
161
|
+
if (maximum === 0)
|
|
162
|
+
return false;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
const CREDENTIALS_REQUIRED_SENTENCE = 'A connected credential is required before this run can continue.';
|
|
166
|
+
function readinessRecord(value) {
|
|
167
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
168
|
+
}
|
|
169
|
+
function blockerSentence(blocker) {
|
|
170
|
+
if (typeof blocker === 'string' && blocker.trim())
|
|
171
|
+
return blocker.trim();
|
|
172
|
+
const record = readinessRecord(blocker);
|
|
173
|
+
const message = record?.message;
|
|
174
|
+
return typeof message === 'string' && message.trim() ? message.trim() : null;
|
|
175
|
+
}
|
|
176
|
+
function blockerCodeOf(blocker) {
|
|
177
|
+
const record = readinessRecord(blocker);
|
|
178
|
+
const code = record?.code;
|
|
179
|
+
return typeof code === 'string' && /^[a-z][a-z0-9_]*$/.test(code) ? code : null;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* HTTP 200 can still be a refusal: smoke and dry_run succeed without spending
|
|
183
|
+
* and attach run_blockers or readiness.blockers. Only those top-level buckets
|
|
184
|
+
* count. A row skip_reason is not a refusal of the command.
|
|
185
|
+
*/
|
|
186
|
+
function readinessRefusal(value) {
|
|
187
|
+
const root = readinessRecord(value);
|
|
188
|
+
const data = readinessRecord(root?.data) ?? root;
|
|
189
|
+
if (!data)
|
|
190
|
+
return null;
|
|
191
|
+
const run = readinessRecord(data.run);
|
|
192
|
+
const outputs = readinessRecord(run?.outputs);
|
|
193
|
+
const buckets = [];
|
|
194
|
+
const states = [];
|
|
195
|
+
const take = (readiness) => {
|
|
196
|
+
if (!readiness)
|
|
197
|
+
return;
|
|
198
|
+
if (Array.isArray(readiness.blockers))
|
|
199
|
+
buckets.push(...readiness.blockers);
|
|
200
|
+
if (typeof readiness.state === 'string')
|
|
201
|
+
states.push(readiness.state);
|
|
202
|
+
};
|
|
203
|
+
// Dry runs put readiness on run.outputs. Durable and polled runs put the
|
|
204
|
+
// same object on run.steps[].outputs. A blocked rehearsal is an array of
|
|
205
|
+
// { status:'blocked', blocked_by, next_action }, not { blockers, state }.
|
|
206
|
+
const absorbSurface = (surface) => {
|
|
207
|
+
if (!surface)
|
|
208
|
+
return;
|
|
209
|
+
if (Array.isArray(surface.run_blockers))
|
|
210
|
+
buckets.push(...surface.run_blockers);
|
|
211
|
+
absorbReadiness(surface.readiness);
|
|
212
|
+
};
|
|
213
|
+
const absorbReadiness = (readiness) => {
|
|
214
|
+
if (Array.isArray(readiness)) {
|
|
215
|
+
for (const entry of readiness) {
|
|
216
|
+
const row = readinessRecord(entry);
|
|
217
|
+
if (!row || row.status !== 'blocked')
|
|
218
|
+
continue;
|
|
219
|
+
const blockedBy = Array.isArray(row.blocked_by)
|
|
220
|
+
? row.blocked_by.filter((item) => typeof item === 'string' && /^[a-z][a-z0-9_]*$/.test(item))
|
|
221
|
+
: [];
|
|
222
|
+
const nextAction = typeof row.next_action === 'string' && row.next_action.trim() ? row.next_action.trim() : null;
|
|
223
|
+
const message = typeof row.message === 'string' && row.message.trim() ? row.message.trim() : null;
|
|
224
|
+
buckets.push({
|
|
225
|
+
...(blockedBy[0] ? { code: blockedBy[0] } : {}),
|
|
226
|
+
message: nextAction ?? message ?? 'This run is blocked until the workspace is ready.',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
take(readinessRecord(readiness));
|
|
232
|
+
};
|
|
233
|
+
absorbSurface(outputs);
|
|
234
|
+
if (Array.isArray(run?.steps)) {
|
|
235
|
+
for (const step of run.steps)
|
|
236
|
+
absorbSurface(readinessRecord(readinessRecord(step)?.outputs));
|
|
237
|
+
}
|
|
238
|
+
absorbReadiness(run?.readiness);
|
|
239
|
+
absorbReadiness(data.readiness);
|
|
240
|
+
const codes = buckets.map(blockerCodeOf).filter((code) => Boolean(code));
|
|
241
|
+
const sentences = buckets.map(blockerSentence).filter((sentence) => Boolean(sentence));
|
|
242
|
+
const credentials = codes.includes('credentials_required') || states.includes('credentials_required');
|
|
243
|
+
if (!credentials && codes.length === 0 && sentences.length === 0)
|
|
244
|
+
return null;
|
|
245
|
+
const sentence = sentences[0] ?? (credentials ? CREDENTIALS_REQUIRED_SENTENCE : 'This run is blocked until the workspace is ready.');
|
|
246
|
+
const code = credentials ? 'credentials_required' : codes[0] ?? 'not_ready';
|
|
247
|
+
return new client_1.CliError(sentence, code, 409, { retryable: false });
|
|
186
248
|
}
|
|
187
249
|
/** Adds the idempotency key to the printed meta, so a user can see which key to re-run with. */
|
|
188
250
|
function withIdempotencyMeta(value, key) {
|
|
@@ -230,28 +292,38 @@ async function runGated(context, input) {
|
|
|
230
292
|
// and a quote is bound to the turn that asked for it. So a turn is reused
|
|
231
293
|
// only while a quote is pending, together with the quote its dry run
|
|
232
294
|
// returned, and forgotten once the paid run is admitted.
|
|
295
|
+
const explicitKey = parameterIdempotencyKey(body) ?? input.idempotencyKey;
|
|
233
296
|
const fingerprint = planFingerprint(body);
|
|
234
297
|
const cached = input.turnToken
|
|
235
298
|
? null
|
|
236
299
|
: await (0, turnCache_1.reusableTurn)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, fingerprint, Date.now(), warnTurnCache);
|
|
237
300
|
const pending = cached?.token ?? null;
|
|
238
301
|
const live = body.mode === 'live';
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
// --turn-token is the contract. Do not fetch tools get to invent one.
|
|
303
|
+
// A cached inline read also skips the fetch until registry_hash changes.
|
|
304
|
+
const cachedPosture = input.turnToken ? null : (0, registryManifest_1.lookupPosture)(capabilityId);
|
|
305
|
+
const inlineFromCache = !input.turnToken && !pending && !explicitKey && cachedPosture?.inlineRead === true;
|
|
306
|
+
if (!input.turnToken && !pending && !inlineFromCache)
|
|
241
307
|
await contract();
|
|
308
|
+
const posture = fetched ? (0, capabilityPosture_1.postureFromContract)(fetched) : cachedPosture;
|
|
309
|
+
if (fetched && posture)
|
|
310
|
+
(0, registryManifest_1.rememberPosture)(capabilityId, posture);
|
|
242
311
|
const refusal = fetched ? cliSurfaceRefusal(capabilityId, fetched) : null;
|
|
243
312
|
if (refusal)
|
|
244
313
|
throw refusal;
|
|
245
|
-
|
|
314
|
+
const inlineRead = !input.turnToken && !pending && !explicitKey && posture?.inlineRead === true;
|
|
315
|
+
let turnToken = input.turnToken ?? pending ?? (inlineRead ? undefined : optionalToolTurnToken(fetched));
|
|
246
316
|
// Only a quote the caller named or a dry run of this exact input returned.
|
|
247
317
|
// The server's refusal also shows a price, but that one is information, not consent.
|
|
248
|
-
|
|
318
|
+
const explicitQuote = typeof body.quote_hash === 'string';
|
|
319
|
+
let quoteHash = explicitQuote
|
|
249
320
|
? body.quote_hash
|
|
250
321
|
: live ? cached?.quote_hash ?? undefined : undefined;
|
|
251
|
-
|
|
252
|
-
let
|
|
322
|
+
// null means the price was not stated (or the flag named a hash with no ceiling).
|
|
323
|
+
let quotedMaximum = !explicitQuote && cached?.quote_hash ? cached.maximum_credits : undefined;
|
|
253
324
|
let ranDryRun = false;
|
|
254
325
|
let sentKey = null;
|
|
326
|
+
const approvalRequired = () => new client_1.CliError('This run spends credits. Rerun the same command with --approved.', 'approval_required', 409, { retryable: false, repair: { kind: 'ask_user', hint: 'Rerun the same command with --approved.' } });
|
|
255
327
|
const requestBody = () => ({
|
|
256
328
|
...body,
|
|
257
329
|
...(turnToken ? { tool_turn_token: turnToken } : {}),
|
|
@@ -284,16 +356,27 @@ async function runGated(context, input) {
|
|
|
284
356
|
let result;
|
|
285
357
|
try {
|
|
286
358
|
try {
|
|
359
|
+
if (live && quoteHash) {
|
|
360
|
+
const maximum = explicitQuote ? null : quotedMaximum ?? null;
|
|
361
|
+
if (spendNeedsApproval(maximum, body.approved === true)) {
|
|
362
|
+
stderr.write(`${quotePriced(capabilityId, body, { quote_hash: quoteHash, maximum_credits: maximum })}\n`);
|
|
363
|
+
throw approvalRequired();
|
|
364
|
+
}
|
|
365
|
+
if (body.approved === true || maximum === 0) {
|
|
366
|
+
stderr.write(`${quotePriced(capabilityId, body, { quote_hash: quoteHash, maximum_credits: maximum })}\nRunning live on that quote.\n`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
287
369
|
result = await run();
|
|
288
370
|
}
|
|
289
371
|
catch (error) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (inlineRead && !recoveredInlineRead && code(error) === 'capability_not_fetched') {
|
|
294
|
-
recoveredInlineRead = true;
|
|
372
|
+
if (!input.turnToken && code(error) === 'capability_not_fetched') {
|
|
373
|
+
// Capability posture can change between tools get and run. Refresh it
|
|
374
|
+
// once rather than maintaining a client-side list of token-free reads.
|
|
295
375
|
fetched = await client.getTool(capabilityId);
|
|
296
|
-
|
|
376
|
+
const refreshed = (0, capabilityPosture_1.postureFromContract)(fetched);
|
|
377
|
+
// The server just required a turn token, so this id is not an inline read.
|
|
378
|
+
(0, registryManifest_1.rememberPosture)(capabilityId, { ...refreshed, inlineRead: false });
|
|
379
|
+
turnToken = optionalToolTurnToken(fetched);
|
|
297
380
|
result = await run();
|
|
298
381
|
}
|
|
299
382
|
else if (live && code(error) === 'quote_required' && !quoteHash && turnToken) {
|
|
@@ -318,8 +401,12 @@ async function runGated(context, input) {
|
|
|
318
401
|
}
|
|
319
402
|
ranDryRun = true;
|
|
320
403
|
quoteHash = quote.quote_hash;
|
|
321
|
-
|
|
322
|
-
|
|
404
|
+
quotedMaximum = quote.maximum_credits;
|
|
405
|
+
stderr.write(`${quotePriced(capabilityId, body, quote)}\n`);
|
|
406
|
+
await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, { quote_hash: quote.quote_hash, fingerprint, maximum_credits: quote.maximum_credits });
|
|
407
|
+
if (spendNeedsApproval(quote.maximum_credits, body.approved === true))
|
|
408
|
+
throw approvalRequired();
|
|
409
|
+
stderr.write('Running live on that quote.\n');
|
|
323
410
|
result = await run();
|
|
324
411
|
}
|
|
325
412
|
else {
|
|
@@ -334,7 +421,7 @@ async function runGated(context, input) {
|
|
|
334
421
|
if (code(error) === 'approval_required') {
|
|
335
422
|
stderr.write('This run needs a person\'s approval. Show them the rows and cost above; when they agree, rerun the same command with --approved.\n');
|
|
336
423
|
}
|
|
337
|
-
await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quoteHash ? { quote_hash: quoteHash, fingerprint } : undefined);
|
|
424
|
+
await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quoteHash ? { quote_hash: quoteHash, fingerprint, ...(quotedMaximum !== undefined ? { maximum_credits: quotedMaximum } : {}) } : undefined);
|
|
338
425
|
}
|
|
339
426
|
// Only an unknown or transient result may resume an attempt. A server
|
|
340
427
|
// refusal with retryable:false is terminal and must receive a new key.
|
|
@@ -346,23 +433,33 @@ async function runGated(context, input) {
|
|
|
346
433
|
if (!input.turnToken) {
|
|
347
434
|
if (body.mode === 'dry_run' && turnToken) {
|
|
348
435
|
const quote = quoteFromResult(result);
|
|
349
|
-
await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quote ? { quote_hash: quote.quote_hash, fingerprint } : undefined);
|
|
436
|
+
await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quote ? { quote_hash: quote.quote_hash, fingerprint, maximum_credits: quote.maximum_credits } : undefined);
|
|
350
437
|
}
|
|
351
|
-
else if (pending || ranDryRun)
|
|
352
|
-
await (0, turnCache_1.forgetTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, warnTurnCache);
|
|
353
438
|
}
|
|
354
439
|
let timedOut = false;
|
|
355
|
-
if (!input.noWait && !
|
|
440
|
+
if (!input.noWait && !posture?.returnsRunId) {
|
|
356
441
|
const waited = await (0, runWait_1.waitForRun)(client, result, input.waitTimeout, context.sleep);
|
|
357
442
|
result = waited.value;
|
|
358
443
|
timedOut = waited.timedOut;
|
|
359
444
|
}
|
|
445
|
+
// A timeout still owns the quote and the idempotency key. Forgetting the
|
|
446
|
+
// turn here made the rerun hash a body with no quote_hash and start a second run.
|
|
447
|
+
if (!input.turnToken && body.mode !== 'dry_run' && (pending || ranDryRun) && !timedOut) {
|
|
448
|
+
await (0, turnCache_1.forgetTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, warnTurnCache);
|
|
449
|
+
}
|
|
360
450
|
// A run still active after waiting has unknown outcome; retain its key for
|
|
361
451
|
// the read-back/retry rule. A terminal response completes this attempt.
|
|
362
452
|
if (attemptIdentity && !timedOut)
|
|
363
453
|
await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
|
|
454
|
+
const blocked = readinessRefusal(result);
|
|
455
|
+
if (blocked)
|
|
456
|
+
throw blocked;
|
|
364
457
|
return { value: withIdempotencyMeta(result, explicitKey ? null : sentKey), timedOut, idempotencyKey: sentKey, turnToken: turnToken ?? null };
|
|
365
458
|
}
|
|
459
|
+
/** Blocker refusal for a run envelope that did not go through runGated (runs wait). */
|
|
460
|
+
function refusalFromRunResult(value) {
|
|
461
|
+
return readinessRefusal(value);
|
|
462
|
+
}
|
|
366
463
|
/** A read the CLI needs before it can form a write, run through the same gate as any other run. */
|
|
367
464
|
async function readCapability(context, capabilityId, params, waitTimeout) {
|
|
368
465
|
const outcome = await runGated(context, { capabilityId, body: { params }, waitTimeout });
|