@geometra/mcp 1.53.0 → 1.54.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.
|
@@ -1087,11 +1087,138 @@ describe('submit_form tool', () => {
|
|
|
1087
1087
|
expect(mockState.sendClick).toHaveBeenCalledTimes(1);
|
|
1088
1088
|
});
|
|
1089
1089
|
});
|
|
1090
|
+
describe('fill transparent fallback', () => {
|
|
1091
|
+
beforeEach(() => {
|
|
1092
|
+
vi.clearAllMocks();
|
|
1093
|
+
resetMockSessionCaches();
|
|
1094
|
+
});
|
|
1095
|
+
it('geometra_run_actions aggregates step-level fill fallback metadata into top-level fallbacks', async () => {
|
|
1096
|
+
const handler = getToolHandler('geometra_run_actions');
|
|
1097
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
1098
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
1099
|
+
children: [
|
|
1100
|
+
node('textbox', 'Full name', { value: '', path: [0] }),
|
|
1101
|
+
],
|
|
1102
|
+
});
|
|
1103
|
+
// Force the batched path to reject with a recoverable error so the
|
|
1104
|
+
// fill_fields step falls through to the sequential loop and tags the step.
|
|
1105
|
+
mockState.sendFillFields.mockRejectedValue(new Error('Unsupported client message type "fillFields"'));
|
|
1106
|
+
// includeSteps:false makes the fill_fields step handler prefer the batched
|
|
1107
|
+
// fast-path. When that path is unavailable, the step flips to sequential
|
|
1108
|
+
// and emits fallback metadata that run_actions lifts into the top-level
|
|
1109
|
+
// `fallbacks` array.
|
|
1110
|
+
const result = await handler({
|
|
1111
|
+
actions: [
|
|
1112
|
+
{
|
|
1113
|
+
type: 'fill_fields',
|
|
1114
|
+
fields: [{ kind: 'text', fieldLabel: 'Full name', value: 'Taylor Applicant' }],
|
|
1115
|
+
},
|
|
1116
|
+
],
|
|
1117
|
+
stopOnError: true,
|
|
1118
|
+
includeSteps: false,
|
|
1119
|
+
detail: 'minimal',
|
|
1120
|
+
});
|
|
1121
|
+
const payload = JSON.parse(result.content[0].text);
|
|
1122
|
+
expect(payload).toMatchObject({
|
|
1123
|
+
completed: true,
|
|
1124
|
+
stepCount: 1,
|
|
1125
|
+
successCount: 1,
|
|
1126
|
+
fallbacks: [
|
|
1127
|
+
{ stepIndex: 0, type: 'fill_fields', attempted: true, used: true, reason: 'batched-unavailable', attempts: 2 },
|
|
1128
|
+
],
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
it('geometra_fill_fields surfaces fallback metadata when the batched path is unavailable', async () => {
|
|
1132
|
+
const handler = getToolHandler('geometra_fill_fields');
|
|
1133
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
1134
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
1135
|
+
children: [
|
|
1136
|
+
node('textbox', 'Full name', { value: '', path: [0] }),
|
|
1137
|
+
],
|
|
1138
|
+
});
|
|
1139
|
+
// Force the batched path to throw a recoverable error so the handler
|
|
1140
|
+
// falls through to the sequential loop and tags the fallback.
|
|
1141
|
+
mockState.sendFillFields.mockRejectedValueOnce(new Error('Unsupported client message type "fillFields"'));
|
|
1142
|
+
const result = await handler({
|
|
1143
|
+
fields: [{ kind: 'text', fieldLabel: 'Full name', value: 'Taylor Applicant' }],
|
|
1144
|
+
stopOnError: true,
|
|
1145
|
+
failOnInvalid: false,
|
|
1146
|
+
includeSteps: false,
|
|
1147
|
+
detail: 'minimal',
|
|
1148
|
+
});
|
|
1149
|
+
const payload = JSON.parse(result.content[0].text);
|
|
1150
|
+
expect(payload).toMatchObject({
|
|
1151
|
+
completed: true,
|
|
1152
|
+
fieldCount: 1,
|
|
1153
|
+
successCount: 1,
|
|
1154
|
+
errorCount: 0,
|
|
1155
|
+
fallback: { attempted: true, used: true, reason: 'batched-unavailable', attempts: 2 },
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
it('geometra_fill_form surfaces fallback metadata when batched throws recoverable error', async () => {
|
|
1159
|
+
const handler = getToolHandler('geometra_fill_form');
|
|
1160
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
1161
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
1162
|
+
children: [
|
|
1163
|
+
node('textbox', 'Full name', { value: '', path: [0] }),
|
|
1164
|
+
],
|
|
1165
|
+
});
|
|
1166
|
+
mockState.formSchemas = [{
|
|
1167
|
+
formId: 'fm:0',
|
|
1168
|
+
name: 'Application',
|
|
1169
|
+
fieldCount: 1,
|
|
1170
|
+
requiredCount: 1,
|
|
1171
|
+
invalidCount: 1,
|
|
1172
|
+
fields: [
|
|
1173
|
+
{ id: 'ff:0.0', kind: 'text', label: 'Full name' },
|
|
1174
|
+
],
|
|
1175
|
+
}];
|
|
1176
|
+
// Both the batched-direct path and the schema-backed batched path call
|
|
1177
|
+
// sendFillFields. Reject all calls so both hit the recoverable-error
|
|
1178
|
+
// branch and the handler lands in the sequential loop.
|
|
1179
|
+
mockState.sendFillFields.mockRejectedValue(new Error('Unsupported client message type "fillFields"'));
|
|
1180
|
+
const result = await handler({
|
|
1181
|
+
valuesByLabel: { 'Full name': 'Taylor Applicant' },
|
|
1182
|
+
includeSteps: false,
|
|
1183
|
+
detail: 'minimal',
|
|
1184
|
+
});
|
|
1185
|
+
const payload = JSON.parse(result.content[0].text);
|
|
1186
|
+
expect(payload).toMatchObject({
|
|
1187
|
+
execution: 'sequential',
|
|
1188
|
+
fallback: { attempted: true, used: true, reason: 'batched-threw', attempts: 2 },
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
});
|
|
1090
1192
|
describe('click transparent fallback', () => {
|
|
1091
1193
|
beforeEach(() => {
|
|
1092
1194
|
vi.clearAllMocks();
|
|
1093
1195
|
resetMockSessionCaches();
|
|
1094
1196
|
});
|
|
1197
|
+
it('surfaces fallback.attempted:false when click fallback attempted and failed', async () => {
|
|
1198
|
+
const handler = getToolHandler('geometra_click');
|
|
1199
|
+
mockState.currentA11yRoot = node('group', undefined, {
|
|
1200
|
+
bounds: { x: 0, y: 0, width: 1280, height: 800 },
|
|
1201
|
+
meta: { pageUrl: 'https://jobs.example.com/application', scrollX: 0, scrollY: 0 },
|
|
1202
|
+
children: [],
|
|
1203
|
+
});
|
|
1204
|
+
const result = await handler({
|
|
1205
|
+
role: 'button',
|
|
1206
|
+
name: 'Does not exist',
|
|
1207
|
+
fullyVisible: true,
|
|
1208
|
+
maxRevealSteps: 1,
|
|
1209
|
+
revealTimeoutMs: 50,
|
|
1210
|
+
detail: 'terse',
|
|
1211
|
+
});
|
|
1212
|
+
// Fallback was attempted (both revision-retry — if mockWaitForUiCondition
|
|
1213
|
+
// returns true — and relaxed-visibility) but neither phase recovered the
|
|
1214
|
+
// missing target, so the handler returns a structured error carrying the
|
|
1215
|
+
// attempted-but-failed telemetry.
|
|
1216
|
+
const errorText = result.content[0].text;
|
|
1217
|
+
expect(errorText).toContain('"fallback"');
|
|
1218
|
+
const parsed = JSON.parse(errorText);
|
|
1219
|
+
expect(parsed.fallback).toMatchObject({ attempted: true, used: false });
|
|
1220
|
+
expect(parsed.fallback.reasonsTried.length).toBeGreaterThan(0);
|
|
1221
|
+
});
|
|
1095
1222
|
it('surfaces fallback.used when relaxed-visibility lets an offscreen submit resolve', async () => {
|
|
1096
1223
|
const handler = getToolHandler('geometra_click');
|
|
1097
1224
|
// First tree: target exists but is offscreen below the viewport, so a
|
|
@@ -1124,7 +1251,7 @@ describe('click transparent fallback', () => {
|
|
|
1124
1251
|
const payload = JSON.parse(result.content[0].text);
|
|
1125
1252
|
expect(payload).toMatchObject({
|
|
1126
1253
|
target: { role: 'button', name: 'Submit' },
|
|
1127
|
-
fallback: { used: true, reason: 'relaxed-visibility' },
|
|
1254
|
+
fallback: { attempted: true, used: true, reason: 'relaxed-visibility' },
|
|
1128
1255
|
});
|
|
1129
1256
|
});
|
|
1130
1257
|
});
|
package/dist/server.js
CHANGED
|
@@ -330,7 +330,7 @@ const batchActionSchema = z.discriminatedUnion('type', [
|
|
|
330
330
|
}),
|
|
331
331
|
]);
|
|
332
332
|
export function createServer() {
|
|
333
|
-
const server = new McpServer({ name: 'geometra', version: '1.19.
|
|
333
|
+
const server = new McpServer({ name: 'geometra', version: '1.19.23' }, { capabilities: { tools: {} } });
|
|
334
334
|
const sessionIdInput = z.string().optional().describe('Session identifier returned by geometra_connect. Omit to use the most recent session.');
|
|
335
335
|
// ── connect ──────────────────────────────────────────────────
|
|
336
336
|
server.tool('geometra_connect', `Connect to a Geometra WebSocket peer, or start \`geometra-proxy\` automatically for a normal web page.
|
|
@@ -774,6 +774,7 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
774
774
|
const resolvedFields = resolveFillFieldInputs(session, fields);
|
|
775
775
|
if (!resolvedFields.ok)
|
|
776
776
|
return err(resolvedFields.error);
|
|
777
|
+
let fallbackFromBatch;
|
|
777
778
|
if (!includeSteps) {
|
|
778
779
|
try {
|
|
779
780
|
const batched = await tryBatchedResolvedFields(session, resolvedFields.fields, detail);
|
|
@@ -792,6 +793,10 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
792
793
|
}
|
|
793
794
|
return ok(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
794
795
|
}
|
|
796
|
+
// Batched path returned {ok: false} — this is the transparent fallback
|
|
797
|
+
// case. Continue into the sequential loop and mark the result so
|
|
798
|
+
// operators can aggregate how often it fires.
|
|
799
|
+
fallbackFromBatch = { attempted: true, used: true, reason: 'batched-unavailable', attempts: 2 };
|
|
795
800
|
}
|
|
796
801
|
catch (e) {
|
|
797
802
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -847,6 +852,7 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
847
852
|
errorCount,
|
|
848
853
|
...(includeSteps ? { steps } : {}),
|
|
849
854
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
855
|
+
...(fallbackFromBatch ? { fallback: fallbackFromBatch } : {}),
|
|
850
856
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
851
857
|
};
|
|
852
858
|
if (failOnInvalid && invalidRemaining > 0) {
|
|
@@ -1017,6 +1023,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1017
1023
|
planned.fields = planned.planned.map(p => p.field);
|
|
1018
1024
|
}
|
|
1019
1025
|
}
|
|
1026
|
+
let fallbackFromBatch;
|
|
1020
1027
|
if (!includeSteps) {
|
|
1021
1028
|
let usedBatch = false;
|
|
1022
1029
|
let batchAckResult;
|
|
@@ -1050,6 +1057,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1050
1057
|
const message = e instanceof Error ? e.message : String(e);
|
|
1051
1058
|
return err(message);
|
|
1052
1059
|
}
|
|
1060
|
+
fallbackFromBatch = { attempted: true, used: true, reason: 'batched-threw', attempts: 2 };
|
|
1053
1061
|
}
|
|
1054
1062
|
if (usedBatch) {
|
|
1055
1063
|
const after = sessionA11y(session);
|
|
@@ -1057,6 +1065,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1057
1065
|
const invalidRemaining = signals?.invalidFields.length ?? 0;
|
|
1058
1066
|
if ((!batchAckResult || batchAckResult.invalidCount > 0) && invalidRemaining > 0) {
|
|
1059
1067
|
usedBatch = false;
|
|
1068
|
+
fallbackFromBatch = { attempted: true, used: true, reason: 'batched-invalid-readback', attempts: 2 };
|
|
1060
1069
|
}
|
|
1061
1070
|
}
|
|
1062
1071
|
if (usedBatch) {
|
|
@@ -1131,6 +1140,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
1131
1140
|
...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
|
|
1132
1141
|
...(includeSteps ? { steps } : {}),
|
|
1133
1142
|
...(stoppedAt !== undefined ? { stoppedAt, resumeFromIndex: stoppedAt + 1 } : {}),
|
|
1143
|
+
...(fallbackFromBatch ? { fallback: fallbackFromBatch } : {}),
|
|
1134
1144
|
...(verification ? { verification } : {}),
|
|
1135
1145
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
1136
1146
|
};
|
|
@@ -1359,6 +1369,10 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1359
1369
|
const steps = [];
|
|
1360
1370
|
let stoppedAt;
|
|
1361
1371
|
const batchStartedAt = performance.now();
|
|
1372
|
+
// Collect transparent-fallback signals from each step so run_actions
|
|
1373
|
+
// surfaces them at top level regardless of `includeSteps` — otherwise
|
|
1374
|
+
// the telemetry is dead code when callers opt out of the steps listing.
|
|
1375
|
+
const fallbackRecords = [];
|
|
1362
1376
|
for (let index = 0; index < actions.length; index++) {
|
|
1363
1377
|
const action = actions[index];
|
|
1364
1378
|
const startedAt = performance.now();
|
|
@@ -1370,6 +1384,17 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1370
1384
|
uiTreeWaitMs = performance.now() - uiTreeWaitStartedAt;
|
|
1371
1385
|
}
|
|
1372
1386
|
const result = await executeBatchAction(session, action, detail, includeSteps);
|
|
1387
|
+
const stepFallback = result.compact.fallback;
|
|
1388
|
+
if (stepFallback?.used) {
|
|
1389
|
+
fallbackRecords.push({
|
|
1390
|
+
stepIndex: index,
|
|
1391
|
+
type: action.type,
|
|
1392
|
+
attempted: true,
|
|
1393
|
+
used: true,
|
|
1394
|
+
reason: stepFallback.reason,
|
|
1395
|
+
attempts: stepFallback.attempts,
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1373
1398
|
const elapsedMs = Number((performance.now() - startedAt).toFixed(1));
|
|
1374
1399
|
const cumulativeMs = Number((performance.now() - batchStartedAt).toFixed(1));
|
|
1375
1400
|
const stepSignals = includeSteps ? (() => {
|
|
@@ -1428,6 +1453,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1428
1453
|
...connection,
|
|
1429
1454
|
completed: stoppedAt === undefined && steps.length === actions.length,
|
|
1430
1455
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
1456
|
+
...(fallbackRecords.length > 0 ? { fallbacks: fallbackRecords } : {}),
|
|
1431
1457
|
...(after ? { final: sessionSignalsPayload(collectSessionSignals(after), detail) } : {}),
|
|
1432
1458
|
}
|
|
1433
1459
|
: {
|
|
@@ -1438,6 +1464,7 @@ Supported step types: \`click\`, \`type\`, \`key\`, \`upload_files\`, \`pick_lis
|
|
|
1438
1464
|
errorCount,
|
|
1439
1465
|
...(includeSteps ? { steps } : {}),
|
|
1440
1466
|
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
1467
|
+
...(fallbackRecords.length > 0 ? { fallbacks: fallbackRecords } : {}),
|
|
1441
1468
|
...(after ? { final: sessionSignalsPayload(collectSessionSignals(after), detail) } : {}),
|
|
1442
1469
|
};
|
|
1443
1470
|
return ok(JSON.stringify(payload, null, detail === 'verbose' ? 2 : undefined));
|
|
@@ -1694,7 +1721,7 @@ After clicking, returns a compact semantic delta when possible (dialogs/forms/li
|
|
|
1694
1721
|
revealTimeoutMs,
|
|
1695
1722
|
});
|
|
1696
1723
|
if (!resolved.ok)
|
|
1697
|
-
return err(resolved
|
|
1724
|
+
return err(clickFallbackErrorMessage(resolved));
|
|
1698
1725
|
const wait = await sendClick(session, resolved.value.x, resolved.value.y, timeoutMs);
|
|
1699
1726
|
const summary = postActionSummary(session, before, wait, detail);
|
|
1700
1727
|
const clickLine = !resolved.value.target
|
|
@@ -2988,22 +3015,25 @@ async function resolveClickLocationWithFallback(session, options) {
|
|
|
2988
3015
|
return first;
|
|
2989
3016
|
if (!hasNodeFilter(options.filter))
|
|
2990
3017
|
return first;
|
|
3018
|
+
const reasonsTried = [];
|
|
2991
3019
|
let attempts = 1;
|
|
2992
3020
|
const startRevision = session.updateRevision;
|
|
2993
3021
|
const revisionAdvanced = await waitForUiCondition(session, () => session.updateRevision > startRevision, 600);
|
|
2994
3022
|
if (revisionAdvanced) {
|
|
2995
3023
|
attempts += 1;
|
|
3024
|
+
reasonsTried.push('revision-retry');
|
|
2996
3025
|
const retry = await resolveClickLocation(session, options);
|
|
2997
3026
|
if (retry.ok) {
|
|
2998
3027
|
return {
|
|
2999
3028
|
ok: true,
|
|
3000
3029
|
value: retry.value,
|
|
3001
|
-
fallback: { used: true, reason: 'revision-retry', attempts },
|
|
3030
|
+
fallback: { attempted: true, used: true, reason: 'revision-retry', attempts },
|
|
3002
3031
|
};
|
|
3003
3032
|
}
|
|
3004
3033
|
}
|
|
3005
3034
|
if (options.fullyVisible !== false) {
|
|
3006
3035
|
attempts += 1;
|
|
3036
|
+
reasonsTried.push('relaxed-visibility');
|
|
3007
3037
|
const relaxed = await resolveClickLocation(session, {
|
|
3008
3038
|
...options,
|
|
3009
3039
|
fullyVisible: false,
|
|
@@ -3013,11 +3043,32 @@ async function resolveClickLocationWithFallback(session, options) {
|
|
|
3013
3043
|
return {
|
|
3014
3044
|
ok: true,
|
|
3015
3045
|
value: relaxed.value,
|
|
3016
|
-
fallback: { used: true, reason: 'relaxed-visibility', attempts },
|
|
3046
|
+
fallback: { attempted: true, used: true, reason: 'relaxed-visibility', attempts },
|
|
3017
3047
|
};
|
|
3018
3048
|
}
|
|
3019
3049
|
}
|
|
3020
|
-
|
|
3050
|
+
// All fallback phases tried and none recovered. Carry the trace of what we
|
|
3051
|
+
// tried so operators see the attempted-but-failed signal alongside the
|
|
3052
|
+
// successful-recovery signal.
|
|
3053
|
+
if (reasonsTried.length === 0)
|
|
3054
|
+
return first;
|
|
3055
|
+
return {
|
|
3056
|
+
ok: false,
|
|
3057
|
+
error: first.error,
|
|
3058
|
+
fallback: { attempted: true, used: false, reasonsTried, attempts },
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
/**
|
|
3062
|
+
* Expose the attempted-but-failed fallback telemetry on error responses. When
|
|
3063
|
+
* `resolved.fallback` is present on a failure, return a structured JSON error
|
|
3064
|
+
* so operators can aggregate recovery failures the same way they aggregate
|
|
3065
|
+
* successful recoveries. Plain-text errors are preserved for the no-fallback
|
|
3066
|
+
* case to avoid churning that error contract.
|
|
3067
|
+
*/
|
|
3068
|
+
function clickFallbackErrorMessage(resolved) {
|
|
3069
|
+
if (!resolved.fallback)
|
|
3070
|
+
return resolved.error;
|
|
3071
|
+
return JSON.stringify({ error: resolved.error, fallback: resolved.fallback });
|
|
3021
3072
|
}
|
|
3022
3073
|
function describeFormattedNode(node) {
|
|
3023
3074
|
return `${node.role}${node.name ? ` ${JSON.stringify(node.name)}` : ''} (${node.id})`;
|
|
@@ -3516,7 +3567,7 @@ async function executeBatchAction(session, action, detail, includeSteps) {
|
|
|
3516
3567
|
revealTimeoutMs: action.revealTimeoutMs,
|
|
3517
3568
|
});
|
|
3518
3569
|
if (!resolved.ok)
|
|
3519
|
-
throw new Error(resolved
|
|
3570
|
+
throw new Error(clickFallbackErrorMessage(resolved));
|
|
3520
3571
|
const wait = await sendClick(session, resolved.value.x, resolved.value.y, action.timeoutMs);
|
|
3521
3572
|
const targetSummary = resolved.value.target
|
|
3522
3573
|
? `Clicked ${describeFormattedNode(resolved.value.target)} at (${resolved.value.x}, ${resolved.value.y}).`
|
|
@@ -3762,6 +3813,7 @@ async function executeBatchAction(session, action, detail, includeSteps) {
|
|
|
3762
3813
|
const verifyFillsFn = action.verifyFills
|
|
3763
3814
|
? () => verifyFormFills(session, resolvedFields.fields.map(field => ({ field, confidence: 1.0, matchMethod: 'label-exact' })))
|
|
3764
3815
|
: undefined;
|
|
3816
|
+
let fallbackFromBatch;
|
|
3765
3817
|
if (!includeSteps) {
|
|
3766
3818
|
const batched = await tryBatchedResolvedFields(session, resolvedFields.fields, detail);
|
|
3767
3819
|
if (batched.ok) {
|
|
@@ -3777,6 +3829,10 @@ async function executeBatchAction(session, action, detail, includeSteps) {
|
|
|
3777
3829
|
},
|
|
3778
3830
|
};
|
|
3779
3831
|
}
|
|
3832
|
+
// Batched path unavailable — fall through into sequential and tag the
|
|
3833
|
+
// step so `geometra_run_actions` result aggregation matches the shape
|
|
3834
|
+
// emitted by standalone `geometra_fill_fields` / `geometra_fill_form`.
|
|
3835
|
+
fallbackFromBatch = { attempted: true, used: true, reason: 'batched-unavailable', attempts: 2 };
|
|
3780
3836
|
}
|
|
3781
3837
|
const steps = [];
|
|
3782
3838
|
for (let index = 0; index < resolvedFields.fields.length; index++) {
|
|
@@ -3805,6 +3861,7 @@ async function executeBatchAction(session, action, detail, includeSteps) {
|
|
|
3805
3861
|
compact: {
|
|
3806
3862
|
fieldCount: resolvedFields.fields.length,
|
|
3807
3863
|
...(includeSteps ? { steps } : {}),
|
|
3864
|
+
...(fallbackFromBatch ? { fallback: fallbackFromBatch } : {}),
|
|
3808
3865
|
...(verification ? { verification } : {}),
|
|
3809
3866
|
},
|
|
3810
3867
|
};
|
package/package.json
CHANGED