@abloatai/ablo 0.16.3 → 0.18.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/CHANGELOG.md +67 -0
- package/dist/BaseSyncedStore.d.ts +8 -5
- package/dist/BaseSyncedStore.js +65 -18
- package/dist/NetworkMonitor.js +3 -1
- package/dist/cli.cjs +572 -256
- package/dist/client/Ablo.d.ts +33 -1
- package/dist/client/Ablo.js +42 -12
- package/dist/client/auth.js +1 -1
- package/dist/core/StoreManager.js +3 -1
- package/dist/errors.d.ts +9 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/policy/types.d.ts +24 -9
- package/dist/policy/types.js +32 -11
- package/dist/schema/diff.d.ts +14 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +2 -0
- package/dist/sync/SyncWebSocket.js +1 -1
- package/dist/sync/awaitClaimGrant.js +9 -2
- package/dist/sync/createClaimStream.js +32 -0
- package/dist/wire/errorEnvelope.d.ts +13 -0
- package/docs/data-sources.md +164 -287
- package/docs/quickstart.md +51 -63
- package/package.json +1 -1
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -133,6 +133,22 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
133
133
|
* @default 'websocket'
|
|
134
134
|
*/
|
|
135
135
|
transport?: 'websocket' | 'http' | undefined;
|
|
136
|
+
/**
|
|
137
|
+
* Turn Ablo's diagnostic logging on/off. `true` surfaces the `[Ablo]`
|
|
138
|
+
* coordination trace — claims requested / queued / granted / released, agent
|
|
139
|
+
* handovers, connection state — so you can SEE the human+agent coordination
|
|
140
|
+
* you built while debugging. Omitted/`false` keeps the quiet default (only
|
|
141
|
+
* warnings + errors). For a middle ground use {@link logLevel}. Env override:
|
|
142
|
+
* `ABLO_LOG_LEVEL`. Ignored if a custom logger is supplied.
|
|
143
|
+
*/
|
|
144
|
+
debug?: boolean | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* Log threshold for the default `[Ablo]` logger (takes precedence over
|
|
147
|
+
* {@link debug}). `'info'` = coordination + connection events without the
|
|
148
|
+
* per-model registration firehose; `'debug'` = everything; `'warn'` (default)
|
|
149
|
+
* = warnings + errors only; `'silent'` = nothing. Env override: `ABLO_LOG_LEVEL`.
|
|
150
|
+
*/
|
|
151
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent' | undefined;
|
|
136
152
|
/**
|
|
137
153
|
* Bearer auth token. Hosted-cloud consumers pass `apiKey`; self-hosted
|
|
138
154
|
* deployments may pass a bearer token minted by their own auth layer.
|
|
@@ -242,8 +258,24 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
242
258
|
* `apiKey` only; the server handles capability issuance.
|
|
243
259
|
*/
|
|
244
260
|
capabilityToken?: string;
|
|
245
|
-
/** Custom logger (default: console) */
|
|
261
|
+
/** Custom logger (default: console). Supplying one bypasses {@link debug}/{@link logLevel}. */
|
|
246
262
|
logger?: SyncLogger;
|
|
263
|
+
/**
|
|
264
|
+
* Turn Ablo's diagnostic logging on/off. `true` surfaces the `[Ablo]`
|
|
265
|
+
* coordination trace — claims acquired / queued / granted / released, agent
|
|
266
|
+
* handovers, connection state — plus internal lifecycle, so you can SEE the
|
|
267
|
+
* human+agent coordination you built. Omitted/`false` keeps the quiet default
|
|
268
|
+
* (only warnings + errors). For a middle ground use {@link logLevel}.
|
|
269
|
+
* Env override: `ABLO_LOG_LEVEL`. Ignored if a custom {@link logger} is passed.
|
|
270
|
+
*/
|
|
271
|
+
debug?: boolean;
|
|
272
|
+
/**
|
|
273
|
+
* Log threshold for the default `[Ablo]` logger (takes precedence over
|
|
274
|
+
* {@link debug}). `'info'` = coordination + connection events without the
|
|
275
|
+
* per-model registration firehose; `'debug'` = everything; `'warn'` (default)
|
|
276
|
+
* = warnings + errors only; `'silent'` = nothing. Env override: `ABLO_LOG_LEVEL`.
|
|
277
|
+
*/
|
|
278
|
+
logLevel?: 'debug' | 'info' | 'warn' | 'error' | 'silent';
|
|
247
279
|
/** ObjectPool size limit (default: 10000) */
|
|
248
280
|
maxPoolSize?: number;
|
|
249
281
|
/**
|
package/dist/client/Ablo.js
CHANGED
|
@@ -518,17 +518,44 @@ function createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed,
|
|
|
518
518
|
}
|
|
519
519
|
return ModelClass;
|
|
520
520
|
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
521
|
+
const LOG_LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40, silent: 99 };
|
|
522
|
+
/**
|
|
523
|
+
* Resolve the effective level. Precedence: explicit `logLevel` option →
|
|
524
|
+
* `debug: true` (⇒ debug) → `ABLO_LOG_LEVEL` env → default `warn`. `debug: false`
|
|
525
|
+
* / omitted just means "don't raise the level" — it falls through to env/default
|
|
526
|
+
* rather than force-silencing an ops-set env override.
|
|
527
|
+
*/
|
|
528
|
+
function resolveLogLevel(opts) {
|
|
529
|
+
if (opts?.logLevel && opts.logLevel in LOG_LEVEL_RANK)
|
|
530
|
+
return opts.logLevel;
|
|
531
|
+
if (opts?.debug === true)
|
|
532
|
+
return 'debug';
|
|
533
|
+
// `globalThis.process` guard keeps this safe in browser/edge runtimes that
|
|
534
|
+
// have no `process` binding — there we fall through to the default.
|
|
535
|
+
const raw = globalThis.process?.env?.ABLO_LOG_LEVEL;
|
|
536
|
+
const normalized = raw?.toLowerCase();
|
|
537
|
+
if (normalized && normalized in LOG_LEVEL_RANK)
|
|
538
|
+
return normalized;
|
|
539
|
+
return 'warn';
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Build the default logger, gated at `level` and prefixed `[Ablo]` so a creator
|
|
543
|
+
* with a console full of other tools' logs can see at a glance what's ours.
|
|
544
|
+
*/
|
|
545
|
+
function createConsoleLogger(level) {
|
|
546
|
+
const threshold = LOG_LEVEL_RANK[level];
|
|
547
|
+
const emit = (lvl, fn, args) => {
|
|
548
|
+
if (typeof console === 'undefined' || LOG_LEVEL_RANK[lvl] < threshold)
|
|
549
|
+
return;
|
|
550
|
+
fn('[Ablo]', ...args);
|
|
551
|
+
};
|
|
552
|
+
return {
|
|
553
|
+
debug: (...args) => emit('debug', console.debug, args),
|
|
554
|
+
info: (...args) => emit('info', console.info, args),
|
|
555
|
+
warn: (...args) => emit('warn', console.warn, args),
|
|
556
|
+
error: (...args) => emit('error', console.error, args),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
532
559
|
// `readProcessEnv` lives in `./auth` alongside the other resolvers
|
|
533
560
|
// that read it. Re-exported there for use elsewhere in the file.
|
|
534
561
|
// ── Default mutation executor (wire: `commit` frame over WebSocket) ──────
|
|
@@ -712,7 +739,10 @@ export function Ablo(options) {
|
|
|
712
739
|
databaseUrl: configuredDatabaseUrl,
|
|
713
740
|
dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
|
|
714
741
|
});
|
|
715
|
-
|
|
742
|
+
// Custom logger wins; otherwise build the default `[Ablo]` logger at the level
|
|
743
|
+
// resolved from `debug`/`logLevel`/`ABLO_LOG_LEVEL` (default `warn`).
|
|
744
|
+
const logger = internalOptions.logger ??
|
|
745
|
+
createConsoleLogger(resolveLogLevel({ debug: options.debug, logLevel: options.logLevel }));
|
|
716
746
|
// Nudge (once) if a stray DATABASE_URL is in the env but `databaseUrl` wasn't
|
|
717
747
|
// passed — the env value is no longer auto-adopted (see resolveDatabaseUrl).
|
|
718
748
|
warnIfDatabaseUrlEnvIgnored(authInput, (m) => logger.warn(m));
|
package/dist/client/auth.js
CHANGED
|
@@ -83,7 +83,7 @@ export function warnIfDatabaseUrlEnvIgnored(input, warn) {
|
|
|
83
83
|
if (warn)
|
|
84
84
|
warn(message);
|
|
85
85
|
else if (typeof console !== 'undefined')
|
|
86
|
-
console.warn('[
|
|
86
|
+
console.warn('[Ablo]', message);
|
|
87
87
|
}
|
|
88
88
|
export const ABLO_HOSTED_API_DOMAIN = 'api.abloatai.com';
|
|
89
89
|
export const ABLO_HOSTED_HTTP_BASE_URL = `https://${ABLO_HOSTED_API_DOMAIN}`;
|
|
@@ -274,7 +274,9 @@ export class StoreManager {
|
|
|
274
274
|
* Clear all stores
|
|
275
275
|
*/
|
|
276
276
|
async clearAllStores() {
|
|
277
|
-
|
|
277
|
+
// Lifecycle chatter (logout / identity switch / reset) — NOT a warning.
|
|
278
|
+
// `debug` so it's silent under the default `warn` threshold.
|
|
279
|
+
getContext().logger.debug('Clearing all stores');
|
|
278
280
|
const promises = Array.from(this.stores.values()).map((store) => store.clear());
|
|
279
281
|
await Promise.all(promises);
|
|
280
282
|
getContext().logger.info('All stores cleared');
|
package/dist/errors.d.ts
CHANGED
|
@@ -77,6 +77,15 @@ export declare class AbloError extends Error {
|
|
|
77
77
|
request_id?: string;
|
|
78
78
|
[key: string]: unknown;
|
|
79
79
|
};
|
|
80
|
+
/**
|
|
81
|
+
* A single, leak-proof line for logs and `String(err)` / template
|
|
82
|
+
* interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
|
|
83
|
+
*
|
|
84
|
+
* Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
|
|
85
|
+
* makes `console.error(richError)` an unreadable wall of text. The structured
|
|
86
|
+
* payload stays available via {@link toJSON}; this is the human one-liner.
|
|
87
|
+
*/
|
|
88
|
+
toString(): string;
|
|
80
89
|
}
|
|
81
90
|
/**
|
|
82
91
|
* Map a stable error `code` to its docs URL — the one place the convention
|
package/dist/errors.js
CHANGED
|
@@ -91,6 +91,20 @@ export class AbloError extends Error {
|
|
|
91
91
|
...(this.details ?? {}),
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* A single, leak-proof line for logs and `String(err)` / template
|
|
96
|
+
* interpolation: `AbloValidationError [code]: message (see docs) [request_id: …]`.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately does NOT dump `details`, `cause`, or the stack — the thing that
|
|
99
|
+
* makes `console.error(richError)` an unreadable wall of text. The structured
|
|
100
|
+
* payload stays available via {@link toJSON}; this is the human one-liner.
|
|
101
|
+
*/
|
|
102
|
+
toString() {
|
|
103
|
+
const code = this.code ? ` [${this.code}]` : '';
|
|
104
|
+
const docs = this.docUrl ? ` (see ${this.docUrl})` : '';
|
|
105
|
+
const req = this.requestId ? ` [request_id: ${this.requestId}]` : '';
|
|
106
|
+
return `${this.name}${code}: ${this.message}${docs}${req}`;
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
109
|
/**
|
|
96
110
|
* Map a stable error `code` to its docs URL — the one place the convention
|
package/dist/index.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '.
|
|
|
62
62
|
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
63
63
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
64
64
|
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errors.js';
|
|
65
|
+
export { errorEnvelope, statusForType } from './wire/errorEnvelope.js';
|
|
66
|
+
export type { ErrorEnvelope } from './wire/errorEnvelope.js';
|
|
65
67
|
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
66
68
|
export { ENVIRONMENTS, environmentSchema, normalizeEnvironment, environmentFromKeyPrefix, environmentToKeyPrefix, isSandboxEnvironment, } from './environment.js';
|
|
67
69
|
export type { Environment, KeyPrefixEnvironment } from './environment.js';
|
package/dist/index.js
CHANGED
|
@@ -85,6 +85,11 @@ export { defaultPolicy, capabilityPreemptPolicy, interpretConflictAxis } from '.
|
|
|
85
85
|
// consumers need to discriminate failures (`e instanceof AbloX` or
|
|
86
86
|
// `e.type === 'AbloX'`) plus the HTTP-response translator.
|
|
87
87
|
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloNotFoundError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, hasWireCode, errorFromWire, toAbloError, ERROR_CODES, ERROR_CONTRACT_VERSION, errorCodeSpec, isRetryableCode, classifyRecovery, recoveryClassSchema, RECOVERY_CLASSES, } from './errors.js';
|
|
88
|
+
// Canonical wire-egress contract (dependency-free): the error envelope shape +
|
|
89
|
+
// the AbloError-subclass→HTTP-status table. Re-exported so server consumers
|
|
90
|
+
// (e.g. apps/sync-server, which keeps its own self-contained copy) can assert
|
|
91
|
+
// against the ONE source instead of silently drifting. See wire/errorEnvelope.ts.
|
|
92
|
+
export { errorEnvelope, statusForType } from './wire/errorEnvelope.js';
|
|
88
93
|
export { WS_BEARER_SUBPROTOCOL_PREFIX, WS_SYNC_SUBPROTOCOL } from './auth/credentialSource.js';
|
|
89
94
|
export { ENVIRONMENTS, environmentSchema, normalizeEnvironment, environmentFromKeyPrefix, environmentToKeyPrefix, isSandboxEnvironment, } from './environment.js';
|
|
90
95
|
// THE write-options contract — the one Zod schema for the option bag every
|
package/dist/policy/types.d.ts
CHANGED
|
@@ -122,18 +122,32 @@ export type ConflictDecision = {
|
|
|
122
122
|
*/
|
|
123
123
|
export type ConflictPolicy = (conflict: Conflict) => ConflictDecision | Promise<ConflictDecision>;
|
|
124
124
|
/**
|
|
125
|
-
* Default policy
|
|
125
|
+
* Default policy — **Law 7 (the human is the principal) is the engine-wide
|
|
126
|
+
* default, not an opt-in.**
|
|
126
127
|
*
|
|
127
|
-
* `claim_held`
|
|
128
|
-
*
|
|
129
|
-
*
|
|
128
|
+
* A `claim_held` conflict resolves by the COMMITTER's kind. This default is, by
|
|
129
|
+
* construction, identical to `coordination(humansOverwrite(), agentsReject())`
|
|
130
|
+
* applied to every model — so a developer who wants different behaviour just
|
|
131
|
+
* declares the conflict axis on that model (`humansReject()`, `humansNotify()`,
|
|
132
|
+
* …) and it wins. The default and the override speak the same vocabulary; the
|
|
133
|
+
* default is the one you get for free.
|
|
134
|
+
*
|
|
135
|
+
* • `user` → `allow` — a human is never blocked by a claim (a claim is a
|
|
136
|
+
* coordination hint among agents, not a lock on people)
|
|
137
|
+
* • `system` → `allow` — automation principals at the root of a trust chain
|
|
138
|
+
* behave like humans here (matches `mayBypassClaims`)
|
|
139
|
+
* • `agent` → `reject` — an agent yields to a foreign claim (the no-bypass
|
|
140
|
+
* invariant; the ONLY sanctioned agent override is the
|
|
141
|
+
* privileged `claim.preempt` capability seam, see
|
|
142
|
+
* {@link capabilityPreemptPolicy})
|
|
143
|
+
*
|
|
144
|
+
* `stale_context` conflicts honor the committer's declared `onStale` intent:
|
|
130
145
|
*
|
|
131
146
|
* • `'notify'` → notify + hold (op withheld; the actor resolves)
|
|
132
147
|
* • anything else (incl. `'reject'`, absent) → reject
|
|
133
148
|
*
|
|
134
|
-
* `'overwrite'` never reaches a policy — it's a hard opt-out
|
|
135
|
-
* detection.
|
|
136
|
-
* don't opt into `notify`.
|
|
149
|
+
* `'overwrite'` never reaches a policy on the stale path — it's a hard opt-out
|
|
150
|
+
* resolved before detection.
|
|
137
151
|
*/
|
|
138
152
|
export declare const defaultPolicy: (conflict: Conflict) => ConflictDecision;
|
|
139
153
|
/**
|
|
@@ -172,8 +186,9 @@ export interface ConflictAxis {
|
|
|
172
186
|
* synchronous (no I/O), so it runs on either side of the schema-agnostic
|
|
173
187
|
* boundary. Looks up the committer's kind and maps the declared `OnStaleMode`:
|
|
174
188
|
*
|
|
175
|
-
* - undefined → the engine default (`defaultPolicy`:
|
|
176
|
-
*
|
|
189
|
+
* - undefined → the engine default (`defaultPolicy`: Law 7 — a human/system
|
|
190
|
+
* committer is allowed (never blocked), an agent rejects on a
|
|
191
|
+
* `claim_held`; on a stale write, honor `onStale: 'notify'`)
|
|
177
192
|
* - `overwrite` → `allow` — the write wins / the committer is never blocked
|
|
178
193
|
* - `reject` → `reject` — the committer yields
|
|
179
194
|
* - `notify` → on `stale_context`: notify + hold (re-read & re-apply);
|
package/dist/policy/types.js
CHANGED
|
@@ -7,18 +7,32 @@
|
|
|
7
7
|
* Adding new shapes is additive on the discriminated union.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
|
-
* Default policy
|
|
10
|
+
* Default policy — **Law 7 (the human is the principal) is the engine-wide
|
|
11
|
+
* default, not an opt-in.**
|
|
11
12
|
*
|
|
12
|
-
* `claim_held`
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* A `claim_held` conflict resolves by the COMMITTER's kind. This default is, by
|
|
14
|
+
* construction, identical to `coordination(humansOverwrite(), agentsReject())`
|
|
15
|
+
* applied to every model — so a developer who wants different behaviour just
|
|
16
|
+
* declares the conflict axis on that model (`humansReject()`, `humansNotify()`,
|
|
17
|
+
* …) and it wins. The default and the override speak the same vocabulary; the
|
|
18
|
+
* default is the one you get for free.
|
|
19
|
+
*
|
|
20
|
+
* • `user` → `allow` — a human is never blocked by a claim (a claim is a
|
|
21
|
+
* coordination hint among agents, not a lock on people)
|
|
22
|
+
* • `system` → `allow` — automation principals at the root of a trust chain
|
|
23
|
+
* behave like humans here (matches `mayBypassClaims`)
|
|
24
|
+
* • `agent` → `reject` — an agent yields to a foreign claim (the no-bypass
|
|
25
|
+
* invariant; the ONLY sanctioned agent override is the
|
|
26
|
+
* privileged `claim.preempt` capability seam, see
|
|
27
|
+
* {@link capabilityPreemptPolicy})
|
|
28
|
+
*
|
|
29
|
+
* `stale_context` conflicts honor the committer's declared `onStale` intent:
|
|
15
30
|
*
|
|
16
31
|
* • `'notify'` → notify + hold (op withheld; the actor resolves)
|
|
17
32
|
* • anything else (incl. `'reject'`, absent) → reject
|
|
18
33
|
*
|
|
19
|
-
* `'overwrite'` never reaches a policy — it's a hard opt-out
|
|
20
|
-
* detection.
|
|
21
|
-
* don't opt into `notify`.
|
|
34
|
+
* `'overwrite'` never reaches a policy on the stale path — it's a hard opt-out
|
|
35
|
+
* resolved before detection.
|
|
22
36
|
*/
|
|
23
37
|
// Typed by its real SYNCHRONOUS shape (via `satisfies`) rather than the
|
|
24
38
|
// async-permissive `ConflictPolicy` alias, so sync callers like
|
|
@@ -26,8 +40,14 @@
|
|
|
26
40
|
// `ConflictDecision` back (not `… | Promise<…>`). Still assignable to
|
|
27
41
|
// `ConflictPolicy` everywhere it's used as a policy.
|
|
28
42
|
export const defaultPolicy = ((conflict) => {
|
|
29
|
-
if (conflict.kind
|
|
30
|
-
|
|
43
|
+
if (conflict.kind === 'claim_held') {
|
|
44
|
+
// Law 7 universal default: principals (human / system) are never blocked;
|
|
45
|
+
// agents yield. Keeping `agent → reject` here is what makes the no-bypass
|
|
46
|
+
// invariant hold even on the registry/default resolution path (which, unlike
|
|
47
|
+
// the declared-axis path, has no separate agent-degradation guard).
|
|
48
|
+
return conflict.committer.kind === 'agent'
|
|
49
|
+
? { action: 'reject', reason: 'claim_conflict' }
|
|
50
|
+
: { action: 'allow', note: 'principal:not-blocked' };
|
|
31
51
|
}
|
|
32
52
|
return conflict.requestedMode === 'notify'
|
|
33
53
|
? { action: 'notify', reason: 'stale_notify_hold' }
|
|
@@ -53,8 +73,9 @@ export const capabilityPreemptPolicy = (conflict) => {
|
|
|
53
73
|
* synchronous (no I/O), so it runs on either side of the schema-agnostic
|
|
54
74
|
* boundary. Looks up the committer's kind and maps the declared `OnStaleMode`:
|
|
55
75
|
*
|
|
56
|
-
* - undefined → the engine default (`defaultPolicy`:
|
|
57
|
-
*
|
|
76
|
+
* - undefined → the engine default (`defaultPolicy`: Law 7 — a human/system
|
|
77
|
+
* committer is allowed (never blocked), an agent rejects on a
|
|
78
|
+
* `claim_held`; on a stale write, honor `onStale: 'notify'`)
|
|
58
79
|
* - `overwrite` → `allow` — the write wins / the committer is never blocked
|
|
59
80
|
* - `reject` → `reject` — the committer yields
|
|
60
81
|
* - `notify` → on `stale_context`: notify + hold (re-read & re-apply);
|
package/dist/schema/diff.d.ts
CHANGED
|
@@ -133,6 +133,20 @@ export interface MigrationSignal {
|
|
|
133
133
|
readonly model: string;
|
|
134
134
|
readonly field?: string;
|
|
135
135
|
readonly detail: string;
|
|
136
|
+
/**
|
|
137
|
+
* Reader-visibility context for a removal that shadows an existing artifact:
|
|
138
|
+
* the active schema this push is being diffed against. Lets the CLI show the
|
|
139
|
+
* baseline — version + WHEN it was pushed — so "incompatible" isn't a mystery
|
|
140
|
+
* (e.g. a first sandbox push diffed against a months-old production schema).
|
|
141
|
+
*/
|
|
142
|
+
readonly shadowed?: {
|
|
143
|
+
readonly environment: string;
|
|
144
|
+
readonly version: number;
|
|
145
|
+
/** ISO timestamp the shadowed artifact was activated/pushed, or null. */
|
|
146
|
+
readonly pushedAt: string | null;
|
|
147
|
+
/** Who pushed it (e.g. `apikey:…`), or null. */
|
|
148
|
+
readonly pushedBy: string | null;
|
|
149
|
+
};
|
|
136
150
|
}
|
|
137
151
|
export interface MigrationClassification {
|
|
138
152
|
/** Execute but may lose or risk data on a non-empty table. */
|
package/dist/surface.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare const PUBLIC_MODEL_VERBS: readonly ["retrieve", "list", "get", "g
|
|
|
23
23
|
export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
|
|
24
24
|
/** Public keys of `AbloOptions`. `schema` is required; the rest are optional
|
|
25
25
|
* (the locked happy path is `Ablo({ schema, apiKey, databaseUrl, transport })`). */
|
|
26
|
-
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "databaseUrl", "persistence", "transport", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
26
|
+
export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "databaseUrl", "persistence", "transport", "debug", "logLevel", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser"];
|
|
27
27
|
export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
|
|
28
28
|
export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
|
|
29
29
|
export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
|
package/dist/surface.js
CHANGED
|
@@ -177,7 +177,7 @@ export class SyncWebSocket extends EventEmitter {
|
|
|
177
177
|
// which is unreliable but the only signal available; in Node it returns true
|
|
178
178
|
// (assume online) so the sidecar/agent path doesn't short-circuit here.
|
|
179
179
|
if (!getContext().onlineStatus.isOnline()) {
|
|
180
|
-
getContext().logger.
|
|
180
|
+
getContext().logger.debug('onlineStatus reports offline, but attempting connection anyway');
|
|
181
181
|
}
|
|
182
182
|
this.isConnecting = true;
|
|
183
183
|
this.isManualClose = false;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* fake; `SyncWebSocket` satisfies it structurally.
|
|
14
14
|
*/
|
|
15
15
|
import { AbloClaimedError, formatClaimedErrorMessage, claimTargetLabel, } from '../errors.js';
|
|
16
|
+
import { getContext } from '../context.js';
|
|
16
17
|
export function awaitClaimGrant(transport, claimId, options) {
|
|
17
18
|
return new Promise((resolve, reject) => {
|
|
18
19
|
const unsubs = [];
|
|
@@ -28,12 +29,18 @@ export function awaitClaimGrant(transport, claimId, options) {
|
|
|
28
29
|
// we waited in line, and reached the head → `claim_granted`. Either frame
|
|
29
30
|
// means the lease is now ours; `waited` records which path it was.
|
|
30
31
|
unsubs.push(transport.subscribe('claim_acquired', (p) => {
|
|
31
|
-
if (p?.claimId === claimId)
|
|
32
|
+
if (p?.claimId === claimId) {
|
|
33
|
+
getContext().logger.debug(`claim: acquired ${claimId} (target was free)`);
|
|
32
34
|
settle(() => resolve({ waited: false }));
|
|
35
|
+
}
|
|
33
36
|
}));
|
|
34
37
|
unsubs.push(transport.subscribe('claim_granted', (p) => {
|
|
35
|
-
if (p?.claimId === claimId)
|
|
38
|
+
if (p?.claimId === claimId) {
|
|
39
|
+
// Promoted to the head of the line — the creator's "it's the agent's
|
|
40
|
+
// turn now" moment after waiting behind a holder.
|
|
41
|
+
getContext().logger.info(`claim: granted ${claimId} — your turn (waited in queue)`);
|
|
36
42
|
settle(() => resolve({ waited: true }));
|
|
43
|
+
}
|
|
37
44
|
}));
|
|
38
45
|
if (options?.maxQueueDepth !== undefined) {
|
|
39
46
|
const max = options.maxQueueDepth;
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
import { asyncIteratorFrom } from '../utils/asyncIterator.js';
|
|
25
25
|
import { toMs } from '../utils/duration.js';
|
|
26
26
|
import { descriptionFromMeta, participantKindFromWire, } from '../coordination/schema.js';
|
|
27
|
+
import { getContext } from '../context.js';
|
|
28
|
+
/** Readable target for the coordination trace: `documents:abc` / `documents:abc.title`. */
|
|
29
|
+
function claimLabel(type, id, field) {
|
|
30
|
+
return field ? `${type}:${id}.${field}` : `${type}:${id}`;
|
|
31
|
+
}
|
|
27
32
|
export function createClaimStream(config, transport = null) {
|
|
28
33
|
const { participantId } = config;
|
|
29
34
|
// ── State: others' open claims, keyed by claimId ───────────────
|
|
@@ -37,6 +42,9 @@ export function createClaimStream(config, transport = null) {
|
|
|
37
42
|
const queueByEntity = new Map();
|
|
38
43
|
const entityKey = (type, id) => `${type}:${id}`;
|
|
39
44
|
const EMPTY_QUEUE = Object.freeze([]);
|
|
45
|
+
// Last queue position we logged per own-claim, so advancing in line is traced
|
|
46
|
+
// once per change (not re-logged on every server re-fan of the same line).
|
|
47
|
+
const lastLoggedQueuePos = new Map();
|
|
40
48
|
// ── Subscribers ──────────────────────────────────────────────────
|
|
41
49
|
const listeners = new Set();
|
|
42
50
|
const rejectionListeners = new Set();
|
|
@@ -122,6 +130,12 @@ export function createClaimStream(config, transport = null) {
|
|
|
122
130
|
unsubs.push(t.subscribe('claim_rejected', (rejection) => {
|
|
123
131
|
if (!rejection.claimId)
|
|
124
132
|
return;
|
|
133
|
+
if (ownClaims.has(rejection.claimId)) {
|
|
134
|
+
const tgt = rejection.target
|
|
135
|
+
? claimLabel(rejection.target.entityType, rejection.target.entityId, rejection.target.field)
|
|
136
|
+
: rejection.claimId;
|
|
137
|
+
getContext().logger.info(`claim: rejected ${tgt}${rejection.heldBy ? ` — held by ${rejection.heldBy}` : ''}`, { claimId: rejection.claimId, reason: rejection.reason });
|
|
138
|
+
}
|
|
125
139
|
// Drop the rejected own-claim so reconnect doesn't re-announce
|
|
126
140
|
// a claim the server already rejected (would just spam both
|
|
127
141
|
// sides with conflicts).
|
|
@@ -141,6 +155,10 @@ export function createClaimStream(config, transport = null) {
|
|
|
141
155
|
const lost = payload;
|
|
142
156
|
if (!lost.claimId)
|
|
143
157
|
return;
|
|
158
|
+
if (ownClaims.has(lost.claimId)) {
|
|
159
|
+
const c = ownClaims.get(lost.claimId);
|
|
160
|
+
getContext().logger.info(`claim: lost ${c ? claimLabel(c.entityType, c.entityId, c.field) : lost.claimId} (preempted or expired)`, { claimId: lost.claimId });
|
|
161
|
+
}
|
|
144
162
|
// Drop the lost own-claim so reconnect doesn't re-announce a lease we
|
|
145
163
|
// no longer hold.
|
|
146
164
|
ownClaims.delete(lost.claimId);
|
|
@@ -166,6 +184,16 @@ export function createClaimStream(config, transport = null) {
|
|
|
166
184
|
queueByEntity.delete(key);
|
|
167
185
|
else
|
|
168
186
|
queueByEntity.set(key, Object.freeze([...line]));
|
|
187
|
+
// If WE are in this line, trace our position (the "agent queued behind a
|
|
188
|
+
// claim" moment) — once per position change, so advancing is visible.
|
|
189
|
+
const ourIndex = line.findIndex((c) => ownClaims.has(c.id));
|
|
190
|
+
if (ourIndex >= 0) {
|
|
191
|
+
const ourId = line[ourIndex].id;
|
|
192
|
+
if (lastLoggedQueuePos.get(ourId) !== ourIndex) {
|
|
193
|
+
lastLoggedQueuePos.set(ourId, ourIndex);
|
|
194
|
+
getContext().logger.info(`claim: queued for ${claimLabel(p.target.type, p.target.id)} — position ${ourIndex + 1} of ${line.length}, waiting`, { claimId: ourId });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
169
197
|
notifyListeners();
|
|
170
198
|
}));
|
|
171
199
|
// (3) On reconnect, re-announce every open self-claim — the
|
|
@@ -251,6 +279,9 @@ export function createClaimStream(config, transport = null) {
|
|
|
251
279
|
};
|
|
252
280
|
ownClaims.set(claimId, claim);
|
|
253
281
|
sendBegin(claimId, claim);
|
|
282
|
+
// Coordination trace (info): the creator can SEE their human/agent claims.
|
|
283
|
+
getContext().logger.info(`claim: requesting ${claimLabel(claim.entityType, claim.entityId, claim.field)} for "${claim.reason}"` +
|
|
284
|
+
(claim.queue ? ' (will queue if contended)' : ''), { claimId });
|
|
254
285
|
let revoked = false;
|
|
255
286
|
const revoke = () => {
|
|
256
287
|
if (revoked)
|
|
@@ -258,6 +289,7 @@ export function createClaimStream(config, transport = null) {
|
|
|
258
289
|
revoked = true;
|
|
259
290
|
ownClaims.delete(claimId);
|
|
260
291
|
sendAbandon(claimId, claim);
|
|
292
|
+
getContext().logger.info(`claim: released ${claimLabel(claim.entityType, claim.entityId, claim.field)}`, { claimId });
|
|
261
293
|
};
|
|
262
294
|
return {
|
|
263
295
|
object: 'claim',
|
|
@@ -8,6 +8,19 @@ export interface ErrorEnvelope {
|
|
|
8
8
|
readonly message: string;
|
|
9
9
|
readonly doc_url?: string;
|
|
10
10
|
readonly request_id?: string;
|
|
11
|
+
/** Aggregate field-level failures — so one 4xx can report EVERY invalid input
|
|
12
|
+
* at once (schema push, batch commit, CLI-arg validation) instead of failing
|
|
13
|
+
* on the first. `param` stays the single-field convenience case. (RFC 9457
|
|
14
|
+
* `errors[]` / JSON:API `errors[]` / Google `BadRequest.fieldViolations[]`.) */
|
|
15
|
+
readonly errors?: ReadonlyArray<{
|
|
16
|
+
readonly code?: string;
|
|
17
|
+
readonly message: string;
|
|
18
|
+
readonly param?: string;
|
|
19
|
+
}>;
|
|
20
|
+
/** Typed-details slot: `AbloError.toJSON()` spreads its `details` (e.g.
|
|
21
|
+
* `missingIds`, `conflicts`, `retryAfterSeconds`) as top-level members.
|
|
22
|
+
* Consumers MUST ignore members they don't recognize (forward-compat). */
|
|
23
|
+
readonly [key: string]: unknown;
|
|
11
24
|
}
|
|
12
25
|
/** {@link AbloError} subclass → default HTTP status. The subclass is chosen to
|
|
13
26
|
* match status semantics (a validation error is a 400, a permission error a
|