@lotics/app-sdk 0.113.1 → 0.115.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.
@@ -174,6 +174,42 @@ function transportErrorMessage(status, parsed) {
174
174
  return status < 500 || authored ? jsonMessage : gatewayErrorMessage(status);
175
175
  }
176
176
 
177
+ // ../shared/src/schemas/read_row.ts
178
+ var RECORD_ID_COLUMN = "__source_record_id";
179
+
180
+ // ../shared/src/schemas/app_query_aggregate.ts
181
+ var APP_QUERY_BATCH_MAX = 16;
182
+ var AGGREGATE_COUNT_COLUMN = "__count";
183
+ var AGGREGATE_SUM_COLUMN = "__sum";
184
+ var AGGREGATE_DAY_COLUMN = "__day";
185
+ function dayOf(cell) {
186
+ return typeof cell === "string" && cell !== "" ? cell.slice(0, 10) : null;
187
+ }
188
+ function aggregateRows(rows, aggregate) {
189
+ const by = aggregate.by ?? [];
190
+ const day = aggregate.day;
191
+ const sum = aggregate.sum;
192
+ const keyed = /* @__PURE__ */ new Map();
193
+ for (const row of rows) {
194
+ const key = JSON.stringify([...by.map((column) => row[column] ?? null), ...day === void 0 ? [] : [dayOf(row[day])]]);
195
+ keyed.set(key, [...keyed.get(key) ?? [], row]);
196
+ }
197
+ return [...keyed.values()].map((members) => {
198
+ const [first2] = members;
199
+ const figures = members.flatMap((member) => {
200
+ const value = sum === void 0 ? void 0 : member[sum];
201
+ return typeof value === "number" ? [value] : [];
202
+ });
203
+ return {
204
+ [RECORD_ID_COLUMN]: null,
205
+ ...Object.fromEntries(by.map((column) => [column, first2?.[column] ?? null])),
206
+ ...day === void 0 ? {} : { [AGGREGATE_DAY_COLUMN]: dayOf(first2?.[day]) },
207
+ [AGGREGATE_COUNT_COLUMN]: members.length,
208
+ ...sum === void 0 ? {} : { [AGGREGATE_SUM_COLUMN]: figures.length === 0 ? null : figures.reduce((total, value) => total + value, 0) }
209
+ };
210
+ });
211
+ }
212
+
177
213
  // src/upload/optimize.ts
178
214
  var DEFAULT_IMAGE_FIDELITY = "high";
179
215
  var IMAGE_POLICIES = {
@@ -1032,9 +1068,47 @@ async function standaloneWriteModel() {
1032
1068
  const { app_id } = await boot();
1033
1069
  return apiCall("GET", `/v1/apps/${app_id}/write_model`, void 0, { appId: app_id });
1034
1070
  }
1035
- async function standaloneQuery(payload) {
1036
- const { app_id } = await boot();
1037
- return apiCall("POST", `/v1/apps/${app_id}/query`, payload, { appId: app_id });
1071
+ var READ_BATCH_WINDOW_MS = 5;
1072
+ var askedReads = [];
1073
+ function standaloneQuery(payload) {
1074
+ return new Promise((resolve, reject) => {
1075
+ askedReads = [...askedReads, { payload, resolve, reject }];
1076
+ if (askedReads.length === 1) setTimeout(() => void sendAskedReads(), READ_BATCH_WINDOW_MS);
1077
+ });
1078
+ }
1079
+ async function sendAskedReads() {
1080
+ const asked = askedReads;
1081
+ askedReads = [];
1082
+ const booted = await boot().catch((error) => {
1083
+ asked.forEach((read) => read.reject(error));
1084
+ return void 0;
1085
+ });
1086
+ if (booted === void 0) return;
1087
+ for (let at = 0; at < asked.length; at += APP_QUERY_BATCH_MAX) {
1088
+ const chunk = asked.slice(at, at + APP_QUERY_BATCH_MAX);
1089
+ apiCall("POST", `/v1/apps/${booted.app_id}/query_batch`, { reads: chunk.map((read) => read.payload) }, { appId: booted.app_id }).then((answered) => {
1090
+ const results = batchResults(answered);
1091
+ chunk.forEach((read, k) => {
1092
+ const answer = results[k];
1093
+ if (answer === void 0) read.reject(new Error(`The server answered ${results.length} of ${chunk.length} reads.`));
1094
+ else if ("result" in answer) read.resolve(answer.result);
1095
+ else read.reject(new ApiRefusal(transportErrorMessage(answer.error.status, answer.error)));
1096
+ });
1097
+ }).catch((error) => chunk.forEach((read) => read.reject(error)));
1098
+ }
1099
+ }
1100
+ function batchResults(answered) {
1101
+ if (answered === null || typeof answered !== "object" || !("results" in answered) || !Array.isArray(answered.results)) {
1102
+ throw new Error("The server's batch answer carried no results.");
1103
+ }
1104
+ return answered.results.map((answer, at) => {
1105
+ if (answer !== null && typeof answer === "object" && "result" in answer) return { result: answer.result };
1106
+ const error = answer !== null && typeof answer === "object" && "error" in answer ? answer.error : void 0;
1107
+ if (error !== null && typeof error === "object" && "status" in error && typeof error.status === "number" && "message" in error && typeof error.message === "string") {
1108
+ return { error: { status: error.status, message: error.message, code: "code" in error ? error.code : void 0 } };
1109
+ }
1110
+ throw new Error(`The server's batch answer ${at} is neither a result nor a refusal.`);
1111
+ });
1038
1112
  }
1039
1113
  async function standaloneExport(payload) {
1040
1114
  const { app_id } = await boot();
@@ -1119,6 +1193,9 @@ async function standaloneUpload(file, fidelity) {
1119
1193
 
1120
1194
  export {
1121
1195
  __export,
1196
+ AGGREGATE_COUNT_COLUMN,
1197
+ AGGREGATE_SUM_COLUMN,
1198
+ aggregateRows,
1122
1199
  DEFAULT_IMAGE_FIDELITY,
1123
1200
  recordingsVersion,
1124
1201
  subscribeRecordings,
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import {
2
+ AGGREGATE_COUNT_COLUMN,
3
+ AGGREGATE_SUM_COLUMN,
2
4
  DEFAULT_IMAGE_FIDELITY,
3
5
  __export,
6
+ aggregateRows,
4
7
  anyRecordingLive,
5
8
  applyPatch,
6
9
  decodeAll,
@@ -22,7 +25,7 @@ import {
22
25
  subscribeRecordings,
23
26
  subscribeUrlParams,
24
27
  urlParam
25
- } from "./chunk-CRJQYSDI.js";
28
+ } from "./chunk-EWCMZCKA.js";
26
29
 
27
30
  // src/mount.tsx
28
31
  import { createRoot } from "react-dom/client";
@@ -23968,6 +23971,180 @@ function everyOptionMarked(nameOf2) {
23968
23971
  };
23969
23972
  }
23970
23973
 
23974
+ // ../shared/src/schemas/iam.ts
23975
+ var jsonSchemaSchema = zod_default.string();
23976
+ var organizationSchema = zod_default.object({
23977
+ id: zod_default.string().describe("Unique identifier for the organization"),
23978
+ name: zod_default.string().describe("Display name of the organization"),
23979
+ slug: zod_default.string().nullable().describe("Slug for the organization")
23980
+ });
23981
+ var memberAccountTypeSchema = zod_default.enum(["self_managed", "admin_managed", "service"]).describe(
23982
+ "How the account is credentialed. admin_managed means an admin chose the sign-in name and set the password; the account has NO email address at all, so nothing can be mailed to it until the member claims one they control. service means no credential exists at all \u2014 the membership is an integration's identity, reached only by an API key bound to it."
23983
+ );
23984
+ var ALL_ACCOUNT_TYPES = memberAccountTypeSchema.options;
23985
+ var memberSchema = zod_default.object({
23986
+ member_id: zod_default.string().describe("Unique identifier for the member"),
23987
+ organization_id: zod_default.string().describe("Organization ID the member belongs to"),
23988
+ name: zod_default.string().describe("Display name of the member"),
23989
+ email_address: zod_default.string().nullable().describe(
23990
+ "What the member types to sign in: an email address for a self-managed member, an administrator-issued name (e.g. giamdoc.sunrise) for an admin-managed one. The latter is NOT an address and cannot receive mail \u2014 the field name predates the split. Null for a service identity, which holds no credential and signs in nowhere."
23991
+ ),
23992
+ role: zod_default.enum(["owner", "admin", "member"]).describe("Organization role"),
23993
+ image: zod_default.string().nullable().optional().describe("URL to the member's profile picture"),
23994
+ account_type: memberAccountTypeSchema,
23995
+ archived: zod_default.boolean().optional().describe(
23996
+ "True when this person has been removed from the organization. The membership survives removal so records naming them keep resolving; it grants nothing. Optional because an older server omits it \u2014 absent means active."
23997
+ ),
23998
+ joined_at: zod_default.string().optional().describe(
23999
+ "ISO timestamp of when this membership began. Optional because an older server omits it \u2014 absent means not told, never 'joined at the epoch'."
24000
+ )
24001
+ });
24002
+ var pendingInvitationSchema = zod_default.object({
24003
+ invitation_id: zod_default.string().describe("Unique identifier for the pending invitation"),
24004
+ organization_id: zod_default.string().describe("Organization the invitee will join on accept"),
24005
+ email: zod_default.string().describe("Email the invitation was sent to"),
24006
+ name: zod_default.string().nullable().describe("Admin-supplied display name (carried onto member.name on accept; invitee can override at accept time)"),
24007
+ role: zod_default.enum(["owner", "admin", "member"]).describe("Role the invitee will receive on accept"),
24008
+ expires_at: zod_default.string().describe("ISO timestamp when the invitation expires")
24009
+ });
24010
+ var ianaTimezoneSchema = zod_default.string().refine(
24011
+ (tz) => {
24012
+ try {
24013
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
24014
+ return true;
24015
+ } catch {
24016
+ return false;
24017
+ }
24018
+ },
24019
+ { message: "must be an IANA timezone name, e.g. Asia/Ho_Chi_Minh or America/New_York" }
24020
+ ).describe("IANA timezone name (e.g., 'America/New_York', 'Asia/Ho_Chi_Minh')");
24021
+ var currencyCodeSchema = zod_default.string().trim().toUpperCase().regex(/^[A-Z]{3}$/, "must be an ISO 4217 currency code, e.g. VND, USD, EUR").describe("ISO 4217 currency code (e.g. 'VND', 'USD', 'EUR'). Upper-cased.");
24022
+ var workspaceSchema = zod_default.object({
24023
+ id: zod_default.string().describe("Unique identifier for the workspace"),
24024
+ name: zod_default.string().describe("Display name of the workspace"),
24025
+ default_currency: zod_default.string().describe("Default currency code for the workspace"),
24026
+ // Plain string on the way OUT, deliberately. This is a response schema, so
24027
+ // validating here would make the server reject its own stored rows — and on
24028
+ // the workspace LIST one bad row fails the entire array. The zone is enforced
24029
+ // where it is written; a read is too late to do anything about it anyway.
24030
+ timezone: zod_default.string().describe("IANA timezone name (e.g., 'America/New_York', 'Asia/Ho_Chi_Minh')"),
24031
+ organization_id: zod_default.string().describe("Organization ID this workspace belongs to"),
24032
+ created_at: zod_default.string().describe("Timestamp when workspace was created")
24033
+ });
24034
+ var memberGroupSchema = zod_default.object({
24035
+ id: zod_default.string().describe("Unique identifier for the member group"),
24036
+ name: zod_default.string().describe("Display name of the group"),
24037
+ description: zod_default.string().optional().describe("Optional description of the group's purpose"),
24038
+ member_ids: zod_default.array(zod_default.string()).describe("Array of member IDs in this group"),
24039
+ invitation_ids: zod_default.array(zod_default.string()).describe("Array of pending invitation IDs pre-granted membership; migrated to member_ids on accept"),
24040
+ organization_id: zod_default.string().describe("ID of the organization this group belongs to"),
24041
+ created_at: zod_default.string().describe("Timestamp when group was created"),
24042
+ created_by: zod_default.string().describe("Member ID of the user who created this group")
24043
+ });
24044
+ var principalGroupSchema = zod_default.object({
24045
+ id: zod_default.string().describe("Unique identifier for the member group"),
24046
+ name: zod_default.string().describe("Display name of the group"),
24047
+ description: zod_default.string().optional().describe("Optional description of the group's purpose"),
24048
+ member_count: zod_default.number().int().describe("Number of accepted members in the group"),
24049
+ invitation_count: zod_default.number().int().describe("Number of pending invitations pre-granted membership")
24050
+ });
24051
+ var PUBLIC_PRINCIPAL_ID = "public";
24052
+ var principalSchema = zod_default.discriminatedUnion("type", [
24053
+ zod_default.object({
24054
+ type: zod_default.literal("member").describe("Principal type: individual member"),
24055
+ id: zod_default.string().describe("Member ID")
24056
+ }),
24057
+ zod_default.object({
24058
+ type: zod_default.literal("member_group").describe("Principal type: group of members"),
24059
+ id: zod_default.string().describe("Member group ID")
24060
+ }),
24061
+ zod_default.object({
24062
+ type: zod_default.literal("organization").describe("Principal type: all members in organization"),
24063
+ id: zod_default.string().describe("Organization ID")
24064
+ }),
24065
+ zod_default.object({
24066
+ type: zod_default.literal("public").describe("Principal type: anyone, including unauthenticated visitors"),
24067
+ id: zod_default.literal(PUBLIC_PRINCIPAL_ID).describe("Always the `public` sentinel \u2014 no entity")
24068
+ }),
24069
+ // Pending invitation as principal: lets admins pre-grant group membership and
24070
+ // resource access before the invitee accepts. Never resolved as a Subject —
24071
+ // a pending invitee cannot authenticate, so these bindings stay dormant until
24072
+ // accept-time migration rewrites them to type:"member".
24073
+ zod_default.object({
24074
+ type: zod_default.literal("invitation").describe("Principal type: pending invitation (not yet accepted)"),
24075
+ id: zod_default.string().describe("Invitation ID")
24076
+ })
24077
+ ]);
24078
+ var subjectSchema = zod_default.discriminatedUnion("type", [
24079
+ zod_default.object({
24080
+ type: zod_default.literal("member").describe("Subject type: authenticated member"),
24081
+ id: zod_default.string().describe("Member ID")
24082
+ }),
24083
+ zod_default.object({
24084
+ type: zod_default.literal("public").describe("Subject type: unauthenticated visitor")
24085
+ })
24086
+ ]);
24087
+ var resourceTypeSchema = zod_default.enum(["table", "table_view", "app", "automation", "skill", "document_template", "instruction", "knowledge_doc", "connected_account"]);
24088
+ var RESOURCE_ROLE_ACTIONS = {
24089
+ table: {
24090
+ editor: ["read_data", "write_data"],
24091
+ viewer: ["read_data"]
24092
+ },
24093
+ app: { manager: ["use", "manage"], user: ["use"] },
24094
+ skill: { manager: ["use", "manage"], user: ["use"] },
24095
+ document_template: { manager: ["use", "manage"], user: ["use"] },
24096
+ instruction: { manager: ["use", "manage"], user: ["use"] },
24097
+ knowledge_doc: { manager: ["use", "manage"], user: ["use"] },
24098
+ connected_account: { manager: ["use", "manage"], user: ["use"] },
24099
+ table_view: {},
24100
+ automation: {}
24101
+ };
24102
+ function shareableRolesFor(type) {
24103
+ return Object.keys(RESOURCE_ROLE_ACTIONS[type]);
24104
+ }
24105
+ var SHAREABLE_RESOURCE_TYPES = resourceTypeSchema.options.filter(
24106
+ (type) => shareableRolesFor(type).length > 0
24107
+ );
24108
+ var accessPrincipalSchema = zod_default.discriminatedUnion("type", [
24109
+ zod_default.object({
24110
+ type: zod_default.literal("member").describe("Principal type: an individual member"),
24111
+ id: zod_default.string().describe("Member ID")
24112
+ }),
24113
+ zod_default.object({
24114
+ type: zod_default.literal("member_group").describe("Principal type: a member group"),
24115
+ id: zod_default.string().describe("Group ID")
24116
+ }),
24117
+ zod_default.object({
24118
+ type: zod_default.literal("organization").describe("Principal type: everyone in the organization"),
24119
+ id: zod_default.string().describe("Organization ID")
24120
+ })
24121
+ ]);
24122
+ var resourceAccessSchema = zod_default.object({
24123
+ resource_id: zod_default.string().describe("ID of the resource these principals can reach"),
24124
+ principals: zod_default.array(accessPrincipalSchema).describe("Members, groups and the organization that have been granted access")
24125
+ });
24126
+ var contentResourceTypeSchema = zod_default.enum(["knowledge_doc", "document_template"]);
24127
+ var contentItemSchema = zod_default.object({
24128
+ id: zod_default.string().describe("ID of the knowledge doc or template"),
24129
+ name: zod_default.string().describe("Its name"),
24130
+ owner_member_id: zod_default.string().nullable().describe("Member who owns it; null when nobody does"),
24131
+ created_at: zod_default.string().describe("When it was created"),
24132
+ updated_at: zod_default.string().nullable().describe("When a knowledge doc was last changed; null for a template"),
24133
+ hidden_at: zod_default.string().nullable().describe("When a knowledge doc was hidden; null when it is in use, and for a template"),
24134
+ template_type: zod_default.string().nullable().describe("A template's type (html, email, word, excel, pdf-form); null for a knowledge doc"),
24135
+ can_open: zod_default.boolean().describe("Whether the caller may open it; false means it is not shared with them"),
24136
+ principals: zod_default.array(accessPrincipalSchema).describe("Members, groups and the organization it has been shared with, besides its owner")
24137
+ });
24138
+ var resourceRoleBindingSchema = zod_default.object({
24139
+ id: zod_default.string().describe("Unique identifier for the role binding"),
24140
+ principal: principalSchema.describe(
24141
+ "Who this role binding applies to (member, group, or organization)"
24142
+ ),
24143
+ resource_type: resourceTypeSchema.describe("Type of resource being accessed"),
24144
+ resource_id: zod_default.string().describe("ID of the specific resource"),
24145
+ role: zod_default.string().describe("Role name (e.g., 'viewer', 'editor', 'manager', 'user')")
24146
+ });
24147
+
23971
24148
  // ../shared/src/diacritics.ts
23972
24149
  var COMBINING_MARKS = /[̀-ͯ]/g;
23973
24150
  function foldDiacritics(value) {
@@ -24828,7 +25005,7 @@ var tableFieldTypeConfigSchema = zod_default.discriminatedUnion("type", [
24828
25005
  zod_default.object({
24829
25006
  type: zod_default.literal("number"),
24830
25007
  format: numberFormatSchema.default("number"),
24831
- currency: zod_default.string().optional().describe("ISO 4217 code"),
25008
+ currency: currencyCodeSchema.optional(),
24832
25009
  unit: zod_default.string().optional().describe(UNIT_DESCRIPTION),
24833
25010
  unit_field: zod_default.string().optional().describe(UNIT_FIELD_DESCRIPTION),
24834
25011
  currency_field: zod_default.string().optional().describe(CURRENCY_FIELD_DESCRIPTION),
@@ -24897,7 +25074,7 @@ var tableFieldTypeConfigSchema = zod_default.discriminatedUnion("type", [
24897
25074
  }),
24898
25075
  zod_default.object({
24899
25076
  type: zod_default.literal("formula"),
24900
- formula: formulaInputSchema
25077
+ formula: formulaInputSchema.extend({ currency: currencyCodeSchema.optional() })
24901
25078
  }).superRefine((field, ctx) => figureIssue(["formula"])(field.formula, ctx)),
24902
25079
  // `button` is deliberately absent: button fields are legacy — this union
24903
25080
  // shapes CREATE input only, and new buttons can no longer be created
@@ -24969,173 +25146,6 @@ function projectCell(field, value) {
24969
25146
  }
24970
25147
  }
24971
25148
 
24972
- // ../shared/src/walk_workflow_expression.ts
24973
- var LINKED_CALL = "linked";
24974
- var GRAMMAR_NAME_SHAPE = `${LINKED_CALL}(record["fld_link"])[0] fetches one linked row`;
24975
- var RUNTIME_KEYS = /* @__PURE__ */ new Set([
24976
- "timezone",
24977
- "workflow_id",
24978
- "execution_id",
24979
- "workspace_id",
24980
- "organization_id",
24981
- "now",
24982
- "change_origin",
24983
- "execution_principal",
24984
- "triggered_by_member_id"
24985
- ]);
24986
- var CURRENT_MEMBER_IN_ANY_GROUP = "current_member_in_any_group";
24987
- var PARKED_RUNTIME_KEYS = /* @__PURE__ */ new Map([
24988
- [
24989
- "execution_principal",
24990
- `runtime.execution_principal is not readable \u2014 a workflow runs under the OWNER's authority, so it never answers "is the person who triggered this allowed?". Authorize the caller with ${CURRENT_MEMBER_IN_ANY_GROUP}(["grp_\u2026"]) or read runtime.triggered_by_member_id.`
24991
- ]
24992
- ]);
24993
- var READABLE_RUNTIME_KEYS = Array.from(RUNTIME_KEYS).filter(
24994
- (key) => !PARKED_RUNTIME_KEYS.has(key)
24995
- );
24996
- var INLINE_HELPERS = [
24997
- "formatCurrency",
24998
- "randomNumber",
24999
- "randomAlphaNumeric",
25000
- "sample",
25001
- // Actor authorization predicate — resolves the triggering member's groups
25002
- // server-side (bound in the backend helper registry, not a pure shared
25003
- // helper).
25004
- CURRENT_MEMBER_IN_ANY_GROUP
25005
- ];
25006
- var HELPERS = /* @__PURE__ */ new Set([
25007
- ...Object.keys(expression_array_exports),
25008
- ...Object.keys(expression_string_exports),
25009
- ...Object.keys(expression_math_exports),
25010
- ...Object.keys(expression_object_exports),
25011
- ...Object.keys(expression_type_exports),
25012
- ...Object.keys(getDateExpressionFunctions()),
25013
- ...INLINE_HELPERS
25014
- ]);
25015
- function isHelper(name) {
25016
- return HELPERS.has(name);
25017
- }
25018
- var HELPER_MENU = [
25019
- `text \u2014 ${Object.keys(expression_string_exports).sort().join(", ")}`,
25020
- `number \u2014 ${Object.keys(expression_math_exports).sort().join(", ")}`,
25021
- `array \u2014 ${Object.keys(expression_array_exports).sort().join(", ")}`,
25022
- `object \u2014 ${Object.keys(expression_object_exports).sort().join(", ")}`,
25023
- `type/null \u2014 ${Object.keys(expression_type_exports).sort().join(", ")}`,
25024
- `date \u2014 ${Object.keys(getDateExpressionFunctions()).sort().join(", ")}`,
25025
- `other \u2014 ${[...INLINE_HELPERS].sort().join(", ")}`
25026
- ].join("; ");
25027
- var AWAIT_IF_FORM = "Write the if form: let x = null; if (<condition>) { x = await tool({...}); }";
25028
- var AWAIT_IN_OPTIONAL_CHAIN = `An await after \`?.\` runs only when the value before it is not null. ${AWAIT_IF_FORM}`;
25029
- var CALLBACK_HELPERS = /* @__PURE__ */ new Set([
25030
- "filter",
25031
- "find",
25032
- "pluck",
25033
- "sortBy",
25034
- "sumBy",
25035
- "meanBy",
25036
- "minBy",
25037
- "maxBy",
25038
- "countBy",
25039
- "groupBy",
25040
- "uniqueBy",
25041
- "some",
25042
- "every",
25043
- "differenceBy",
25044
- "intersectionBy",
25045
- "reduce"
25046
- ]);
25047
- var LAMBDA_POSITION_HINT = `Function expressions are not supported here. A lambda \u2014 either \`(x) => <expr>\` or \`function (x) { return <expr>; }\` \u2014 is legal only INLINE as a callback argument to ${[...CALLBACK_HELPERS].sort().join(" / ")}, e.g. pluck(rows, (r) => r.qty * r.price). To NAME reusable logic, declare it at the top level instead: \`function toLine(l = {}) { return { ... }; }\` \u2014 every parameter needs a default, and the body is inlined at each call site. Binding a lambda to a name (const f = (x) => ...) or calling one directly stays unsupported.`;
25048
- var HELPER_ALIASES = /* @__PURE__ */ new Map([
25049
- ["String", "toString"],
25050
- ["Number", "toNumber"],
25051
- ["parseFloat", "toNumber"],
25052
- // faithful: toNumber IS parseFloat semantics
25053
- ["toUpperCase", "upper"],
25054
- ["toLowerCase", "lower"],
25055
- ["map", "pluck"],
25056
- // arr.map(x => …) ≡ pluck(arr, x => …)
25057
- ["toFixed", "formatNumber"]
25058
- // x.toFixed(2) ≡ formatNumber(x, 2)
25059
- ]);
25060
- var REJECTED_HELPERS = /* @__PURE__ */ new Map([
25061
- [
25062
- "parseInt",
25063
- {
25064
- message: "parseInt is not supported \u2014 parse with toNumber(x), then apply floor(...), ceil(...), or round(...) to get a whole number. floor rounds down (toward negative infinity), so for negative values choose the rounding deliberately.",
25065
- construct: "parse_int"
25066
- }
25067
- ]
25068
- ]);
25069
- var NAMESPACE_HELPERS = /* @__PURE__ */ new Map([
25070
- ["JSON", /* @__PURE__ */ new Map([["parse", "parseJson"], ["stringify", "toJson"]])],
25071
- ["Array", /* @__PURE__ */ new Map([["isArray", "isArray"]])],
25072
- ["Object", /* @__PURE__ */ new Map([["keys", "keys"], ["values", "values"], ["entries", "entries"]])],
25073
- ["Number", /* @__PURE__ */ new Map([["parseFloat", "toNumber"]])]
25074
- ]);
25075
- var METHOD_FORM_NAMES = /* @__PURE__ */ new Set([
25076
- // String.prototype
25077
- "trim",
25078
- "toUpperCase",
25079
- "toLowerCase",
25080
- "replace",
25081
- "replaceAll",
25082
- "split",
25083
- "substring",
25084
- "padStart",
25085
- "padEnd",
25086
- "startsWith",
25087
- "endsWith",
25088
- // String.prototype + Array.prototype
25089
- "includes",
25090
- "slice",
25091
- "at",
25092
- "concat",
25093
- // Array.prototype
25094
- "join",
25095
- "reverse",
25096
- "map",
25097
- "filter",
25098
- "find",
25099
- "some",
25100
- "every",
25101
- "reduce",
25102
- // Number.prototype / Object.prototype
25103
- "toFixed",
25104
- "toString"
25105
- ]);
25106
- var METHOD_FORM_MENU = [...METHOD_FORM_NAMES].sort().join(", ");
25107
- var PROPERTY_HELPERS = /* @__PURE__ */ new Map([["length", "length"]]);
25108
- var HELPER_SPELLINGS = (() => {
25109
- const out = /* @__PURE__ */ new Map();
25110
- const add = (helper, spelling) => {
25111
- if (isHelper(helper) && !out.has(helper)) out.set(helper, spelling);
25112
- };
25113
- for (const name of METHOD_FORM_NAMES) add(HELPER_ALIASES.get(name) ?? name, { kind: "method", name });
25114
- for (const [name, helper] of PROPERTY_HELPERS) add(helper, { kind: "property", name });
25115
- for (const [namespace, members2] of NAMESPACE_HELPERS) {
25116
- for (const [member, helper] of members2) add(helper, { kind: "namespace", namespace, member });
25117
- }
25118
- for (const fn of Object.keys(expression_math_exports)) {
25119
- if (fn !== "random" && typeof Reflect.get(Math, fn) === "function") {
25120
- add(fn, { kind: "namespace", namespace: "Math", member: fn });
25121
- }
25122
- }
25123
- return out;
25124
- })();
25125
- var RESERVED_CALLABLE_NAMES = /* @__PURE__ */ new Set([
25126
- ...REJECTED_HELPERS.keys(),
25127
- ...NAMESPACE_HELPERS.keys(),
25128
- "Math",
25129
- "Boolean"
25130
- ]);
25131
- var LINKED_ARITY = `${LINKED_CALL}(...) takes exactly one argument \u2014 the read of the link field, e.g. ${LINKED_CALL}(record["fld_link"])[0] for one row or ${LINKED_CALL}(record["fld_link"]) for every row.`;
25132
- var LINKED_ARGUMENT_SHAPE = `The argument of ${LINKED_CALL}(...) must END on the link field, because that field is what it follows \u2014 ${LINKED_CALL}(record["fld_link"])[0]. A value that names no field (a binding, a literal, a helper result) cannot say which link to follow; read the field on it: ${LINKED_CALL}(ids["fld_link"]).`;
25133
- var LINKED_OPTIONAL_RESULT = `${LINKED_CALL}(...) always returns an array, never null, so \`?.\` on its result can never fire. The argument takes a guard \u2014 ${LINKED_CALL}(record?.["fld_link"])[0] \u2014 and so does the fetched row, which is null where the linked record is deleted or unreadable: ${LINKED_CALL}(record["fld_link"])[0]?.["fld_x"].`;
25134
- var LINKED_METHOD_FORM = `${LINKED_CALL} has no method form \u2014 write ${LINKED_CALL}(record["fld_link"])[0], with the link read as the argument.`;
25135
- var LINKED_ON_NON_RECORD = `${LINKED_CALL}(...) follows a link field on a record, and this value is not one. Read the record first, then follow the link: ${LINKED_CALL}(record["fld_link"])[0].`;
25136
- var LINKED_ON_CHANGES = `A change diff is not a record, so ${LINKED_CALL}(...) cannot follow a link off it. Read the post-state with ${LINKED_CALL}(record["fld_link"]) or the pre-state with ${LINKED_CALL}(prev_record["fld_link"]).`;
25137
- var READ_ROOT_HINT = `A read starts from a name: a reserved identifier (record, prev_record, changes, runtime, trigger, index), a binding, or ${LINKED_CALL}(record["fld_link"]). Bind the value first \u2014 const x = <expression>; \u2014 then read x.y.`;
25138
-
25139
25149
  // ../shared/src/schemas/workflow_expressions.ts
25140
25150
  var runtimeKeySchema = zod_default.enum([
25141
25151
  "timezone",
@@ -25145,15 +25155,6 @@ var runtimeKeySchema = zod_default.enum([
25145
25155
  "organization_id",
25146
25156
  "now",
25147
25157
  "change_origin",
25148
- // PARKED — accepted in stored ASTs, rejected for new saves at parse (see
25149
- // `PARKED_RUNTIME_KEYS` in walk_workflow_expression.ts). It is the OWNER's
25150
- // principal (a workflow runs under owner authority), so it can't answer the
25151
- // caller-authorization question anyone reads it for; the generated `.d.ts`
25152
- // omits it and the docs point at `current_member_in_any_group` /
25153
- // `runtime.triggered_by_member_id` instead. The entry stays so any stored
25154
- // AST keeps validating and executing — drop it only once a prod probe shows
25155
- // zero references (`workflows.steps_v2::text LIKE '%execution_principal%'`).
25156
- "execution_principal",
25157
25158
  "triggered_by_member_id"
25158
25159
  ]);
25159
25160
  var nestedExpressionSchema = zod_default.lazy(
@@ -25326,6 +25327,154 @@ var templatePartSchema = zod_default.lazy(
25326
25327
  ])
25327
25328
  );
25328
25329
 
25330
+ // ../shared/src/walk_workflow_expression.ts
25331
+ var LINKED_CALL = "linked";
25332
+ var GRAMMAR_NAME_SHAPE = `${LINKED_CALL}(record["fld_link"])[0] fetches one linked row`;
25333
+ var RUNTIME_KEYS = new Set(runtimeKeySchema.options);
25334
+ var CURRENT_MEMBER_IN_ANY_GROUP = "current_member_in_any_group";
25335
+ var INLINE_HELPERS = [
25336
+ "formatCurrency",
25337
+ "randomNumber",
25338
+ "randomAlphaNumeric",
25339
+ "sample",
25340
+ // Actor authorization predicate — resolves the triggering member's groups
25341
+ // server-side (bound in the backend helper registry, not a pure shared
25342
+ // helper).
25343
+ CURRENT_MEMBER_IN_ANY_GROUP
25344
+ ];
25345
+ var HELPERS = /* @__PURE__ */ new Set([
25346
+ ...Object.keys(expression_array_exports),
25347
+ ...Object.keys(expression_string_exports),
25348
+ ...Object.keys(expression_math_exports),
25349
+ ...Object.keys(expression_object_exports),
25350
+ ...Object.keys(expression_type_exports),
25351
+ ...Object.keys(getDateExpressionFunctions()),
25352
+ ...INLINE_HELPERS
25353
+ ]);
25354
+ function isHelper(name) {
25355
+ return HELPERS.has(name);
25356
+ }
25357
+ var HELPER_MENU = [
25358
+ `text \u2014 ${Object.keys(expression_string_exports).sort().join(", ")}`,
25359
+ `number \u2014 ${Object.keys(expression_math_exports).sort().join(", ")}`,
25360
+ `array \u2014 ${Object.keys(expression_array_exports).sort().join(", ")}`,
25361
+ `object \u2014 ${Object.keys(expression_object_exports).sort().join(", ")}`,
25362
+ `type/null \u2014 ${Object.keys(expression_type_exports).sort().join(", ")}`,
25363
+ `date \u2014 ${Object.keys(getDateExpressionFunctions()).sort().join(", ")}`,
25364
+ `other \u2014 ${[...INLINE_HELPERS].sort().join(", ")}`
25365
+ ].join("; ");
25366
+ var AWAIT_IF_FORM = "Write the if form: let x = null; if (<condition>) { x = await tool({...}); }";
25367
+ var AWAIT_IN_OPTIONAL_CHAIN = `An await after \`?.\` runs only when the value before it is not null. ${AWAIT_IF_FORM}`;
25368
+ var CALLBACK_HELPERS = /* @__PURE__ */ new Set([
25369
+ "filter",
25370
+ "find",
25371
+ "pluck",
25372
+ "sortBy",
25373
+ "sumBy",
25374
+ "meanBy",
25375
+ "minBy",
25376
+ "maxBy",
25377
+ "countBy",
25378
+ "groupBy",
25379
+ "uniqueBy",
25380
+ "some",
25381
+ "every",
25382
+ "differenceBy",
25383
+ "intersectionBy",
25384
+ "reduce"
25385
+ ]);
25386
+ var LAMBDA_POSITION_HINT = `Function expressions are not supported here. A lambda \u2014 either \`(x) => <expr>\` or \`function (x) { return <expr>; }\` \u2014 is legal only INLINE as a callback argument to ${[...CALLBACK_HELPERS].sort().join(" / ")}, e.g. pluck(rows, (r) => r.qty * r.price). To NAME reusable logic, declare it at the top level instead: \`function toLine(l = {}) { return { ... }; }\` \u2014 every parameter needs a default, and the body is inlined at each call site. Binding a lambda to a name (const f = (x) => ...) or calling one directly stays unsupported.`;
25387
+ var HELPER_ALIASES = /* @__PURE__ */ new Map([
25388
+ ["String", "toString"],
25389
+ ["Number", "toNumber"],
25390
+ ["parseFloat", "toNumber"],
25391
+ // faithful: toNumber IS parseFloat semantics
25392
+ ["toUpperCase", "upper"],
25393
+ ["toLowerCase", "lower"],
25394
+ ["map", "pluck"],
25395
+ // arr.map(x => …) ≡ pluck(arr, x => …)
25396
+ ["toFixed", "formatNumber"]
25397
+ // x.toFixed(2) ≡ formatNumber(x, 2)
25398
+ ]);
25399
+ var REJECTED_HELPERS = /* @__PURE__ */ new Map([
25400
+ [
25401
+ "parseInt",
25402
+ {
25403
+ message: "parseInt is not supported \u2014 parse with toNumber(x), then apply floor(...), ceil(...), or round(...) to get a whole number. floor rounds down (toward negative infinity), so for negative values choose the rounding deliberately.",
25404
+ construct: "parse_int"
25405
+ }
25406
+ ]
25407
+ ]);
25408
+ var NAMESPACE_HELPERS = /* @__PURE__ */ new Map([
25409
+ ["JSON", /* @__PURE__ */ new Map([["parse", "parseJson"], ["stringify", "toJson"]])],
25410
+ ["Array", /* @__PURE__ */ new Map([["isArray", "isArray"]])],
25411
+ ["Object", /* @__PURE__ */ new Map([["keys", "keys"], ["values", "values"], ["entries", "entries"]])],
25412
+ ["Number", /* @__PURE__ */ new Map([["parseFloat", "toNumber"]])]
25413
+ ]);
25414
+ var METHOD_FORM_NAMES = /* @__PURE__ */ new Set([
25415
+ // String.prototype
25416
+ "trim",
25417
+ "toUpperCase",
25418
+ "toLowerCase",
25419
+ "replace",
25420
+ "replaceAll",
25421
+ "split",
25422
+ "substring",
25423
+ "padStart",
25424
+ "padEnd",
25425
+ "startsWith",
25426
+ "endsWith",
25427
+ // String.prototype + Array.prototype
25428
+ "includes",
25429
+ "slice",
25430
+ "at",
25431
+ "concat",
25432
+ // Array.prototype
25433
+ "join",
25434
+ "reverse",
25435
+ "map",
25436
+ "filter",
25437
+ "find",
25438
+ "some",
25439
+ "every",
25440
+ "reduce",
25441
+ // Number.prototype / Object.prototype
25442
+ "toFixed",
25443
+ "toString"
25444
+ ]);
25445
+ var METHOD_FORM_MENU = [...METHOD_FORM_NAMES].sort().join(", ");
25446
+ var PROPERTY_HELPERS = /* @__PURE__ */ new Map([["length", "length"]]);
25447
+ var HELPER_SPELLINGS = (() => {
25448
+ const out = /* @__PURE__ */ new Map();
25449
+ const add = (helper, spelling) => {
25450
+ if (isHelper(helper) && !out.has(helper)) out.set(helper, spelling);
25451
+ };
25452
+ for (const name of METHOD_FORM_NAMES) add(HELPER_ALIASES.get(name) ?? name, { kind: "method", name });
25453
+ for (const [name, helper] of PROPERTY_HELPERS) add(helper, { kind: "property", name });
25454
+ for (const [namespace, members2] of NAMESPACE_HELPERS) {
25455
+ for (const [member, helper] of members2) add(helper, { kind: "namespace", namespace, member });
25456
+ }
25457
+ for (const fn of Object.keys(expression_math_exports)) {
25458
+ if (fn !== "random" && typeof Reflect.get(Math, fn) === "function") {
25459
+ add(fn, { kind: "namespace", namespace: "Math", member: fn });
25460
+ }
25461
+ }
25462
+ return out;
25463
+ })();
25464
+ var RESERVED_CALLABLE_NAMES = /* @__PURE__ */ new Set([
25465
+ ...REJECTED_HELPERS.keys(),
25466
+ ...NAMESPACE_HELPERS.keys(),
25467
+ "Math",
25468
+ "Boolean"
25469
+ ]);
25470
+ var LINKED_ARITY = `${LINKED_CALL}(...) takes exactly one argument \u2014 the read of the link field, e.g. ${LINKED_CALL}(record["fld_link"])[0] for one row or ${LINKED_CALL}(record["fld_link"]) for every row.`;
25471
+ var LINKED_ARGUMENT_SHAPE = `The argument of ${LINKED_CALL}(...) must END on the link field, because that field is what it follows \u2014 ${LINKED_CALL}(record["fld_link"])[0]. A value that names no field (a binding, a literal, a helper result) cannot say which link to follow; read the field on it: ${LINKED_CALL}(ids["fld_link"]).`;
25472
+ var LINKED_OPTIONAL_RESULT = `${LINKED_CALL}(...) always returns an array, never null, so \`?.\` on its result can never fire. The argument takes a guard \u2014 ${LINKED_CALL}(record?.["fld_link"])[0] \u2014 and so does the fetched row, which is null where the linked record is deleted or unreadable: ${LINKED_CALL}(record["fld_link"])[0]?.["fld_x"].`;
25473
+ var LINKED_METHOD_FORM = `${LINKED_CALL} has no method form \u2014 write ${LINKED_CALL}(record["fld_link"])[0], with the link read as the argument.`;
25474
+ var LINKED_ON_NON_RECORD = `${LINKED_CALL}(...) follows a link field on a record, and this value is not one. Read the record first, then follow the link: ${LINKED_CALL}(record["fld_link"])[0].`;
25475
+ var LINKED_ON_CHANGES = `A change diff is not a record, so ${LINKED_CALL}(...) cannot follow a link off it. Read the post-state with ${LINKED_CALL}(record["fld_link"]) or the pre-state with ${LINKED_CALL}(prev_record["fld_link"]).`;
25476
+ var READ_ROOT_HINT = `A read starts from a name: a reserved identifier (record, prev_record, changes, runtime, trigger, index), a binding, or ${LINKED_CALL}(record["fld_link"]). Bind the value first \u2014 const x = <expression>; \u2014 then read x.y.`;
25477
+
25329
25478
  // ../shared/src/chat_models.ts
25330
25479
  var MODEL_TIERS = ["haiku", "sonnet", "opus"];
25331
25480
  var TIER_MODELS = {
@@ -26973,36 +27122,46 @@ var WorkflowEvalError = class extends Error {
26973
27122
  detail;
26974
27123
  };
26975
27124
  var WorkflowTimeBudgetError = class extends Error {
26976
- constructor(subject, budgetMs) {
26977
- super(`${subject} exceeded its time budget of ${budgetMs / 1e3}s.`);
27125
+ /** `waitedFor`: what the run's deadline passed waiting on, when that was not the run's own work. */
27126
+ constructor(subject, budgetMs, waitedFor) {
27127
+ super(`${subject} exceeded its time budget of ${budgetMs / 1e3}s${waitedFor === void 0 ? "" : ` waiting for ${waitedFor}`}.`);
26978
27128
  this.name = "WorkflowTimeBudgetError";
26979
27129
  }
26980
27130
  };
26981
27131
  var WorkflowExpressionBudgetError = class extends Error {
26982
- constructor(exceeded) {
26983
- super(
26984
- exceeded === "nodes" ? `Expression evaluated more than ${MAX_EVALUATION_NODES} nodes. A callback nested in another runs once per pair of elements.` : `The run built more than ${MAX_RUN_BUILT_BYTES / 1024 / 1024}MB of arrays, objects and text. Build long text with join() rather than a fold of +.`
26985
- );
26986
- this.exceeded = exceeded;
27132
+ constructor(budget) {
27133
+ super(budgetMessage(budget));
27134
+ this.budget = budget;
26987
27135
  this.name = "WorkflowExpressionBudgetError";
26988
27136
  }
26989
- exceeded;
27137
+ budget;
26990
27138
  };
26991
- var MAX_EVALUATION_NODES = 2e7;
27139
+ function budgetMessage(budget) {
27140
+ switch (budget.exceeded) {
27141
+ case "bytes":
27142
+ return `The run built more than ${MAX_RUN_BUILT_BYTES / 1024 / 1024}MB of arrays, objects and text.`;
27143
+ case "heap":
27144
+ return `The expression used more than ${budget.heap_mb}MB of memory.`;
27145
+ case "compute":
27146
+ return `The expression computed for more than ${budget.compute_ms / 1e3}s.`;
27147
+ }
27148
+ }
26992
27149
  var SLOT_BYTES = 8;
26993
27150
  var CHAR_BYTES = 2;
27151
+ var OBJECT_BYTES = 56;
26994
27152
  var EVALUATION_SLICE_MS = 20;
26995
27153
  var turn = {
26996
27154
  slice_started_ms: performance.now(),
26997
27155
  pause: null
26998
27156
  };
26999
- function startRunBudget(subject, budgetMs) {
27000
- return { deadline_ms: Date.now() + budgetMs, budget_ms: budgetMs, subject, built_bytes: 0 };
27157
+ function startRunBudget(subject, budgetMs, path) {
27158
+ return { host: "shared", deadline_ms: Date.now() + budgetMs, budget_ms: budgetMs, subject, path, built_bytes: 0 };
27001
27159
  }
27002
27160
  function runClock(run2) {
27003
27161
  if (Date.now() > run2.deadline_ms) {
27004
27162
  throw new WorkflowTimeBudgetError(run2.subject, run2.budget_ms);
27005
27163
  }
27164
+ if (run2.host === "isolated") return null;
27006
27165
  if (turn.pause === null && performance.now() - turn.slice_started_ms >= EVALUATION_SLICE_MS) {
27007
27166
  turn.pause = new Promise((resolve) => setTimeout(resolve, 0)).then(() => {
27008
27167
  turn.pause = null;
@@ -27012,15 +27171,16 @@ function runClock(run2) {
27012
27171
  return turn.pause;
27013
27172
  }
27014
27173
  function chargeBuilt(run2, bytes) {
27174
+ if (run2.host === "isolated") return;
27015
27175
  run2.built_bytes += bytes;
27016
27176
  if (run2.built_bytes > MAX_RUN_BUILT_BYTES) {
27017
- throw new WorkflowExpressionBudgetError("bytes");
27177
+ throw new WorkflowExpressionBudgetError({ exceeded: "bytes" });
27018
27178
  }
27019
27179
  }
27020
27180
  function builtSize(value) {
27021
27181
  if (typeof value === "string") return value.length * CHAR_BYTES;
27022
- if (Array.isArray(value)) return value.length * SLOT_BYTES;
27023
- if (typeof value === "object" && value !== null) return Object.keys(value).length * SLOT_BYTES;
27182
+ if (Array.isArray(value)) return OBJECT_BYTES + value.length * SLOT_BYTES;
27183
+ if (typeof value === "object" && value !== null) return OBJECT_BYTES + Object.keys(value).length * SLOT_BYTES;
27024
27184
  return 0;
27025
27185
  }
27026
27186
  var PARSED_NODE_BYTES = 40;
@@ -27050,13 +27210,13 @@ function callbackWorkers() {
27050
27210
  };
27051
27211
  }
27052
27212
  function evaluateWorkflowExpression(expr2, ctx) {
27053
- const budget = { nodes: 0, workers: callbackWorkers(), settled: false };
27213
+ const budget = { workers: callbackWorkers(), settled: false };
27054
27214
  return evaluateExpression(expr2, { ...ctx, evaluation_budget: budget }).finally(() => {
27055
27215
  budget.settled = true;
27056
27216
  });
27057
27217
  }
27058
27218
  async function evaluateExpression(expr2, ctx) {
27059
- for (let pause = spendNode(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27219
+ for (let pause = checkClock(ctx); pause !== null; pause = checkClock(ctx)) await pause;
27060
27220
  switch (expr2.kind) {
27061
27221
  case "lit":
27062
27222
  return expr2.value;
@@ -27093,15 +27253,6 @@ async function evaluateExpression(expr2, ctx) {
27093
27253
  return assertNever(expr2);
27094
27254
  }
27095
27255
  }
27096
- function spendNode(ctx) {
27097
- const budget = ctx.evaluation_budget;
27098
- assertUnsettled(budget);
27099
- budget.nodes += 1;
27100
- if (budget.nodes > MAX_EVALUATION_NODES) {
27101
- throw new WorkflowExpressionBudgetError("nodes");
27102
- }
27103
- return runClock(ctx.run_budget);
27104
- }
27105
27256
  function checkClock(ctx) {
27106
27257
  assertUnsettled(ctx.evaluation_budget);
27107
27258
  return runClock(ctx.run_budget);
@@ -28093,9 +28244,6 @@ function clientRuntime(input) {
28093
28244
  },
28094
28245
  get change_origin() {
28095
28246
  return unread("change_origin");
28096
- },
28097
- get execution_principal() {
28098
- return unread("execution_principal");
28099
28247
  }
28100
28248
  };
28101
28249
  }
@@ -28128,7 +28276,7 @@ async function predictWorkflowEffects(input) {
28128
28276
  trigger_table: null,
28129
28277
  member_lookup: async () => unread("member display"),
28130
28278
  lexical_bindings: [{}],
28131
- run_budget: startRunBudget("Write prediction", PREDICTION_BUDGET_MS)
28279
+ run_budget: startRunBudget("Write prediction", PREDICTION_BUDGET_MS, "write")
28132
28280
  };
28133
28281
  const walk = { ctx, effects: [], written, input, unsure: false, aligned: true };
28134
28282
  try {
@@ -29066,41 +29214,6 @@ function useAgentRuns(sessionId, opts) {
29066
29214
  // src/queries.ts
29067
29215
  import { useCallback as useCallback5, useMemo as useMemo3, useState as useState3 } from "react";
29068
29216
 
29069
- // ../shared/src/schemas/read_row.ts
29070
- var RECORD_ID_COLUMN = "__source_record_id";
29071
-
29072
- // ../shared/src/schemas/app_query_aggregate.ts
29073
- var AGGREGATE_COUNT_COLUMN = "__count";
29074
- var AGGREGATE_SUM_COLUMN = "__sum";
29075
- var AGGREGATE_DAY_COLUMN = "__day";
29076
- function dayOf(cell) {
29077
- return typeof cell === "string" && cell !== "" ? cell.slice(0, 10) : null;
29078
- }
29079
- function aggregateRows(rows, aggregate) {
29080
- const by = aggregate.by ?? [];
29081
- const day = aggregate.day;
29082
- const sum2 = aggregate.sum;
29083
- const keyed = /* @__PURE__ */ new Map();
29084
- for (const row2 of rows) {
29085
- const key = JSON.stringify([...by.map((column) => row2[column] ?? null), ...day === void 0 ? [] : [dayOf(row2[day])]]);
29086
- keyed.set(key, [...keyed.get(key) ?? [], row2]);
29087
- }
29088
- return [...keyed.values()].map((members2) => {
29089
- const [first2] = members2;
29090
- const figures = members2.flatMap((member) => {
29091
- const value = sum2 === void 0 ? void 0 : member[sum2];
29092
- return typeof value === "number" ? [value] : [];
29093
- });
29094
- return {
29095
- [RECORD_ID_COLUMN]: null,
29096
- ...Object.fromEntries(by.map((column) => [column, first2?.[column] ?? null])),
29097
- ...day === void 0 ? {} : { [AGGREGATE_DAY_COLUMN]: dayOf(first2?.[day]) },
29098
- [AGGREGATE_COUNT_COLUMN]: members2.length,
29099
- ...sum2 === void 0 ? {} : { [AGGREGATE_SUM_COLUMN]: figures.length === 0 ? null : figures.reduce((total, value) => total + value, 0) }
29100
- };
29101
- });
29102
- }
29103
-
29104
29217
  // src/row.ts
29105
29218
  var PENDING = "__pending";
29106
29219
  function opt(v) {
package/dist/router.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isEmbedded,
3
3
  postHostNotification,
4
4
  setUrlParams
5
- } from "./chunk-CRJQYSDI.js";
5
+ } from "./chunk-EWCMZCKA.js";
6
6
 
7
7
  // src/router.tsx
8
8
  import { useEffect } from "react";
@@ -143,7 +143,7 @@ write it as `{ node_type: "condition", type: "record_id", operator, value }`.
143
143
  inline `{ status: "open" }` each render is the same key; you never need to memoize params.
144
144
  - **The cache survives unmount/remount**, and **arrival is a refresh event**: returning to a screen
145
145
  renders the cached rows instantly *and* re-reads them in the background, so a list reflects what
146
- another screen changed while you were away. Identical concurrent reads share one request.
146
+ another screen changed while you were away. Identical concurrent reads share one request, and reads asked within a few milliseconds of each other go to the server together, each answered or refused on its own.
147
147
  Freshness comes from four places — arrival, window focus / tab return / network reconnect
148
148
  (`revalidateOnFocus`, default on; a read asked within the last few seconds is not asked again),
149
149
  a **realtime push**, and **this app's own successful write**, which re-reads the mounted queries
package/docs/mutations.md CHANGED
@@ -397,9 +397,8 @@ Why a full-form snapshot save is a bug, not a style choice — three independent
397
397
  moment *any* field is touched — there is no value-equality pass (deep equality over stored
398
398
  shapes would be a silent lock bypass). A snapshot touches every field, so it turns every
399
399
  save against a locked record into a total rejection even when nothing changed.
400
- 2. **`before_update` table workflows fire on the write.** Tables can carry `before_update`
401
- workflows (autofills, validations, side effects). They run on every write that touches the
402
- record — a snapshot save fires them for fields the user never edited.
400
+ 2. **A table's checks and `after_update` workflows run on the write.** They run on every
401
+ write that touches the record — a snapshot save runs them for fields the user never edited.
403
402
  3. **Snapshots clobber concurrent edits.** An update writes exactly the fields present in the
404
403
  payload; unsent fields keep their current value. A diff therefore coexists with a
405
404
  colleague's concurrent edit to a *different* field — a snapshot overwrites it with the
package/docs/workflows.md CHANGED
@@ -344,8 +344,8 @@ try {
344
344
 
345
345
  `e` is shaped `{ message, type, step_id?, detail? }`. Caught: tool errors and expression errors
346
346
  thrown inside the body. **Not caught:** `validate` failures — a check that throws included — and
347
- `return` (control flow), the run's own bounds (its time, its step count, and the node and 512 MB
348
- bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
347
+ `return` (control flow), the run's own bounds (its time, its step count, and the compute and
348
+ memory bounds in *Traps*), and errors from steps that run on *resume* after a `wait` inside the body —
349
349
  the wrapper spans one execution pass. There is no `finally`: put always-run steps after the `try`,
350
350
  always-on-failure steps in the `catch`.
351
351
 
@@ -611,7 +611,7 @@ key preserves. So passing a possibly-null read straight through is safe. Use
611
611
  `set_skip_null` is what turns a bag of optional workflow inputs into a diff-write without a guard
612
612
  per field; naming the same field in both `set` and `set_skip_null` is an error. A call whose
613
613
  surfaces all resolve to nothing — an all-dropped `set_skip_null`, an op object with no entries —
614
- is a no-op: no records touched, no `before_update` hooks. So a conditionally-built op object needs
614
+ is a no-op: no records touched, no table check asked. So a conditionally-built op object needs
615
615
  no guard around the call. Why diffs and not snapshots, plus locked records and the
616
616
  `request_locked_record_change` path: [mutations](./mutations.md#diff-before-update--send-only-what-changed).
617
617
 
@@ -791,9 +791,6 @@ The rules that are easy to get wrong because the failing code looks correct.
791
791
  init/update included. Resume re-enters *after* the whole loop, so a wait inside would run
792
792
  iteration 0 and silently skip every remaining iteration and its mutations. Move the wait out,
793
793
  or split the loop into separately triggered runs.
794
- - **Waits and `agent` steps are disallowed in `before_*` table workflows.** Those run inside the
795
- mutation pipeline and must complete synchronously; an LLM loop cannot return an accept/reject
796
- verdict in time.
797
794
  - **String contexts coerce to display text.** A read inside a template or a string-typed argument
798
795
  renders the *label*; everywhere else it is the raw key. So
799
796
  `` `Status: ${record["fld_status"]}` `` prints "Done" while
@@ -827,11 +824,14 @@ The rules that are easy to get wrong because the failing code looks correct.
827
824
  takes an array of at most 100,000. `join`, `replaceAll`, `padStart`, `padEnd`, `toString`,
828
825
  `toJson` and a template string write at most 10,000,000 characters; `randomNumber` /
829
826
  `randomAlphaNumeric` take a length of at most 1,000. Past one of those the call throws an
830
- expression error, which `try`/`catch` catches, and so does a step whose result would take what
831
- the run keeps past 67,108,864 characters of JSON. One evaluation runs at most 20,000,000 nodes —
832
- a callback nested in another runs once per pair of elements — and the run's expressions build at
833
- most 512 MB of arrays, objects and text in all, every temporary counted: past either, the run
834
- fails whatever catches it. Build long text with `join`, never a fold of `+`.
827
+ expression error, which `try`/`catch` catches, and so does an expression whose value would write
828
+ more than 67,108,864 characters of JSON, or a step whose result would take what the run keeps
829
+ past that. One evaluation computes for at most 6 seconds — waiting on a linked row, a member's
830
+ name or `current_member_in_any_group` does not count — and uses at most 256 MB of memory (512 MB
831
+ on a private instance with 4 GB or more). The values a run's expressions answer come to at most
832
+ 512 MB in all, counted each time one is answered, a text at every place the value holds it.
833
+ Past any of these, the run fails whatever catches it. A callback nested in another runs once
834
+ per pair of elements.
835
835
 
836
836
  ## The bright line
837
837
 
@@ -860,8 +860,7 @@ genuinely recursive.
860
860
  `set_app_workflow` with `verify_only: true` gives the verdict a save would, and writes nothing. The
861
861
  server runs every check a save runs — the subset parse, the type pass against the alias's
862
862
  declared inputs, tool and kwarg resolution, field, option, table and group resolution, `switch`
863
- case validation, the literal `formatDate` probe, waits in loops, the `before_*` restrictions,
864
- table reach, the `field_key` rule and the published-API guard. **Green means the save will take
863
+ case validation, the literal `formatDate` probe, waits in loops, table reach, the `field_key` rule and the published-API guard. **Green means the save will take
865
864
  it, red means real** — do not push through a red check. Lint comes back as warnings, which do not
866
865
  fail it.
867
866
 
@@ -900,7 +899,7 @@ check's message joined with `"; "`, and one `field_errors` entry per failing che
900
899
  iterations of a `while` / `do_while` / `c_for`, 10 000 steps, 50 live reads, and 20 seconds — and
901
900
  tripping any of them appends an entry naming the bound and ending "the plan below is incomplete",
902
901
  then stops. It also stops, with an entry saying why, where the live run would fail: at a
903
- `validate` check that throws, an evaluation past the run's node or 512 MB bound (*Traps*), or a
902
+ `validate` check that throws, an evaluation past the run's compute or memory bounds (*Traps*), or a
904
903
  return too long to write.
905
904
  So read `evaluation_errors` before reading `planned_calls`: an entry there may be a
906
905
  truncated plan rather than a bug in your body. A `foreach` is deliberately NOT capped at 1 000 —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.113.1",
3
+ "version": "0.115.0",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through \u2014 typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {