@lotics/app-sdk 0.120.2 → 0.121.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/AGENTS.md +1 -1
- package/dist/activity.d.ts +15 -2
- package/dist/{chunk-NZXJENJ2.js → chunk-MK4DBY6R.js} +653 -20
- package/dist/index.d.ts +2 -2
- package/dist/index.js +139 -50
- package/dist/router.js +1 -1
- package/dist/rpc.d.ts +1 -1
- package/dist/shared_types.d.ts +2 -1
- package/dist/store.d.ts +2 -0
- package/dist/viewer.d.ts +2 -0
- package/docs/members_and_options.md +21 -5
- package/docs/runtime.md +1 -0
- package/docs/security.md +1 -0
- package/docs/workflows.md +3 -3
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,35 +3,44 @@ import {
|
|
|
3
3
|
AGGREGATE_SUM_COLUMN,
|
|
4
4
|
DEFAULT_IMAGE_FIDELITY,
|
|
5
5
|
__export,
|
|
6
|
+
addDecimal,
|
|
6
7
|
aggregateRows,
|
|
7
8
|
anyRecordingLive,
|
|
8
9
|
applyPatch,
|
|
9
|
-
assertNever,
|
|
10
10
|
bandKey,
|
|
11
11
|
bandOf,
|
|
12
|
+
compareDecimal,
|
|
12
13
|
decodeAll,
|
|
14
|
+
divideDecimal,
|
|
13
15
|
encodePatch,
|
|
16
|
+
fixedDecimal,
|
|
14
17
|
getUrlParams,
|
|
15
18
|
isEmbedded,
|
|
19
|
+
moduloDecimal,
|
|
20
|
+
multiplyDecimal,
|
|
21
|
+
multiplyExactly,
|
|
16
22
|
paramsEqual,
|
|
17
23
|
peekUrlParams,
|
|
24
|
+
percentOf,
|
|
18
25
|
postHostNotification,
|
|
26
|
+
powerDecimal,
|
|
19
27
|
readRecordingSnapshot,
|
|
20
28
|
recordingStateOf,
|
|
21
29
|
recordingsVersion,
|
|
22
30
|
replaceRecordings,
|
|
31
|
+
roundDecimal,
|
|
23
32
|
rpc,
|
|
24
33
|
rpcAgentRun,
|
|
25
34
|
rpcAgentRunContinue,
|
|
26
|
-
sample,
|
|
27
35
|
setUrlParams,
|
|
28
36
|
subscribeQueriesChanged,
|
|
29
37
|
subscribeRecordings,
|
|
30
38
|
subscribeUrlParams,
|
|
39
|
+
subtractDecimal,
|
|
31
40
|
sumDecimal,
|
|
32
41
|
tiersAscend,
|
|
33
42
|
urlParam
|
|
34
|
-
} from "./chunk-
|
|
43
|
+
} from "./chunk-MK4DBY6R.js";
|
|
35
44
|
|
|
36
45
|
// src/mount.tsx
|
|
37
46
|
import { createRoot } from "react-dom/client";
|
|
@@ -602,6 +611,14 @@ function readNow(aliases) {
|
|
|
602
611
|
function refetchReads(aliases) {
|
|
603
612
|
for (const slot of readNow(aliases)) void ask(slot, true);
|
|
604
613
|
}
|
|
614
|
+
async function readsAnswered(aliases) {
|
|
615
|
+
await Promise.all(
|
|
616
|
+
readNow(aliases).map(async (slot) => {
|
|
617
|
+
await ask(slot, true);
|
|
618
|
+
while (slot.inflight !== null) await slot.inflight;
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
}
|
|
605
622
|
function askInOnePaint(read2) {
|
|
606
623
|
if (read2.length === 0) return;
|
|
607
624
|
const paint = { waiting: new Set(read2), held: [], drawn: false, cap: setTimeout(() => draw(paint), PAINT_CAP_MS) };
|
|
@@ -1729,6 +1746,15 @@ var customRandom = (alphabet, defaultSize, getRandom) => {
|
|
|
1729
1746
|
};
|
|
1730
1747
|
var customAlphabet = (alphabet, size2 = 21) => customRandom(alphabet, size2 | 0, random);
|
|
1731
1748
|
|
|
1749
|
+
// ../shared/src/utils.ts
|
|
1750
|
+
function sample(arr) {
|
|
1751
|
+
if (arr.length === 0) return void 0;
|
|
1752
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
1753
|
+
}
|
|
1754
|
+
function assertNever(value) {
|
|
1755
|
+
throw new Error(`Unhandled union member: ${JSON.stringify(value)}`);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1732
1758
|
// ../shared/src/expression_array.ts
|
|
1733
1759
|
var expression_array_exports = {};
|
|
1734
1760
|
__export(expression_array_exports, {
|
|
@@ -2782,7 +2808,7 @@ function formatNumber(value, decimals) {
|
|
|
2782
2808
|
throw new Error("formatNumber: expected number, got " + typeof value);
|
|
2783
2809
|
}
|
|
2784
2810
|
assertDecimals(decimals, "formatNumber");
|
|
2785
|
-
return value
|
|
2811
|
+
return fixedDecimal(value, decimals);
|
|
2786
2812
|
}
|
|
2787
2813
|
function formatDecimal(value, decimals, locale) {
|
|
2788
2814
|
if (value == null) return "";
|
|
@@ -2902,9 +2928,7 @@ function round(value, precision) {
|
|
|
2902
2928
|
if (precision != null && typeof precision !== "number") {
|
|
2903
2929
|
throw new Error("round: precision must be a number");
|
|
2904
2930
|
}
|
|
2905
|
-
|
|
2906
|
-
const multiplier = Math.pow(10, prec);
|
|
2907
|
-
return Math.round(value * multiplier) / multiplier;
|
|
2931
|
+
return roundDecimal(value, precision ?? 0);
|
|
2908
2932
|
}
|
|
2909
2933
|
function sum(arr) {
|
|
2910
2934
|
if (arr == null) return 0;
|
|
@@ -2970,12 +2994,8 @@ function percentage(part, total, decimals) {
|
|
|
2970
2994
|
if (decimals != null && typeof decimals !== "number") {
|
|
2971
2995
|
throw new Error("percentage: decimals must be a number");
|
|
2972
2996
|
}
|
|
2973
|
-
const percent = part
|
|
2974
|
-
|
|
2975
|
-
const multiplier = Math.pow(10, decimals);
|
|
2976
|
-
return Math.round(percent * multiplier) / multiplier;
|
|
2977
|
-
}
|
|
2978
|
-
return percent;
|
|
2997
|
+
const percent = divideDecimal(multiplyDecimal(part, 100), total);
|
|
2998
|
+
return decimals != null ? roundDecimal(percent, decimals) : percent;
|
|
2979
2999
|
}
|
|
2980
3000
|
function pow(base, exponent) {
|
|
2981
3001
|
if (base == null || exponent == null) return null;
|
|
@@ -2985,7 +3005,7 @@ function pow(base, exponent) {
|
|
|
2985
3005
|
if (typeof exponent !== "number") {
|
|
2986
3006
|
throw new Error("pow: exponent must be a number");
|
|
2987
3007
|
}
|
|
2988
|
-
return
|
|
3008
|
+
return powerDecimal(base, exponent);
|
|
2989
3009
|
}
|
|
2990
3010
|
function sqrt(n) {
|
|
2991
3011
|
if (n == null) return null;
|
|
@@ -3008,7 +3028,7 @@ function mod(n, divisor) {
|
|
|
3008
3028
|
if (divisor === 0) {
|
|
3009
3029
|
throw new Error("mod: divisor cannot be zero");
|
|
3010
3030
|
}
|
|
3011
|
-
return n
|
|
3031
|
+
return moduloDecimal(n, divisor);
|
|
3012
3032
|
}
|
|
3013
3033
|
|
|
3014
3034
|
// ../shared/src/expression_object.ts
|
|
@@ -5815,9 +5835,21 @@ function isNothing(value) {
|
|
|
5815
5835
|
return value === null || value === void 0;
|
|
5816
5836
|
}
|
|
5817
5837
|
var MULTIPLICATIVE_OPERATORS = /* @__PURE__ */ new Map([
|
|
5818
|
-
["*",
|
|
5819
|
-
["/",
|
|
5820
|
-
["%",
|
|
5838
|
+
["*", multiplyDecimal],
|
|
5839
|
+
["/", divideDecimal],
|
|
5840
|
+
["%", moduloDecimal]
|
|
5841
|
+
]);
|
|
5842
|
+
var COMPARISONS = /* @__PURE__ */ new Map([
|
|
5843
|
+
// eslint-disable-next-line eqeqeq
|
|
5844
|
+
["==", { ofNumbers: (order) => order === 0, ofOthers: (left, right) => left == right }],
|
|
5845
|
+
// eslint-disable-next-line eqeqeq
|
|
5846
|
+
["!=", { ofNumbers: (order) => order !== 0, ofOthers: (left, right) => left != right }],
|
|
5847
|
+
["===", { ofNumbers: (order) => order === 0, ofOthers: (left, right) => left === right }],
|
|
5848
|
+
["!==", { ofNumbers: (order) => order !== 0, ofOthers: (left, right) => left !== right }],
|
|
5849
|
+
["<", { ofNumbers: (order) => order < 0, ofOthers: (left, right) => left < right }],
|
|
5850
|
+
["<=", { ofNumbers: (order) => order <= 0, ofOthers: (left, right) => left <= right }],
|
|
5851
|
+
[">", { ofNumbers: (order) => order > 0, ofOthers: (left, right) => left > right }],
|
|
5852
|
+
[">=", { ofNumbers: (order) => order >= 0, ofOthers: (left, right) => left >= right }]
|
|
5821
5853
|
]);
|
|
5822
5854
|
var SafeEvalAstFactory = class extends EvalAstFactory {
|
|
5823
5855
|
id(name) {
|
|
@@ -5856,6 +5888,26 @@ var SafeEvalAstFactory = class extends EvalAstFactory {
|
|
|
5856
5888
|
}
|
|
5857
5889
|
};
|
|
5858
5890
|
}
|
|
5891
|
+
const comparison = COMPARISONS.get(op);
|
|
5892
|
+
if (comparison) {
|
|
5893
|
+
return {
|
|
5894
|
+
type: "Binary",
|
|
5895
|
+
operator: op,
|
|
5896
|
+
left,
|
|
5897
|
+
right,
|
|
5898
|
+
evaluate(scope) {
|
|
5899
|
+
const leftValue = this.left.evaluate(scope);
|
|
5900
|
+
const rightValue = this.right.evaluate(scope);
|
|
5901
|
+
if (typeof leftValue === "number" && typeof rightValue === "number") return comparison.ofNumbers(compareDecimal(leftValue, rightValue));
|
|
5902
|
+
return comparison.ofOthers(leftValue, rightValue);
|
|
5903
|
+
},
|
|
5904
|
+
getIds(idents) {
|
|
5905
|
+
this.left.getIds(idents);
|
|
5906
|
+
this.right.getIds(idents);
|
|
5907
|
+
return idents;
|
|
5908
|
+
}
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5859
5911
|
if (op !== "+" && op !== "-") {
|
|
5860
5912
|
return super.binary(left, op, right);
|
|
5861
5913
|
}
|
|
@@ -5871,7 +5923,8 @@ var SafeEvalAstFactory = class extends EvalAstFactory {
|
|
|
5871
5923
|
if (isNothing(leftValue) && isNothing(rightValue)) return null;
|
|
5872
5924
|
const leftOperand = leftValue ?? 0;
|
|
5873
5925
|
const rightOperand = rightValue ?? 0;
|
|
5874
|
-
|
|
5926
|
+
if (op === "-") return subtractDecimal(Number(leftOperand), Number(rightOperand));
|
|
5927
|
+
return typeof leftOperand === "number" && typeof rightOperand === "number" ? addDecimal(leftOperand, rightOperand) : leftOperand + rightOperand;
|
|
5875
5928
|
};
|
|
5876
5929
|
return op === "+" ? addOrConcat(leftValue, rightValue, combine) : combine();
|
|
5877
5930
|
},
|
|
@@ -21164,7 +21217,7 @@ function aggregateTextField(values2, operation) {
|
|
|
21164
21217
|
const emptyCount = values2.filter(
|
|
21165
21218
|
(value) => value === null || value === void 0 || value === ""
|
|
21166
21219
|
).length;
|
|
21167
|
-
const percentage2 =
|
|
21220
|
+
const percentage2 = percentOf(emptyCount, totalCount);
|
|
21168
21221
|
return { result: percentage2 };
|
|
21169
21222
|
}
|
|
21170
21223
|
case "percent_filled": {
|
|
@@ -21172,7 +21225,7 @@ function aggregateTextField(values2, operation) {
|
|
|
21172
21225
|
const filledCount = values2.filter(
|
|
21173
21226
|
(value) => value !== null && value !== void 0 && value !== ""
|
|
21174
21227
|
).length;
|
|
21175
|
-
const percentage2 =
|
|
21228
|
+
const percentage2 = percentOf(filledCount, totalCount);
|
|
21176
21229
|
return { result: percentage2 };
|
|
21177
21230
|
}
|
|
21178
21231
|
case "unique": {
|
|
@@ -21188,7 +21241,7 @@ function aggregateTextField(values2, operation) {
|
|
|
21188
21241
|
);
|
|
21189
21242
|
if (filledValues.length === 0) return { result: 0 };
|
|
21190
21243
|
const uniqueValues = new Set(filledValues);
|
|
21191
|
-
const percentage2 =
|
|
21244
|
+
const percentage2 = percentOf(uniqueValues.size, filledValues.length);
|
|
21192
21245
|
return { result: percentage2 };
|
|
21193
21246
|
}
|
|
21194
21247
|
default:
|
|
@@ -21212,13 +21265,13 @@ function aggregateNumberField(values2, operation) {
|
|
|
21212
21265
|
case "percent_empty": {
|
|
21213
21266
|
if (totalCount === 0) return { result: 0 };
|
|
21214
21267
|
const emptyCount = values2.filter((value) => !hasNumberValue(value)).length;
|
|
21215
|
-
const percentage2 =
|
|
21268
|
+
const percentage2 = percentOf(emptyCount, totalCount);
|
|
21216
21269
|
return { result: percentage2 };
|
|
21217
21270
|
}
|
|
21218
21271
|
case "percent_filled": {
|
|
21219
21272
|
if (totalCount === 0) return { result: 0 };
|
|
21220
21273
|
const filledCount = values2.filter(hasNumberValue).length;
|
|
21221
|
-
const percentage2 =
|
|
21274
|
+
const percentage2 = percentOf(filledCount, totalCount);
|
|
21222
21275
|
return { result: percentage2 };
|
|
21223
21276
|
}
|
|
21224
21277
|
case "unique": {
|
|
@@ -21230,7 +21283,7 @@ function aggregateNumberField(values2, operation) {
|
|
|
21230
21283
|
const filledValues = values2.filter(hasNumberValue);
|
|
21231
21284
|
if (filledValues.length === 0) return { result: 0 };
|
|
21232
21285
|
const uniqueValues = new Set(filledValues);
|
|
21233
|
-
const percentage2 =
|
|
21286
|
+
const percentage2 = percentOf(uniqueValues.size, filledValues.length);
|
|
21234
21287
|
return { result: percentage2 };
|
|
21235
21288
|
}
|
|
21236
21289
|
}
|
|
@@ -21288,13 +21341,13 @@ function aggregateBooleanField(values2, operation) {
|
|
|
21288
21341
|
case "percent_checked": {
|
|
21289
21342
|
if (totalCount === 0) return { result: 0 };
|
|
21290
21343
|
const checkedCount = values2.filter((value) => value === true).length;
|
|
21291
|
-
const percentage2 =
|
|
21344
|
+
const percentage2 = percentOf(checkedCount, totalCount);
|
|
21292
21345
|
return { result: percentage2 };
|
|
21293
21346
|
}
|
|
21294
21347
|
case "percent_unchecked": {
|
|
21295
21348
|
if (totalCount === 0) return { result: 0 };
|
|
21296
21349
|
const uncheckedCount = values2.filter((value) => !value).length;
|
|
21297
|
-
const percentage2 =
|
|
21350
|
+
const percentage2 = percentOf(uncheckedCount, totalCount);
|
|
21298
21351
|
return { result: percentage2 };
|
|
21299
21352
|
}
|
|
21300
21353
|
default:
|
|
@@ -21321,7 +21374,7 @@ function aggregateDateField(values2, operation) {
|
|
|
21321
21374
|
const emptyCount = values2.filter(
|
|
21322
21375
|
(value) => value === null || value === void 0 || value === ""
|
|
21323
21376
|
).length;
|
|
21324
|
-
const percentage2 =
|
|
21377
|
+
const percentage2 = percentOf(emptyCount, totalCount);
|
|
21325
21378
|
return { result: percentage2 };
|
|
21326
21379
|
}
|
|
21327
21380
|
case "percent_filled": {
|
|
@@ -21329,7 +21382,7 @@ function aggregateDateField(values2, operation) {
|
|
|
21329
21382
|
const filledCount = values2.filter(
|
|
21330
21383
|
(value) => value !== null && value !== void 0 && value !== ""
|
|
21331
21384
|
).length;
|
|
21332
|
-
const percentage2 =
|
|
21385
|
+
const percentage2 = percentOf(filledCount, totalCount);
|
|
21333
21386
|
return { result: percentage2 };
|
|
21334
21387
|
}
|
|
21335
21388
|
case "unique": {
|
|
@@ -21345,7 +21398,7 @@ function aggregateDateField(values2, operation) {
|
|
|
21345
21398
|
);
|
|
21346
21399
|
if (filledValues.length === 0) return { result: 0 };
|
|
21347
21400
|
const uniqueValues = new Set(filledValues);
|
|
21348
|
-
const percentage2 =
|
|
21401
|
+
const percentage2 = percentOf(uniqueValues.size, filledValues.length);
|
|
21349
21402
|
return { result: percentage2 };
|
|
21350
21403
|
}
|
|
21351
21404
|
}
|
|
@@ -21451,13 +21504,13 @@ function aggregateGenericField(values2, operation) {
|
|
|
21451
21504
|
case "percent_empty": {
|
|
21452
21505
|
if (totalCount === 0) return { result: 0 };
|
|
21453
21506
|
const emptyCount = values2.filter(isEmpty2).length;
|
|
21454
|
-
const percentage2 =
|
|
21507
|
+
const percentage2 = percentOf(emptyCount, totalCount);
|
|
21455
21508
|
return { result: percentage2 };
|
|
21456
21509
|
}
|
|
21457
21510
|
case "percent_filled": {
|
|
21458
21511
|
if (totalCount === 0) return { result: 0 };
|
|
21459
21512
|
const filledCount = values2.filter((value) => !isEmpty2(value)).length;
|
|
21460
|
-
const percentage2 =
|
|
21513
|
+
const percentage2 = percentOf(filledCount, totalCount);
|
|
21461
21514
|
return { result: percentage2 };
|
|
21462
21515
|
}
|
|
21463
21516
|
case "unique": {
|
|
@@ -21484,7 +21537,7 @@ function aggregateGenericField(values2, operation) {
|
|
|
21484
21537
|
uniqueValues.add(value);
|
|
21485
21538
|
}
|
|
21486
21539
|
}
|
|
21487
|
-
const percentage2 =
|
|
21540
|
+
const percentage2 = percentOf(uniqueValues.size, filledValues.length);
|
|
21488
21541
|
return { result: percentage2 };
|
|
21489
21542
|
}
|
|
21490
21543
|
case "selected":
|
|
@@ -21513,13 +21566,13 @@ function aggregateSelectRecordLinkField(values2, operation) {
|
|
|
21513
21566
|
case "percent_empty": {
|
|
21514
21567
|
if (totalCount === 0) return { result: 0 };
|
|
21515
21568
|
const emptyCount = values2.filter(isEmpty2).length;
|
|
21516
|
-
const percentage2 =
|
|
21569
|
+
const percentage2 = percentOf(emptyCount, totalCount);
|
|
21517
21570
|
return { result: percentage2 };
|
|
21518
21571
|
}
|
|
21519
21572
|
case "percent_filled": {
|
|
21520
21573
|
if (totalCount === 0) return { result: 0 };
|
|
21521
21574
|
const filledCount = values2.filter((value) => !isEmpty2(value)).length;
|
|
21522
|
-
const percentage2 =
|
|
21575
|
+
const percentage2 = percentOf(filledCount, totalCount);
|
|
21523
21576
|
return { result: percentage2 };
|
|
21524
21577
|
}
|
|
21525
21578
|
case "selected":
|
|
@@ -21564,13 +21617,13 @@ function aggregateRollupField(values2, operation) {
|
|
|
21564
21617
|
case "percent_empty": {
|
|
21565
21618
|
if (totalCount === 0) return { result: 0 };
|
|
21566
21619
|
const emptyCount = values2.filter(isEmpty2).length;
|
|
21567
|
-
const percentage2 =
|
|
21620
|
+
const percentage2 = percentOf(emptyCount, totalCount);
|
|
21568
21621
|
return { result: percentage2 };
|
|
21569
21622
|
}
|
|
21570
21623
|
case "percent_filled": {
|
|
21571
21624
|
if (totalCount === 0) return { result: 0 };
|
|
21572
21625
|
const filledCount = values2.filter((value) => !isEmpty2(value)).length;
|
|
21573
|
-
const percentage2 =
|
|
21626
|
+
const percentage2 = percentOf(filledCount, totalCount);
|
|
21574
21627
|
return { result: percentage2 };
|
|
21575
21628
|
}
|
|
21576
21629
|
case "unique": {
|
|
@@ -21583,7 +21636,7 @@ function aggregateRollupField(values2, operation) {
|
|
|
21583
21636
|
const filledValues = values2.filter((value) => !isEmpty2(value));
|
|
21584
21637
|
if (filledValues.length === 0) return { result: 0 };
|
|
21585
21638
|
const uniqueValues = new Set(filledValues);
|
|
21586
|
-
const percentage2 =
|
|
21639
|
+
const percentage2 = percentOf(uniqueValues.size, filledValues.length);
|
|
21587
21640
|
return { result: percentage2 };
|
|
21588
21641
|
}
|
|
21589
21642
|
}
|
|
@@ -21904,9 +21957,8 @@ function currencyCodeOf(written) {
|
|
|
21904
21957
|
if (Intl.supportedValuesOf("currency").includes(upper2)) return upper2;
|
|
21905
21958
|
return DONG_SPELLINGS.includes(unitKey(written)) ? "VND" : void 0;
|
|
21906
21959
|
}
|
|
21907
|
-
var EXACT_DIGITS = 15;
|
|
21908
21960
|
function convert(value, from, to) {
|
|
21909
|
-
return from.code === to.code ? value :
|
|
21961
|
+
return from.code === to.code ? value : divideDecimal(multiplyDecimal(value, from.factor), to.factor);
|
|
21910
21962
|
}
|
|
21911
21963
|
function scaleIn(own, pack) {
|
|
21912
21964
|
const kin = MEASURED_UNITS.filter((one) => one.dimension === own.dimension);
|
|
@@ -26351,6 +26403,9 @@ var appQueryDeclarationSchema = zod_default.object({
|
|
|
26351
26403
|
),
|
|
26352
26404
|
templates: zod_default.record(zod_default.string().regex(APP_ALIAS_REGEX), appExportTemplateSchema).optional().describe(
|
|
26353
26405
|
"Document templates an export of this query's rows may fill, by the name the export asks for. An export names one of these or none; a template it does not declare is refused."
|
|
26406
|
+
),
|
|
26407
|
+
screen_only: zod_default.literal(true).optional().describe(
|
|
26408
|
+
"The query feeds one control of the app's screen and answers no question a person asks. It runs like any other; a catalog of what an agent may ask leaves it out."
|
|
26354
26409
|
)
|
|
26355
26410
|
});
|
|
26356
26411
|
var appAgentWritesSchema = zod_default.object({
|
|
@@ -26692,6 +26747,15 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
26692
26747
|
// same position `query_records.field_keys` holds.
|
|
26693
26748
|
return_fields: { kind: "field_key_refs", table_from: "table_id" }
|
|
26694
26749
|
},
|
|
26750
|
+
import_rows: {
|
|
26751
|
+
table_id: { kind: "table_ref", rows: "change" },
|
|
26752
|
+
match: { kind: "field_key_refs", table_from: "table_id" },
|
|
26753
|
+
// Each row's `values` are keyed by field, a link cell possibly `{ lookup }`; the tool resolves both at execute.
|
|
26754
|
+
rows: { kind: "dynamic" },
|
|
26755
|
+
create: { kind: "scalar" },
|
|
26756
|
+
// `fields` is keyed by field; the tool resolves it at execute.
|
|
26757
|
+
conflicts: { kind: "dynamic" }
|
|
26758
|
+
},
|
|
26695
26759
|
delete_records: {
|
|
26696
26760
|
// THE ONE CALL THAT TAKES ROWS. Every column of the row goes with it, so
|
|
26697
26761
|
// nothing field-keyed can carry the fact and it rides the table position —
|
|
@@ -26727,6 +26791,22 @@ var TOOL_INPUT_SEMANTICS = {
|
|
|
26727
26791
|
changes: { kind: "dynamic" },
|
|
26728
26792
|
reason: { kind: "text" }
|
|
26729
26793
|
},
|
|
26794
|
+
// The record names its own table; an entry's id and a cursor are the trail's.
|
|
26795
|
+
get_record_activity: {
|
|
26796
|
+
record_id: { kind: "record_ref" },
|
|
26797
|
+
after: { kind: "scalar" }
|
|
26798
|
+
},
|
|
26799
|
+
undo_record_change: {
|
|
26800
|
+
record_id: { kind: "record_ref" },
|
|
26801
|
+
entry_id: { kind: "scalar" }
|
|
26802
|
+
},
|
|
26803
|
+
// `table_id` names the table whose rows the undo changes, moves to the trash and brings back; a run's id is the
|
|
26804
|
+
// trail's.
|
|
26805
|
+
undo_record_changes: {
|
|
26806
|
+
table_id: { kind: "table_ref", rows: "remove" },
|
|
26807
|
+
record_ids: { kind: "record_refs" },
|
|
26808
|
+
workflow_execution_id: { kind: "scalar" }
|
|
26809
|
+
},
|
|
26730
26810
|
// ─── Query surfaces ────────────────────────────────────────────────────
|
|
26731
26811
|
query_records: {
|
|
26732
26812
|
table_id: { kind: "table_ref", rows: "none" },
|
|
@@ -28163,33 +28243,34 @@ async function evaluateBinary(op, leftExpr, rightExpr, ctx) {
|
|
|
28163
28243
|
assertCheap(ctx.evaluation_budget, left, right);
|
|
28164
28244
|
assertTextWithin("+", typeof left === "string" ? null : left, typeof right === "string" ? null : right);
|
|
28165
28245
|
}
|
|
28166
|
-
const sum2 = addOrConcat(left, right, () => assertNumber(left, "add", "left")
|
|
28246
|
+
const sum2 = addOrConcat(left, right, () => addDecimal(assertNumber(left, "add", "left"), assertNumber(right, "add", "right")));
|
|
28167
28247
|
charge(ctx, builtSize(sum2));
|
|
28168
28248
|
return sum2;
|
|
28169
28249
|
}
|
|
28170
28250
|
case "sub":
|
|
28171
|
-
return assertNumber(left, "sub", "left")
|
|
28251
|
+
return subtractDecimal(assertNumber(left, "sub", "left"), assertNumber(right, "sub", "right"));
|
|
28172
28252
|
case "mul":
|
|
28173
|
-
return assertNumber(left, "mul", "left")
|
|
28253
|
+
return multiplyDecimal(assertNumber(left, "mul", "left"), assertNumber(right, "mul", "right"));
|
|
28174
28254
|
case "div": {
|
|
28175
28255
|
const r = assertNumber(right, "div", "right");
|
|
28176
28256
|
if (r === 0) {
|
|
28177
28257
|
throw new WorkflowEvalError("Division by zero", { kind: "binary", at: "div" });
|
|
28178
28258
|
}
|
|
28179
|
-
return assertNumber(left, "div", "left")
|
|
28259
|
+
return divideDecimal(assertNumber(left, "div", "left"), r);
|
|
28180
28260
|
}
|
|
28181
28261
|
case "mod": {
|
|
28182
28262
|
const r = assertNumber(right, "mod", "right");
|
|
28183
28263
|
if (r === 0) {
|
|
28184
28264
|
throw new WorkflowEvalError("Modulo by zero", { kind: "binary", at: "mod" });
|
|
28185
28265
|
}
|
|
28186
|
-
return assertNumber(left, "mod", "left")
|
|
28266
|
+
return moduloDecimal(assertNumber(left, "mod", "left"), r);
|
|
28187
28267
|
}
|
|
28188
28268
|
}
|
|
28189
28269
|
}
|
|
28190
28270
|
function looseEq(a, b) {
|
|
28191
28271
|
if (isComposite(a) && b !== null && b !== void 0 && !isComposite(b)) return looseEq(textOf(a), b);
|
|
28192
28272
|
if (isComposite(b) && a !== null && a !== void 0 && !isComposite(a)) return looseEq(a, textOf(b));
|
|
28273
|
+
if (typeof a === "number" && typeof b === "number") return compareDecimal(a, b) === 0;
|
|
28193
28274
|
return a == b;
|
|
28194
28275
|
}
|
|
28195
28276
|
function isComposite(value) {
|
|
@@ -28204,6 +28285,7 @@ function isScalar(value) {
|
|
|
28204
28285
|
return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
|
|
28205
28286
|
}
|
|
28206
28287
|
function strictEq(a, b) {
|
|
28288
|
+
if (typeof a === "number" && typeof b === "number") return compareDecimal(a, b) === 0;
|
|
28207
28289
|
return a === b;
|
|
28208
28290
|
}
|
|
28209
28291
|
function compareOrdered(a, b) {
|
|
@@ -28211,7 +28293,7 @@ function compareOrdered(a, b) {
|
|
|
28211
28293
|
return NaN;
|
|
28212
28294
|
}
|
|
28213
28295
|
if (typeof a === "number" && typeof b === "number") {
|
|
28214
|
-
return a
|
|
28296
|
+
return compareDecimal(a, b);
|
|
28215
28297
|
}
|
|
28216
28298
|
if (typeof a === "string" && typeof b === "string") {
|
|
28217
28299
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
@@ -28986,6 +29068,7 @@ function useAppContext() {
|
|
|
28986
29068
|
const { data, error: error51, refetch } = useKey(APP_CONTEXT_KEY, readContext, { focus: false, arrival: false });
|
|
28987
29069
|
return {
|
|
28988
29070
|
memberId: data?.member_id ?? null,
|
|
29071
|
+
insider: data !== void 0 && data.member_id !== null && data.group_ids !== void 0,
|
|
28989
29072
|
groupIds: data?.group_ids ?? null,
|
|
28990
29073
|
absences: data?.absences ?? null,
|
|
28991
29074
|
openableApps: data?.openable_apps ?? null,
|
|
@@ -30510,7 +30593,7 @@ function evaluateCondition(record2, fieldByKey, looseCondition, memberContext, f
|
|
|
30510
30593
|
if (reading.unit !== void 0 && filterCondition.value !== null && filterCondition.value !== void 0) {
|
|
30511
30594
|
const factor = rowUnitFactor(reading.unit, data);
|
|
30512
30595
|
if (factor === void 0) return false;
|
|
30513
|
-
const inBase = { ...filterCondition, value: filterCondition.value
|
|
30596
|
+
const inBase = { ...filterCondition, value: multiplyExactly(filterCondition.value, reading.unit.factor) };
|
|
30514
30597
|
return matchCell(columnValue, field, inBase, looseCondition, memberContext, fileFacts, factor);
|
|
30515
30598
|
}
|
|
30516
30599
|
}
|
|
@@ -30619,7 +30702,7 @@ function matchCell(cell, field, filterCondition, looseCondition, memberContext,
|
|
|
30619
30702
|
matches = foldedValue !== void 0 && needles.some((needle) => foldedValue.includes(needle));
|
|
30620
30703
|
}
|
|
30621
30704
|
} else if (filterCondition.type === "number") {
|
|
30622
|
-
const numberValue = typeof columnValue === "number" ? columnValue
|
|
30705
|
+
const numberValue = typeof columnValue === "number" ? multiplyExactly(columnValue, figureFactor) : columnValue;
|
|
30623
30706
|
if (filterCondition.operator === "is_empty") {
|
|
30624
30707
|
matches = !numberValue && numberValue !== 0;
|
|
30625
30708
|
} else if (filterCondition.operator === "is_not_empty") {
|
|
@@ -32671,8 +32754,7 @@ function oneQueryAlias(entity) {
|
|
|
32671
32754
|
var NO_ENTRIES = [];
|
|
32672
32755
|
function useRecordActivity(entity, record_id, options = {}) {
|
|
32673
32756
|
const { after } = options;
|
|
32674
|
-
const {
|
|
32675
|
-
const available = memberId !== null;
|
|
32757
|
+
const { insider: available, resolved } = useAppContext();
|
|
32676
32758
|
const page = useKey(
|
|
32677
32759
|
available ? JSON.stringify(["activity", entity, record_id, after ?? null]) : null,
|
|
32678
32760
|
() => rpc("activity.list", { entity, record_id, ...after === void 0 ? {} : { after } }),
|
|
@@ -32688,6 +32770,12 @@ function useRecordActivity(entity, record_id, options = {}) {
|
|
|
32688
32770
|
refetch: page.refetch
|
|
32689
32771
|
};
|
|
32690
32772
|
}
|
|
32773
|
+
async function undoRecordChange(entity, record_id, entry_id) {
|
|
32774
|
+
const result = await rpc("activity.undo", { entity, record_id, entry_id });
|
|
32775
|
+
refetchReads([listQueryAlias(entity)]);
|
|
32776
|
+
await readsAnswered([oneQueryAlias(entity)]);
|
|
32777
|
+
return result;
|
|
32778
|
+
}
|
|
32691
32779
|
|
|
32692
32780
|
// src/recording.ts
|
|
32693
32781
|
import { useCallback as useCallback7, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore3 } from "react";
|
|
@@ -33031,6 +33119,7 @@ export {
|
|
|
33031
33119
|
requestGeofencedLocation,
|
|
33032
33120
|
row,
|
|
33033
33121
|
rpc,
|
|
33122
|
+
undoRecordChange,
|
|
33034
33123
|
urlParam,
|
|
33035
33124
|
useAgentRun,
|
|
33036
33125
|
useAgentRuns,
|
package/dist/router.js
CHANGED
package/dist/rpc.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
|
|
|
9
9
|
* app → host: { id, op, payload }
|
|
10
10
|
* host → app: { id, type: "result", data } | { id, type: "error", message }
|
|
11
11
|
*/
|
|
12
|
-
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "recording.start" | "recording.stop";
|
|
12
|
+
export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "activity.undo" | "recording.start" | "recording.stop";
|
|
13
13
|
export interface AgentRunPayload {
|
|
14
14
|
alias: string;
|
|
15
15
|
session_id: string;
|
package/dist/shared_types.d.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* `scripts/build_sdk.mjs`, because that package is private and ships in no
|
|
4
4
|
* tarball. Written at every build, never edited.
|
|
5
5
|
*/
|
|
6
|
-
export type ActivityEntry = { changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
|
|
6
|
+
export type ActivityEntry = { undoable: boolean; changes: { field_id: string; label: string; before: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; after: string | number | boolean | { key: string; label: string; }[] | { id: string; display: string; }[] | { id: string; name: string | null; image?: string | undefined; }[] | { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | null; removed?: true | undefined; }[]; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "change"; created?: true | undefined; undo?: true | undefined; undone?: { by: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; } | undefined; } | { comment: { id: string; body: string; mentions: { id: string; name: string; }[]; files?: { id: string; filename: string; mime_type: string; file_storage_key?: string | undefined; url?: string | undefined; thumbnail_url?: string | undefined; preview_url?: string | undefined; size?: number | null | undefined; created_at?: string | undefined; document_template_id?: string | undefined; }[] | undefined; }; id: string; actor: { kind: "member" | "agent" | "workflow" | "import"; id: string; name: string; image?: string | undefined; }; at: string; until: string; kind: "comment"; };
|
|
7
|
+
export type ActivityUndoResult = { reverted: { field_id: string; label: string; }[]; kept: { field_id: string; label: string; reason: "changed_since" | "refused"; }[]; };
|
|
7
8
|
export type AppAbsence = { member: { id: string; name: string; }; stand_in: { id: string; name: string; }; starts_on: string; ends_on: string; group_ids: string[]; };
|
|
8
9
|
/** The file an app export made, as a link to download it: no workspace file, so no id, and kept a day. */
|
|
9
10
|
export type ExportedFile = {
|
package/dist/store.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export type Fetcher<T> = (askedAt: number, current: T | undefined) => Promise<T>
|
|
|
4
4
|
export declare function tick(): number;
|
|
5
5
|
/** Ask again every key reading one of `aliases` — every query key where none is named; an unmounted one on its next mount. */
|
|
6
6
|
export declare function refetchReads(aliases: readonly string[] | undefined): void;
|
|
7
|
+
/** `refetchReads`, settled once every key a screen reads now holds an answer asked after this call — or its refusal. */
|
|
8
|
+
export declare function readsAnswered(aliases: readonly string[]): Promise<void>;
|
|
7
9
|
/** The host's push, naming the aliases whose tables moved: each key reading one asked again (`refetchReads`), in one
|
|
8
10
|
* paint. */
|
|
9
11
|
export declare function refetchPushed(aliases: readonly string[]): void;
|
package/dist/viewer.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare function readContext(): Promise<AppContext>;
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function useAppContext(): {
|
|
12
12
|
memberId: string | null;
|
|
13
|
+
/** The viewer is a signed-in member of the app's organization: the host named them and told their groups. */
|
|
14
|
+
insider: boolean;
|
|
13
15
|
/** The viewer's member groups, by id; `null` where the host did not say. */
|
|
14
16
|
groupIds: readonly string[] | null;
|
|
15
17
|
/** Who in the organization is away, or will be, and who stands in; `null` where the host did not say. */
|
|
@@ -373,7 +373,7 @@ table's id: read it off a row of the query that renders the badges (`rows[0]?.__
|
|
|
373
373
|
rather than pasting a `tbl_` id, which a copy of the app cannot carry. Pass `undefined` while no
|
|
374
374
|
row is on screen — no request is sent and `counts` is `{}`.
|
|
375
375
|
|
|
376
|
-
## Record activity: `useRecordActivity`
|
|
376
|
+
## Record activity: `useRecordActivity`, `undoRecordChange`
|
|
377
377
|
|
|
378
378
|
Who changed which of a record's fields, and who commented on it — one entry per save
|
|
379
379
|
(`dist/activity.d.ts`). For a JSON app: `entity` is the alias the app's spec names the record's
|
|
@@ -383,6 +383,8 @@ entity under.
|
|
|
383
383
|
const { entries, next, loading, error, available, refetch } = useRecordActivity("job", recordId);
|
|
384
384
|
// The page past it, mounted where the reader asks for more:
|
|
385
385
|
const older = useRecordActivity("job", recordId, { after: next });
|
|
386
|
+
// One change entry put back, as the viewer's own save:
|
|
387
|
+
const { reverted, kept } = await undoRecordChange("job", recordId, entry.id);
|
|
386
388
|
```
|
|
387
389
|
|
|
388
390
|
- **Authority: the app's own read of the record.** The server reaches the record through the
|
|
@@ -392,8 +394,8 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
392
394
|
entry. A field taken out of the app while its table keeps it stays hidden; a field gone from its
|
|
393
395
|
table appears where the version serving when it was written read it, marked `removed`. Comments
|
|
394
396
|
appear only where the app declares `comments`.
|
|
395
|
-
- **Members of the app's organization only** — the trail names its people.
|
|
396
|
-
is `false` and nothing is
|
|
397
|
+
- **Members of the app's organization only** — the trail names its people. For a public visitor,
|
|
398
|
+
a member of another organization, and standalone, `available` is `false` and nothing is asked.
|
|
397
399
|
- **One entry per save.** Edits by one actor on one record less than ten minutes apart read as one
|
|
398
400
|
entry: each field from its value before the first edit to its value after the last, a field edited
|
|
399
401
|
back to where it began dropped (and an entry left with nothing). A comment, another actor or a
|
|
@@ -401,7 +403,9 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
401
403
|
its maker's edits after it, however soon, are entries of their own. `at` is the entry's first
|
|
402
404
|
write, `until` its last.
|
|
403
405
|
- **An entry** (`ActivityEntry`) is `kind: "change"` with `changes`, or `kind: "comment"` with
|
|
404
|
-
`comment: { id, body, mentions }`.
|
|
406
|
+
`comment: { id, body, mentions }`. A change entry also says `undoable` — the viewer may put it
|
|
407
|
+
back — `undone: { by, at }` once someone has, and `undo: true` where it is itself the save that
|
|
408
|
+
put an earlier one back. `actor`: `{ kind, id, name, image? }` — `member` (a person),
|
|
405
409
|
`agent` (an AI acting for a member: the chat, an app's agent, an MCP client — named by that
|
|
406
410
|
member), `workflow` (a workflow on its own, named by the workflow) or `import` (a script over the
|
|
407
411
|
API or the CLI, named by the member whose key ran it). `name` is empty where nothing resolves it.
|
|
@@ -411,6 +415,17 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
411
415
|
`[{ key, label }]` (`readSelect`; each option's label as it was when written), a member field
|
|
412
416
|
`[{ id, name, image? }]`, a link `[{ id, display }]` (`readLinks`), files presigned (`readFiles`),
|
|
413
417
|
`null` for empty.
|
|
418
|
+
- **`undoRecordChange(entity, record_id, entry_id)`** puts one change entry back: each field it
|
|
419
|
+
changed that the app reads returns to what it held before the entry, where the record still holds
|
|
420
|
+
what the entry wrote. It answers `{ reverted, kept }`: the fields put back (`{ field_id, label }`),
|
|
421
|
+
and those left as they stand with a `reason` — `changed_since` (someone changed it since) or
|
|
422
|
+
`refused` (the table refused the write: a lock, a check). A field already holding what it held
|
|
423
|
+
before is in neither. The write is the viewer's own save: they hold `write_data` on the table as
|
|
424
|
+
the call arrives, on a row their own row rule covers, and the record's lock, the table's checks
|
|
425
|
+
and its write rules answer as for any save of theirs. `undoable` is `false`, and the call
|
|
426
|
+
refused, for the entry that made the record, an entry that is an undo, one already put back, and
|
|
427
|
+
a viewer who may not write the record.
|
|
428
|
+
The undo is an entry of its own; the answer arrives once the record's reads are read anew.
|
|
414
429
|
- **Pages**: newest first, about thirty entries each; `next` is where the trail goes on —
|
|
415
430
|
`undefined` at its end. A page may hold fewer entries and still say `next` where the trail it read
|
|
416
431
|
held little the reader may see.
|
|
@@ -428,4 +443,5 @@ const older = useRecordActivity("job", recordId, { after: next });
|
|
|
428
443
|
| `useViewer` | member id (view-as target) | `null` |
|
|
429
444
|
| `useWorkspaceTimezone` / `useWorkspaceCurrency` | ✓ | ✓ |
|
|
430
445
|
| `useComments` / `useCommentCounts` | ✓ when capability declared | ✗ `available: false` |
|
|
431
|
-
| `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false
|
|
446
|
+
| `useRecordActivity` | ✓ the app's organization's members | ✗ `available: false`, nothing asked |
|
|
447
|
+
| `undoRecordChange` | ✓ a member holding the table's write, on a row they may write | ✗ rejects |
|
package/docs/runtime.md
CHANGED
|
@@ -210,6 +210,7 @@ surfaces:
|
|
|
210
210
|
|---|---|---|
|
|
211
211
|
| `query`, `field_options`, `workflow`, `members`, `context`, `upload`, `urlState.get/set`, `openExternal` | yes | yes |
|
|
212
212
|
| `comments.*` | yes | rejects — `"Comments are available only in embedded apps — a signed-in member is required."` |
|
|
213
|
+
| `activity.list`, `activity.undo` | yes, for a member of the app's organization; `activity.undo` for one who may write the record. A host that does not carry the op answers `"Unknown RPC op: activity.undo"` | rejects — `"A record's activity is available only in embedded apps — a signed-in member is required."` |
|
|
213
214
|
| `agentRun` (streaming, internal to `useAgentRun`) | yes | yes |
|
|
214
215
|
| `agentRun.get`, `agentRun.cancel` | yes | yes |
|
|
215
216
|
| `agentRuns` (session history) | **no** — the product host doesn't implement it either (`"Unknown RPC op: agentRuns"`) | rejects — a standalone caller is anonymous, and a history is its member's (`"App agent runs require an authenticated member …"`) |
|