@fadhilp/stateql 0.6.0 → 0.7.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/README.md CHANGED
@@ -253,8 +253,9 @@ cancels active work.
253
253
  - MySQL deadlines destroy the active connection.
254
254
  - MongoDB uses driver deadlines and closes stopped operations.
255
255
 
256
- A timed-out write may return `OUTCOME_UNKNOWN` when its commit status cannot be
257
- proven.
256
+ A timed-out or cancelled write may return `OUTCOME_UNKNOWN` when its commit
257
+ status cannot be proven. Cancellation stops that command's driver work; it does
258
+ not close the `StateQL` actor, and later commands remain usable.
258
259
 
259
260
  ## Durable state and result reuse
260
261
 
@@ -424,6 +425,30 @@ if (response.ok) {
424
425
  }
425
426
  ```
426
427
 
428
+ Hosts that dispatch batch-shaped commands can attach trusted metadata out of
429
+ band. `origin` is audit/source metadata only; it never changes actor membership,
430
+ workspace access, or write authorization.
431
+
432
+ ```ts
433
+ const controller = new AbortController();
434
+ await stateql.executeCommand(
435
+ { command: "query", sql: "SELECT * FROM users", cache: "bypass" },
436
+ { origin: "user", signal: controller.signal },
437
+ );
438
+
439
+ const userHistory = await stateql.history(50, { origin: "user" });
440
+ await stateql.executeCommand(
441
+ { command: "history", limit: 50, history_origin: "user" },
442
+ { origin: "model" },
443
+ );
444
+ ```
445
+
446
+ Supported origins are `legacy`, `user`, `model`, `system`, and `api`. Existing
447
+ direct calls and `executeCommand(command)` calls are recorded as `legacy`.
448
+ `history_origin` is only a retrieval filter; putting an `origin` field in a
449
+ `BatchCommand` cannot attribute the command. `batch` accepts the same trusted
450
+ context as `options.executionContext` for all commands in that batch.
451
+
427
452
  ### Actor workspaces
428
453
 
429
454
  `StateQL.forActor(...)` resolves the actor's attached session directly from
@@ -1,4 +1,4 @@
1
1
  export { StateQL } from "./stateql.js";
2
2
  export { CredentialResolutionError, StateQLError, exitCodeFor, } from "./errors.js";
3
3
  export type { CredentialResolutionFailure } from "./errors.js";
4
- export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CapabilitiesData, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfilesData, PurgeData, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
4
+ export type { ActorData, ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchCommandName, BatchOptions, CapabilitiesData, CommandExecutionContext, CommandOrigin, CloseSessionData, Column, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, CredentialAccess, CredentialOperation, CredentialRequest, CredentialResolver, DisconnectData, DoctorData, Driver, ExecData, ExecOptions, ExecutionOptions, ExportData, Failure, FilterOptions, HistoryData, HistoryEntry, HistoryOptions, MongoAggregateOptions, MongoDocument, MongoFindOptions, MongoMutationOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, MongoWriteOutcome, OperationData, PlanData, PlanOptions, ProfileData, ProfileOptions, ProfilesData, PurgeData, QueryOptions, RecentOperationData, RecentResultData, RemovedProfileData, Response, ResultData, RollbackTransactionData, Row, RowsData, RowsOptions, SqlParameters, SqlDriver, StateQLActorOptions, StateQLOptions, StateConfidence, StateQLSnapshot, StatusConnectionData, StatusData, Success, SessionData, SessionListItem, SessionsData, SessionSummaryData, TransactionData, TransactionReferenceData, Warning, } from "./types.js";
@@ -64,6 +64,17 @@ const MIGRATIONS = [
64
64
  requireColumns(db, "operations", ["outcome_json"]);
65
65
  },
66
66
  },
67
+ {
68
+ name: "history_origin_v1",
69
+ apply(db) {
70
+ addColumn(db, "history", "origin", "TEXT NOT NULL DEFAULT 'legacy'");
71
+ db.exec("CREATE INDEX IF NOT EXISTS history_session_origin ON history(session_id, origin)");
72
+ },
73
+ validate(db) {
74
+ requireColumns(db, "history", ["origin"]);
75
+ requireIndexes(db, ["history_session_origin"]);
76
+ },
77
+ },
67
78
  ];
68
79
  export function runMigrations(db, now) {
69
80
  db.exec(`
@@ -221,6 +232,7 @@ function createInitialSchema(db) {
221
232
  session_id TEXT NOT NULL,
222
233
  actor_id TEXT NOT NULL,
223
234
  command TEXT NOT NULL,
235
+ origin TEXT NOT NULL DEFAULT 'legacy',
224
236
  sql TEXT,
225
237
  handle TEXT,
226
238
  executed INTEGER NOT NULL,
@@ -1,4 +1,4 @@
1
- import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, BatchOptions, CapabilitiesData, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, StateQLActorOptions, StateQLOptions, StateQLSnapshot, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
1
+ import type { ActorLinkData, ActorResolutionData, ActorsData, ActorUnlinkData, AliasData, ApplyData, BatchCommand, CommandExecutionContext, BatchOptions, CapabilitiesData, CloseSessionData, ColumnsData, CommitTransactionData, ConnectOptions, ConnectionData, CountData, DisconnectData, DoctorData, ExecData, ExecOptions, ExecutionOptions, ExportData, FilterOptions, HistoryData, HistoryOptions, OperationData, PlanData, PlanOptions, ProfileData, ProfilesData, ProfileOptions, MongoExecOptions, MongoPlanOptions, MongoQueryOptions, MongoReadCommand, MongoWriteCommand, PurgeData, QueryOptions, RemovedProfileData, Response, ResultData, RollbackTransactionData, RowsData, RowsOptions, StateQLActorOptions, StateQLOptions, StateQLSnapshot, StatusData, SessionData, SessionsData, SessionSummaryData, TransactionData } from "./types.js";
2
2
  export declare class StateQL {
3
3
  static forActor(options: StateQLActorOptions): StateQL;
4
4
  private readonly store;
@@ -12,6 +12,7 @@ export declare class StateQL {
12
12
  private readonly maxResultBytes;
13
13
  private readonly timeoutMs;
14
14
  private readonly signal?;
15
+ private readonly commandContexts;
15
16
  private readonly credentialResolver?;
16
17
  private readonly now;
17
18
  private closed;
@@ -57,11 +58,11 @@ export declare class StateQL {
57
58
  plan(sql: string, options?: PlanOptions): Promise<Response<PlanData>>;
58
59
  mongoPlan(command: MongoWriteCommand, options?: MongoPlanOptions): Promise<Response<PlanData>>;
59
60
  apply(planId: string, options?: ExecutionOptions): Promise<Response<ApplyData>>;
60
- history(limit?: number): Promise<Response<HistoryData>>;
61
+ history(limit?: number, options?: HistoryOptions): Promise<Response<HistoryData>>;
61
62
  doctor(): Promise<Response<DoctorData>>;
62
63
  purge(scope?: "expired" | "results" | "history" | "all"): Promise<Response<PurgeData>>;
63
64
  capabilities(): Promise<Response<CapabilitiesData>>;
64
- executeCommand(command: BatchCommand): Promise<Response<unknown>>;
65
+ executeCommand(command: BatchCommand, context?: CommandExecutionContext): Promise<Response<unknown>>;
65
66
  batch(commands: Iterable<BatchCommand> | AsyncIterable<BatchCommand>, options?: BatchOptions): AsyncGenerator<Response<unknown>>;
66
67
  private performExec;
67
68
  private performMongoExec;
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import { writeFileSync } from "node:fs";
2
3
  import { basename, resolve } from "node:path";
3
4
  import { env } from "node:process";
@@ -41,6 +42,7 @@ export class StateQL {
41
42
  maxResultBytes;
42
43
  timeoutMs;
43
44
  signal;
45
+ commandContexts = new AsyncLocalStorage();
44
46
  credentialResolver;
45
47
  now;
46
48
  closed = false;
@@ -1303,11 +1305,13 @@ export class StateQL {
1303
1305
  }
1304
1306
  }, () => historySql);
1305
1307
  }
1306
- async history(limit = 20) {
1308
+ async history(limit = 20, options = {}) {
1307
1309
  return this.run("history", async (session) => ({
1308
1310
  data: {
1309
1311
  history: this.store
1310
- .history(session.id, positiveInteger(limit, "limit"))
1312
+ .history(session.id, positiveInteger(limit, "limit"), options.origin === undefined
1313
+ ? undefined
1314
+ : parseCommandOrigin(options.origin))
1311
1315
  .map(historyEntry),
1312
1316
  },
1313
1317
  }));
@@ -1364,169 +1368,180 @@ export class StateQL {
1364
1368
  },
1365
1369
  }));
1366
1370
  }
1367
- async executeCommand(command) {
1368
- if (!command || typeof command !== "object") {
1369
- return this.batchFailure("Batch command must be an object.");
1370
- }
1371
+ async executeCommand(command, context = {}) {
1372
+ let activeContext;
1371
1373
  try {
1372
- switch (command.command) {
1373
- case "connect":
1374
- return this.connect(command.target, {
1375
- name: command.name,
1376
- readOnly: command.read_only,
1377
- secretEnv: command.secret_env,
1378
- profile: command.profile,
1379
- timeoutMs: command.timeout_ms,
1380
- });
1381
- case "disconnect":
1382
- return this.disconnect();
1383
- case "status":
1384
- return this.status();
1385
- case "profile.add":
1386
- return this.addProfile(batchString(command.name, "name"), command.target, {
1387
- readOnly: command.read_only ?? true,
1388
- secretEnv: command.secret_env,
1389
- });
1390
- case "profile.list":
1391
- return this.listProfiles();
1392
- case "profile.show":
1393
- return this.showProfile(batchString(command.name, "name"));
1394
- case "profile.remove":
1395
- return this.removeProfile(batchString(command.name, "name"));
1396
- case "session.start":
1397
- return this.startSession(batchString(command.name, "name"));
1398
- case "session.list":
1399
- return this.listSessions();
1400
- case "session.show":
1401
- return this.showSession(command.name);
1402
- case "session.summary":
1403
- return this.sessionSummary();
1404
- case "session.close":
1405
- return this.closeSession();
1406
- case "query": {
1407
- const response = await this.query(batchString(command.sql, "sql"), {
1408
- params: command.params ?? [],
1409
- cache: command.cache ?? "auto",
1410
- timeoutMs: command.timeout_ms,
1411
- });
1412
- if (!response.ok || !command.as)
1413
- return response;
1414
- const resultId = response.data.result_id;
1415
- if (typeof resultId !== "string")
1416
- return response;
1417
- this.store.setAlias(response.session_id, command.as, resultId);
1418
- return {
1419
- ...response,
1420
- data: { ...response.data, alias: command.as },
1421
- };
1422
- }
1423
- case "mongo.query": {
1424
- const response = await this.mongoQuery(command.mongo, {
1425
- cache: command.cache ?? "auto",
1426
- timeoutMs: command.timeout_ms,
1427
- });
1428
- if (!response.ok || !command.as)
1429
- return response;
1430
- const resultId = response.data.result_id;
1431
- if (typeof resultId !== "string")
1432
- return response;
1433
- this.store.setAlias(response.session_id, command.as, resultId);
1434
- return {
1435
- ...response,
1436
- data: { ...response.data, alias: command.as },
1437
- };
1438
- }
1439
- case "filter": {
1440
- const response = await this.filter(batchString(command.handle, "handle"), batchString(command.where, "where"), { params: command.params ?? [] });
1441
- if (!response.ok || !command.as)
1442
- return response;
1443
- const resultId = response.data.result_id;
1444
- if (typeof resultId !== "string")
1445
- return response;
1446
- this.store.setAlias(response.session_id, command.as, resultId);
1447
- return {
1448
- ...response,
1449
- data: { ...response.data, alias: command.as },
1450
- };
1451
- }
1452
- case "exec":
1453
- return this.exec(batchString(command.sql, "sql"), {
1454
- params: command.params ?? [],
1455
- replay: command.replay ?? false,
1456
- idempotencyKey: command.idempotency_key,
1457
- allowUnbounded: command.allow_unbounded ?? false,
1458
- allowDestructive: command.allow_destructive ?? false,
1459
- timeoutMs: command.timeout_ms,
1460
- });
1461
- case "mongo.exec":
1462
- return this.mongoExec(command.mongo, {
1463
- replay: command.replay ?? false,
1464
- idempotencyKey: command.idempotency_key,
1465
- allowUnbounded: command.allow_unbounded ?? false,
1466
- allowDestructive: command.allow_destructive ?? false,
1467
- timeoutMs: command.timeout_ms,
1468
- });
1469
- case "show":
1470
- return this.show(batchString(command.handle, "handle"));
1471
- case "rows":
1472
- return this.rows(batchString(command.handle, "handle"), {
1473
- offset: command.offset ?? 0,
1474
- limit: command.limit ?? 20,
1475
- });
1476
- case "count":
1477
- return this.count(batchString(command.handle, "handle"));
1478
- case "columns":
1479
- return this.columns(batchString(command.handle, "handle"));
1480
- case "alias.set":
1481
- return this.setAlias(batchString(command.name, "name"), batchString(command.handle, "handle"));
1482
- case "inspect":
1483
- return this.inspect(batchString(command.kind, "kind"), command.table, {
1484
- timeoutMs: command.timeout_ms,
1485
- });
1486
- case "transaction.begin":
1487
- return this.beginTransaction(command.isolation);
1488
- case "transaction.status":
1489
- return this.transactionStatus(command.handle);
1490
- case "transaction.commit":
1491
- return this.commitTransaction(command.handle, {
1492
- timeoutMs: command.timeout_ms,
1493
- });
1494
- case "transaction.rollback":
1495
- return this.rollbackTransaction(command.handle);
1496
- case "plan":
1497
- return this.plan(batchString(command.sql, "sql"), {
1498
- params: command.params ?? [],
1499
- allowUnbounded: command.allow_unbounded ?? false,
1500
- allowDestructive: command.allow_destructive,
1501
- timeoutMs: command.timeout_ms,
1502
- });
1503
- case "mongo.plan":
1504
- return this.mongoPlan(command.mongo, {
1505
- allowUnbounded: command.allow_unbounded ?? false,
1506
- allowDestructive: command.allow_destructive,
1507
- timeoutMs: command.timeout_ms,
1508
- });
1509
- case "apply":
1510
- return this.apply(batchString(command.handle, "handle"), {
1511
- timeoutMs: command.timeout_ms,
1512
- });
1513
- case "history":
1514
- return this.history(command.limit ?? 20);
1515
- case "receipt":
1516
- return this.receipt(batchString(command.handle, "handle"));
1517
- case "doctor":
1518
- return this.doctor();
1519
- case "purge":
1520
- return this.purge(command.scope ?? "expired");
1521
- case "capabilities":
1522
- return this.capabilities();
1523
- default:
1524
- return this.batchFailure(`Unknown batch command "${String(command.command)}".`);
1525
- }
1374
+ activeContext = mergeCommandExecutionContext(this.commandContexts.getStore(), context);
1526
1375
  }
1527
1376
  catch (error) {
1528
1377
  return this.batchFailure(errorMessage(error));
1529
1378
  }
1379
+ return this.commandContexts.run(activeContext, async () => {
1380
+ if (!command || typeof command !== "object") {
1381
+ return this.batchFailure("Batch command must be an object.");
1382
+ }
1383
+ try {
1384
+ switch (command.command) {
1385
+ case "connect":
1386
+ return this.connect(command.target, {
1387
+ name: command.name,
1388
+ readOnly: command.read_only,
1389
+ secretEnv: command.secret_env,
1390
+ profile: command.profile,
1391
+ timeoutMs: command.timeout_ms,
1392
+ });
1393
+ case "disconnect":
1394
+ return this.disconnect();
1395
+ case "status":
1396
+ return this.status();
1397
+ case "profile.add":
1398
+ return this.addProfile(batchString(command.name, "name"), command.target, {
1399
+ readOnly: command.read_only ?? true,
1400
+ secretEnv: command.secret_env,
1401
+ });
1402
+ case "profile.list":
1403
+ return this.listProfiles();
1404
+ case "profile.show":
1405
+ return this.showProfile(batchString(command.name, "name"));
1406
+ case "profile.remove":
1407
+ return this.removeProfile(batchString(command.name, "name"));
1408
+ case "session.start":
1409
+ return this.startSession(batchString(command.name, "name"));
1410
+ case "session.list":
1411
+ return this.listSessions();
1412
+ case "session.show":
1413
+ return this.showSession(command.name);
1414
+ case "session.summary":
1415
+ return this.sessionSummary();
1416
+ case "session.close":
1417
+ return this.closeSession();
1418
+ case "query": {
1419
+ const response = await this.query(batchString(command.sql, "sql"), {
1420
+ params: command.params ?? [],
1421
+ cache: command.cache ?? "auto",
1422
+ timeoutMs: command.timeout_ms,
1423
+ });
1424
+ if (!response.ok || !command.as)
1425
+ return response;
1426
+ const resultId = response.data.result_id;
1427
+ if (typeof resultId !== "string")
1428
+ return response;
1429
+ this.store.setAlias(response.session_id, command.as, resultId);
1430
+ return {
1431
+ ...response,
1432
+ data: { ...response.data, alias: command.as },
1433
+ };
1434
+ }
1435
+ case "mongo.query": {
1436
+ const response = await this.mongoQuery(command.mongo, {
1437
+ cache: command.cache ?? "auto",
1438
+ timeoutMs: command.timeout_ms,
1439
+ });
1440
+ if (!response.ok || !command.as)
1441
+ return response;
1442
+ const resultId = response.data.result_id;
1443
+ if (typeof resultId !== "string")
1444
+ return response;
1445
+ this.store.setAlias(response.session_id, command.as, resultId);
1446
+ return {
1447
+ ...response,
1448
+ data: { ...response.data, alias: command.as },
1449
+ };
1450
+ }
1451
+ case "filter": {
1452
+ const response = await this.filter(batchString(command.handle, "handle"), batchString(command.where, "where"), { params: command.params ?? [] });
1453
+ if (!response.ok || !command.as)
1454
+ return response;
1455
+ const resultId = response.data.result_id;
1456
+ if (typeof resultId !== "string")
1457
+ return response;
1458
+ this.store.setAlias(response.session_id, command.as, resultId);
1459
+ return {
1460
+ ...response,
1461
+ data: { ...response.data, alias: command.as },
1462
+ };
1463
+ }
1464
+ case "exec":
1465
+ return this.exec(batchString(command.sql, "sql"), {
1466
+ params: command.params ?? [],
1467
+ replay: command.replay ?? false,
1468
+ idempotencyKey: command.idempotency_key,
1469
+ allowUnbounded: command.allow_unbounded ?? false,
1470
+ allowDestructive: command.allow_destructive ?? false,
1471
+ timeoutMs: command.timeout_ms,
1472
+ });
1473
+ case "mongo.exec":
1474
+ return this.mongoExec(command.mongo, {
1475
+ replay: command.replay ?? false,
1476
+ idempotencyKey: command.idempotency_key,
1477
+ allowUnbounded: command.allow_unbounded ?? false,
1478
+ allowDestructive: command.allow_destructive ?? false,
1479
+ timeoutMs: command.timeout_ms,
1480
+ });
1481
+ case "show":
1482
+ return this.show(batchString(command.handle, "handle"));
1483
+ case "rows":
1484
+ return this.rows(batchString(command.handle, "handle"), {
1485
+ offset: command.offset ?? 0,
1486
+ limit: command.limit ?? 20,
1487
+ });
1488
+ case "count":
1489
+ return this.count(batchString(command.handle, "handle"));
1490
+ case "columns":
1491
+ return this.columns(batchString(command.handle, "handle"));
1492
+ case "alias.set":
1493
+ return this.setAlias(batchString(command.name, "name"), batchString(command.handle, "handle"));
1494
+ case "inspect":
1495
+ return this.inspect(batchString(command.kind, "kind"), command.table, {
1496
+ timeoutMs: command.timeout_ms,
1497
+ });
1498
+ case "transaction.begin":
1499
+ return this.beginTransaction(command.isolation);
1500
+ case "transaction.status":
1501
+ return this.transactionStatus(command.handle);
1502
+ case "transaction.commit":
1503
+ return this.commitTransaction(command.handle, {
1504
+ timeoutMs: command.timeout_ms,
1505
+ });
1506
+ case "transaction.rollback":
1507
+ return this.rollbackTransaction(command.handle);
1508
+ case "plan":
1509
+ return this.plan(batchString(command.sql, "sql"), {
1510
+ params: command.params ?? [],
1511
+ allowUnbounded: command.allow_unbounded ?? false,
1512
+ allowDestructive: command.allow_destructive,
1513
+ timeoutMs: command.timeout_ms,
1514
+ });
1515
+ case "mongo.plan":
1516
+ return this.mongoPlan(command.mongo, {
1517
+ allowUnbounded: command.allow_unbounded ?? false,
1518
+ allowDestructive: command.allow_destructive,
1519
+ timeoutMs: command.timeout_ms,
1520
+ });
1521
+ case "apply":
1522
+ return this.apply(batchString(command.handle, "handle"), {
1523
+ timeoutMs: command.timeout_ms,
1524
+ });
1525
+ case "history":
1526
+ return this.history(command.limit ?? 20, {
1527
+ origin: command.history_origin,
1528
+ });
1529
+ case "receipt":
1530
+ return this.receipt(batchString(command.handle, "handle"));
1531
+ case "doctor":
1532
+ return this.doctor();
1533
+ case "purge":
1534
+ return this.purge(command.scope ?? "expired");
1535
+ case "capabilities":
1536
+ return this.capabilities();
1537
+ default:
1538
+ return this.batchFailure(`Unknown batch command "${String(command.command)}".`);
1539
+ }
1540
+ }
1541
+ catch (error) {
1542
+ return this.batchFailure(errorMessage(error));
1543
+ }
1544
+ });
1530
1545
  }
1531
1546
  async *batch(commands, options = {}) {
1532
1547
  const maxCommands = options.maxCommands ?? 1_000;
@@ -1541,7 +1556,7 @@ export class StateQL {
1541
1556
  yield await this.batchFailure(`Batch cannot exceed ${maxCommands} commands.`);
1542
1557
  return;
1543
1558
  }
1544
- const response = await this.executeCommand(command);
1559
+ const response = await this.executeCommand(command, options.executionContext);
1545
1560
  yield response;
1546
1561
  if (!response.ok && !options.continueOnError)
1547
1562
  return;
@@ -2074,7 +2089,7 @@ export class StateQL {
2074
2089
  }
2075
2090
  }
2076
2091
  executionContext(options) {
2077
- return createAdapterContext(executionTimeout(options.timeoutMs ?? this.timeoutMs), options.signal ?? this.signal);
2092
+ return createAdapterContext(executionTimeout(options.timeoutMs ?? this.timeoutMs), combineAbortSignals(options.signal, this.commandContexts.getStore()?.signal, this.signal));
2078
2093
  }
2079
2094
  resultData(result, cached) {
2080
2095
  const rows = this.store.resultRows(result);
@@ -2104,6 +2119,7 @@ export class StateQL {
2104
2119
  }
2105
2120
  async run(command, action, historySql) {
2106
2121
  const started = performance.now();
2122
+ const origin = this.commandContexts.getStore()?.origin ?? "legacy";
2107
2123
  let session = this.store.ensureSession(this.sessionName);
2108
2124
  const commandId = this.store.nextId("cmd");
2109
2125
  if (!this.store.isSessionMember(session.id, this.actorId)) {
@@ -2126,6 +2142,7 @@ export class StateQL {
2126
2142
  id: commandId,
2127
2143
  sessionId: session.id,
2128
2144
  actorId: this.actorId,
2145
+ origin,
2129
2146
  command,
2130
2147
  ...(result.handle ? { handle: result.handle } : {}),
2131
2148
  ...(sqlText !== undefined ? { sql: sqlText } : {}),
@@ -2157,6 +2174,7 @@ export class StateQL {
2157
2174
  id: commandId,
2158
2175
  sessionId: session.id,
2159
2176
  actorId: this.actorId,
2177
+ origin,
2160
2178
  command,
2161
2179
  ...(sqlText !== undefined ? { sql: sqlText } : {}),
2162
2180
  executed: stateqlError.details.executed,
@@ -2185,6 +2203,7 @@ function historyEntry(item) {
2185
2203
  timestamp: item.timestamp,
2186
2204
  session_id: item.session_id,
2187
2205
  actor_id: item.actor_id,
2206
+ origin: item.origin,
2188
2207
  command: item.command,
2189
2208
  sql: item.sql,
2190
2209
  handle: item.handle,
@@ -2194,6 +2213,41 @@ function historyEntry(item) {
2194
2213
  error_code: item.error_code,
2195
2214
  };
2196
2215
  }
2216
+ const COMMAND_ORIGINS = new Set([
2217
+ "legacy",
2218
+ "user",
2219
+ "model",
2220
+ "system",
2221
+ "api",
2222
+ ]);
2223
+ function parseCommandOrigin(value) {
2224
+ if (typeof value === "string" && COMMAND_ORIGINS.has(value)) {
2225
+ return value;
2226
+ }
2227
+ throw new StateQLError("INVALID_COMMAND", `Unknown command origin "${String(value)}".`);
2228
+ }
2229
+ function mergeCommandExecutionContext(inherited, supplied) {
2230
+ if (!supplied || typeof supplied !== "object") {
2231
+ throw new StateQLError("INVALID_COMMAND", "Command execution context must be an object.");
2232
+ }
2233
+ if (supplied.signal !== undefined && !(supplied.signal instanceof AbortSignal)) {
2234
+ throw new StateQLError("INVALID_COMMAND", "Command execution context signal must be an AbortSignal.");
2235
+ }
2236
+ return {
2237
+ signal: combineAbortSignals(inherited?.signal, supplied.signal),
2238
+ origin: supplied.origin === undefined
2239
+ ? inherited?.origin
2240
+ : parseCommandOrigin(supplied.origin),
2241
+ };
2242
+ }
2243
+ function combineAbortSignals(...signals) {
2244
+ const present = signals.filter((signal) => signal !== undefined);
2245
+ if (present.length === 0)
2246
+ return undefined;
2247
+ if (present.length === 1)
2248
+ return present[0];
2249
+ return AbortSignal.any(present);
2250
+ }
2197
2251
  function markTransactionOutcomeUnknown(store, transactionId, sessionId, actorId) {
2198
2252
  try {
2199
2253
  store.markTransactionOutcomeUnknown(transactionId, sessionId, actorId);
@@ -1,5 +1,5 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
- import type { Column, Driver, MongoWriteOutcome, Row, SqlParameters, StateConfidence } from "./types.js";
2
+ import type { Column, CommandOrigin, Driver, MongoWriteOutcome, Row, SqlParameters, StateConfidence } from "./types.js";
3
3
  export interface SessionRecord {
4
4
  id: string;
5
5
  name: string;
@@ -98,6 +98,7 @@ export interface HistoryRecord {
98
98
  timestamp: string;
99
99
  session_id: string;
100
100
  actor_id: string;
101
+ origin: CommandOrigin;
101
102
  command: string;
102
103
  sql: string | null;
103
104
  handle: string | null;
@@ -270,6 +271,7 @@ export declare class StateStore {
270
271
  addHistory(input: {
271
272
  sessionId: string;
272
273
  actorId: string;
274
+ origin?: CommandOrigin;
273
275
  command: string;
274
276
  sql?: string;
275
277
  handle?: string;
@@ -279,7 +281,7 @@ export declare class StateStore {
279
281
  errorCode?: string;
280
282
  id?: string;
281
283
  }): HistoryRecord;
282
- history(sessionId: string, limit: number): HistoryRecord[];
284
+ history(sessionId: string, limit: number, origin?: CommandOrigin): HistoryRecord[];
283
285
  recentOperations(sessionId: string, limit: number): OperationRecord[];
284
286
  knownResults(sessionId: string, limit: number): Array<ResultRecord & {
285
287
  alias: string | null;
package/dist/src/store.js CHANGED
@@ -795,10 +795,10 @@ export class StateStore {
795
795
  const id = input.id ?? this.nextId("cmd");
796
796
  this.db
797
797
  .prepare(`INSERT INTO history
798
- (id, timestamp, session_id, actor_id, command, sql, handle, executed,
799
- cached, success, error_code)
800
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
801
- .run(id, this.now().toISOString(), input.sessionId, input.actorId, input.command, boundedHistorySql(input.sql), input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
798
+ (id, timestamp, session_id, actor_id, origin, command, sql, handle,
799
+ executed, cached, success, error_code)
800
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
801
+ .run(id, this.now().toISOString(), input.sessionId, input.actorId, input.origin ?? "legacy", input.command, boundedHistorySql(input.sql), input.handle ?? null, input.executed ? 1 : 0, input.cached ? 1 : 0, input.success ? 1 : 0, input.errorCode ?? null);
802
802
  this.db
803
803
  .prepare(`DELETE FROM history
804
804
  WHERE rowid IN (
@@ -812,13 +812,13 @@ export class StateStore {
812
812
  .prepare("SELECT * FROM history WHERE id = ?")
813
813
  .get(id);
814
814
  }
815
- history(sessionId, limit) {
815
+ history(sessionId, limit, origin) {
816
816
  return this.db
817
817
  .prepare(`SELECT * FROM history
818
- WHERE session_id = ?
818
+ WHERE session_id = ?${origin === undefined ? "" : " AND origin = ?"}
819
819
  ORDER BY rowid DESC
820
820
  LIMIT ?`)
821
- .all(sessionId, limit);
821
+ .all(...(origin === undefined ? [sessionId, limit] : [sessionId, origin, limit]));
822
822
  }
823
823
  recentOperations(sessionId, limit) {
824
824
  return this.db
@@ -1,5 +1,11 @@
1
1
  export type SqlDriver = "sqlite" | "postgres" | "mysql";
2
2
  export type Driver = SqlDriver | "mongodb";
3
+ export type CommandOrigin = "legacy" | "user" | "model" | "system" | "api";
4
+ /** Trusted host metadata for one executeCommand call; never part of BatchCommand input. */
5
+ export interface CommandExecutionContext {
6
+ signal?: AbortSignal;
7
+ origin?: CommandOrigin;
8
+ }
3
9
  export type CredentialAccess = "read" | "write";
4
10
  export type CredentialOperation = "connect" | "query" | "inspect" | "plan" | "exec" | "apply" | "transaction.commit";
5
11
  export interface CredentialRequest {
@@ -64,6 +70,7 @@ export interface HistoryEntry {
64
70
  timestamp: string;
65
71
  session_id: string;
66
72
  actor_id: string;
73
+ origin: CommandOrigin;
67
74
  command: string;
68
75
  sql: string | null;
69
76
  handle: string | null;
@@ -184,6 +191,9 @@ export interface ExecutionOptions {
184
191
  timeoutMs?: number;
185
192
  signal?: AbortSignal;
186
193
  }
194
+ export interface HistoryOptions {
195
+ origin?: CommandOrigin;
196
+ }
187
197
  export interface StateQLOptions extends ExecutionOptions {
188
198
  home?: string;
189
199
  session?: string;
@@ -272,11 +282,14 @@ export interface BatchCommand {
272
282
  limit?: number;
273
283
  isolation?: string;
274
284
  timeout_ms?: number;
285
+ /** Retrieval filter for the history command; does not attribute this command. */
286
+ history_origin?: CommandOrigin;
275
287
  scope?: "expired" | "results" | "history" | "all";
276
288
  }
277
289
  export interface BatchOptions {
278
290
  continueOnError?: boolean;
279
291
  maxCommands?: number;
292
+ executionContext?: CommandExecutionContext;
280
293
  }
281
294
  export interface Column {
282
295
  name: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fadhilp/stateql",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Stateful, agent-oriented database CLI for safe result reuse",
5
5
  "repository": {
6
6
  "type": "git",