@stackbone/sdk 0.1.0-alpha.5 → 0.1.0-alpha.6

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 CHANGED
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.0-alpha.6] - 2026-06-17
11
+
10
12
  ### Added
11
13
 
12
14
  - **`client.connections` surface (gated by the new `connections.actions`
package/index.cjs CHANGED
@@ -8877,7 +8877,8 @@ var journalTerminalStatusSchema = zod.z.enum([
8877
8877
  ]);
8878
8878
  var journalJobStatusSchema = zod.z.enum([
8879
8879
  ...journalTerminalStatusSchema.options,
8880
- "running"
8880
+ "running",
8881
+ "lost"
8881
8882
  ]);
8882
8883
  var jobStatusSchema = zod.z.enum([
8883
8884
  ...bullmqJobStatusSchema.options,
@@ -8979,6 +8980,27 @@ zod.z.object({
8979
8980
  queue: zod.z.string(),
8980
8981
  status: jobStatusSchema
8981
8982
  });
8983
+ zod.z.object({
8984
+ items: zod.z.array(bullmqJobSchema)
8985
+ });
8986
+ zod.z.object({
8987
+ purged: zod.z.number().int().nonnegative()
8988
+ });
8989
+ zod.z.object({
8990
+ payload: jsonObject(),
8991
+ retries: zod.z.number().int().nonnegative().optional(),
8992
+ delay: zod.z.number().int().nonnegative().optional()
8993
+ });
8994
+ zod.z.object({
8995
+ message_id: zod.z.string(),
8996
+ queue: queueNameSchema
8997
+ });
8998
+ zod.z.object({
8999
+ name: queueNameSchema
9000
+ });
9001
+ zod.z.object({
9002
+ limit: zod.z.coerce.number().int().min(1).max(50).default(10)
9003
+ });
8982
9004
 
8983
9005
  // ../shared/validators/src/lib/agents/queue-jobs.ts
8984
9006
  var MAX_JOB_ATTEMPTS = 20;
@@ -9937,7 +9959,7 @@ var ApprovalFacade = class {
9937
9959
  const fallback = onTimeoutToFallback(options.onTimeout);
9938
9960
  const callbackUrl = options.onDecide;
9939
9961
  try {
9940
- const rows = await sql.data`
9962
+ const rows = await this._getDatabase().runShared((db) => db`
9941
9963
  INSERT INTO stackbone_platform.approvals (
9942
9964
  topic, payload, callback_url, idempotency_key,
9943
9965
  fallback, metadata, timeout_at
@@ -9956,7 +9978,7 @@ var ApprovalFacade = class {
9956
9978
  RETURNING id, callback_url,
9957
9979
  to_char(created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS created_at,
9958
9980
  to_char(timeout_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS timeout_at
9959
- `;
9981
+ `);
9960
9982
  const row = rows[0];
9961
9983
  if (!row) {
9962
9984
  return err({
@@ -9991,13 +10013,13 @@ var ApprovalFacade = class {
9991
10013
  const patch = reason ? {
9992
10014
  cancelReason: reason
9993
10015
  } : {};
9994
- await sql.data`
10016
+ await this._getDatabase().runShared((db) => db`
9995
10017
  UPDATE stackbone_platform.approvals
9996
10018
  SET status = 'cancelled',
9997
10019
  decided_at = now(),
9998
10020
  metadata = metadata || ${JSON.stringify(patch)}::jsonb
9999
10021
  WHERE id = ${approvalId}::uuid AND status = 'pending'
10000
- `;
10022
+ `);
10001
10023
  return ok(void 0);
10002
10024
  } catch (cause) {
10003
10025
  return err({
@@ -10017,14 +10039,14 @@ var ApprovalFacade = class {
10017
10039
  const sql = this.sql();
10018
10040
  if (sql.error) return err(sql.error);
10019
10041
  try {
10020
- const rows = await sql.data`
10042
+ const rows = await this._getDatabase().runShared((db) => db`
10021
10043
  SELECT id, topic, status, payload, metadata,
10022
10044
  to_char(created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS created_at,
10023
10045
  to_char(timeout_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS timeout_at
10024
10046
  FROM stackbone_platform.approvals
10025
10047
  WHERE id = ${approvalId}::uuid
10026
10048
  LIMIT 1
10027
- `;
10049
+ `);
10028
10050
  const row = rows[0];
10029
10051
  if (!row) {
10030
10052
  return err({
@@ -10049,7 +10071,7 @@ var ApprovalFacade = class {
10049
10071
  if (sql.error) return err(sql.error);
10050
10072
  const limit = clampLimit(options.limit);
10051
10073
  try {
10052
- const rows = await sql.data`
10074
+ const rows = await this._getDatabase().runShared((db) => db`
10053
10075
  SELECT id, topic, status, payload, metadata,
10054
10076
  to_char(created_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS created_at,
10055
10077
  to_char(timeout_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') AS timeout_at
@@ -10058,7 +10080,7 @@ var ApprovalFacade = class {
10058
10080
  AND (${options.topic ?? null}::text IS NULL OR topic = ${options.topic ?? null})
10059
10081
  ORDER BY created_at DESC, id DESC
10060
10082
  LIMIT ${limit}
10061
- `;
10083
+ `);
10062
10084
  return ok({
10063
10085
  items: rows.map((row) => toRecord(row))
10064
10086
  });
@@ -10224,9 +10246,9 @@ var ConfigFacade = class {
10224
10246
  const sql = this.sql();
10225
10247
  if (sql.error) return err(sql.error);
10226
10248
  try {
10227
- const rows = await sql.data`
10249
+ const rows = await this._getDatabase().runShared((db) => db`
10228
10250
  SELECT payload FROM stackbone_platform.agent_config WHERE id = 1
10229
- `;
10251
+ `);
10230
10252
  return ok(rows[0]?.payload ?? null);
10231
10253
  } catch (cause) {
10232
10254
  return err({
@@ -10300,6 +10322,10 @@ function isPoolTerminatedError(error) {
10300
10322
  return typeof code === "string" && POOL_TERMINATED_ERROR_CODES.has(code);
10301
10323
  }
10302
10324
  __name(isPoolTerminatedError, "isPoolTerminatedError");
10325
+ function invalidatePoolHandle() {
10326
+ cachedHandle = null;
10327
+ }
10328
+ __name(invalidatePoolHandle, "invalidatePoolHandle");
10303
10329
  async function runWithColdStartRetry(firstAttempt, retryAttempt) {
10304
10330
  try {
10305
10331
  return await firstAttempt();
@@ -10468,6 +10494,48 @@ var DatabaseModule = class {
10468
10494
  // a half-baked tx. The contract gate fires before the first attempt.
10469
10495
  transaction = /* @__PURE__ */ __name((...args) => withColdStartRetry(() => getDatabaseHandle(this._databaseUrl).transaction(...args), this._gate), "transaction");
10470
10496
  /**
10497
+ * Cross-surface raw-SQL runner carrying the SAME cold-start resilience the
10498
+ * query-builder verbs above get. Sibling agent-local surfaces (`approval`,
10499
+ * `config`, `secrets`, `prompts`) that issue tagged-template queries over the
10500
+ * shared postgres-js `Sql` MUST execute them through this instead of caching
10501
+ * `shared().$client` once and calling it directly. Otherwise a dead pooled
10502
+ * socket — Fly Machines suspending the Machine between invokes, or a
10503
+ * WSL2/Docker localhost relay dropping an idle connection during a long LLM
10504
+ * step — surfaces as an unretried `CONNECTION_ENDED`/`ECONNRESET`. That is
10505
+ * exactly the asymmetry that let `client.approval.request` fail while the
10506
+ * `client.database.insert` right before it recovered.
10507
+ *
10508
+ * `fn` receives a FRESH `$client` on each attempt, so the single retry runs
10509
+ * against the rebuilt pool. Whole-operation granularity is replayed as a
10510
+ * unit — pass a multi-statement `sql.begin(...)` transaction or a helper that
10511
+ * fires several queries and the replay re-runs all of it; postgres-js never
10512
+ * commits a half-applied tx, so that is safe. SQLSTATE application errors
10513
+ * pass straight through without a retry. Like `shared()`/`raw()`, the
10514
+ * contract gate is intentionally NOT consulted here — gating surfaces gate at
10515
+ * their own call sites.
10516
+ */
10517
+ runShared(fn) {
10518
+ return withColdStartRetry(() => fn(getDatabaseHandle(this._databaseUrl).$client));
10519
+ }
10520
+ /**
10521
+ * `Result`-shaped sibling of `runShared` for surfaces whose driver swallows
10522
+ * the dead-socket error into a returned `Result.err.cause` instead of
10523
+ * throwing it — today only `client.rag`, whose `RagPipeline` returns a
10524
+ * `Result` rather than throwing. `attempt` runs once; if it comes back with a
10525
+ * pool-terminated `cause`, the pool is rebuilt and `attempt` replays exactly
10526
+ * once, then its result (success or the fresh error) is returned verbatim.
10527
+ * The `Result` contract is preserved end to end: a still-dead socket on the
10528
+ * replay surfaces as the surface's own `Result.err`, never as a raw throw.
10529
+ */
10530
+ async runSharedResult(attempt) {
10531
+ const first = await attempt();
10532
+ if (!first.error || !isPoolTerminatedError(first.error.cause)) return first;
10533
+ process.stderr.write(`[stackbone/sdk] database cold-start reconnect: rebuilding pool after ${first.error.cause.code ?? "unknown"}
10534
+ `);
10535
+ invalidatePoolHandle();
10536
+ return attempt();
10537
+ }
10538
+ /**
10471
10539
  * Canonical accessor for the **shared-handles pattern**. Returns the
10472
10540
  * process-wide Drizzle handle backing `client.database`. Cross-surface
10473
10541
  * SDK consumers (RAG today, memory / queues tomorrow) call this — never
@@ -10569,12 +10637,12 @@ var PromptsFacade = class {
10569
10637
  const sql = this.sql();
10570
10638
  if (sql.error) return err(sql.error);
10571
10639
  try {
10572
- const head = await fetchHead(sql.data, key);
10640
+ const head = await this._getDatabase().runShared((db) => fetchHead(db, key));
10573
10641
  if (!head) {
10574
10642
  return err(notFound(key));
10575
10643
  }
10576
10644
  if (options?.version !== void 0 && options.version !== head.current_version) {
10577
- const content = await fetchVersionContent(sql.data, key, options.version);
10645
+ const content = await this._getDatabase().runShared((db) => fetchVersionContent(db, key, options.version));
10578
10646
  if (content === null) {
10579
10647
  return err({
10580
10648
  code: "prompts_not_found",
@@ -10625,7 +10693,7 @@ var PromptsFacade = class {
10625
10693
  const sql = this.sql();
10626
10694
  if (sql.error) return err(sql.error);
10627
10695
  try {
10628
- const rows = await sql.data`
10696
+ const rows = await this._getDatabase().runShared((db) => db`
10629
10697
  SELECT p.key, p.name, p.description, p.current_version, p.metadata,
10630
10698
  p.created_at, p.updated_at, v.content
10631
10699
  FROM stackbone_platform.prompts p
@@ -10634,7 +10702,7 @@ var PromptsFacade = class {
10634
10702
  WHERE p.deleted_at IS NULL
10635
10703
  ORDER BY p.key
10636
10704
  LIMIT ${limit}
10637
- `;
10705
+ `);
10638
10706
  return ok({
10639
10707
  items: rows.map((row) => rowToPrompt(row, row.current_version, row.content))
10640
10708
  });
@@ -10661,7 +10729,7 @@ var PromptsFacade = class {
10661
10729
  if (sql.error) return err(sql.error);
10662
10730
  const variables = extractVars(request.template);
10663
10731
  try {
10664
- const head = await sql.data.begin(async (tx) => {
10732
+ const head = await this._getDatabase().runShared((db) => db.begin(async (tx) => {
10665
10733
  await tx`
10666
10734
  INSERT INTO stackbone_platform.prompts
10667
10735
  (key, name, description, current_version, metadata)
@@ -10689,7 +10757,7 @@ var PromptsFacade = class {
10689
10757
  WHERE p.key = ${request.key}
10690
10758
  `;
10691
10759
  return rows[0];
10692
- });
10760
+ }));
10693
10761
  if (!head) {
10694
10762
  return err({
10695
10763
  code: "prompts_unavailable",
@@ -10722,7 +10790,7 @@ var PromptsFacade = class {
10722
10790
  const sql = this.sql();
10723
10791
  if (sql.error) return err(sql.error);
10724
10792
  try {
10725
- const head = await sql.data.begin(async (tx) => {
10793
+ const head = await this._getDatabase().runShared((db) => db.begin(async (tx) => {
10726
10794
  await tx`SELECT pg_advisory_xact_lock(hashtext(${`prompts:${key}`}))`;
10727
10795
  const existing = await tx`
10728
10796
  SELECT p.key, p.name, p.description, p.current_version, p.metadata,
@@ -10766,7 +10834,7 @@ var PromptsFacade = class {
10766
10834
  WHERE p.key = ${key}
10767
10835
  `;
10768
10836
  return rows[0] ?? null;
10769
- });
10837
+ }));
10770
10838
  if (head === null) {
10771
10839
  return err(notFound(key));
10772
10840
  }
@@ -10781,12 +10849,12 @@ var PromptsFacade = class {
10781
10849
  const sql = this.sql();
10782
10850
  if (sql.error) return err(sql.error);
10783
10851
  try {
10784
- const rows = await sql.data`
10852
+ const rows = await this._getDatabase().runShared((db) => db`
10785
10853
  UPDATE stackbone_platform.prompts
10786
10854
  SET deleted_at = now(), updated_at = now()
10787
10855
  WHERE key = ${key} AND deleted_at IS NULL
10788
10856
  RETURNING key
10789
- `;
10857
+ `);
10790
10858
  const deleted = rows.length;
10791
10859
  if (deleted === 0) {
10792
10860
  return err(notFound(key));
@@ -11870,8 +11938,10 @@ var RagModule = class {
11870
11938
  const sql = this.sql();
11871
11939
  if (sql.error) return err(sql.error);
11872
11940
  try {
11873
- await sql.data`DROP TABLE IF EXISTS rag_chunks`;
11874
- await sql.data`DROP TABLE IF EXISTS _rag_meta`;
11941
+ await this._getDatabase().runShared(async (db) => {
11942
+ await db`DROP TABLE IF EXISTS rag_chunks`;
11943
+ await db`DROP TABLE IF EXISTS _rag_meta`;
11944
+ });
11875
11945
  return ok(void 0);
11876
11946
  } catch (cause) {
11877
11947
  return err(toRagError(cause, "Reset failed"));
@@ -11921,13 +11991,15 @@ var RagModule = class {
11921
11991
  }
11922
11992
  }
11923
11993
  async withPipeline(embedder, run) {
11924
- const pipeline = new RagPipeline({
11925
- sqlProvider: /* @__PURE__ */ __name(() => this.sql(), "sqlProvider"),
11926
- ...embedder && {
11927
- embedder
11928
- }
11994
+ return this._getDatabase().runSharedResult(async () => {
11995
+ const pipeline = new RagPipeline({
11996
+ sqlProvider: /* @__PURE__ */ __name(() => this.sql(), "sqlProvider"),
11997
+ ...embedder && {
11998
+ embedder
11999
+ }
12000
+ });
12001
+ return await run(pipeline);
11929
12002
  });
11930
- return await run(pipeline);
11931
12003
  }
11932
12004
  };
11933
12005
  function countChunks(request) {
@@ -15810,12 +15882,12 @@ var SecretsFacade = class {
15810
15882
  if (sql.error) return err(sql.error);
15811
15883
  let rows;
15812
15884
  try {
15813
- rows = await sql.data`
15885
+ rows = await this._getDatabase().runShared((db) => db`
15814
15886
  SELECT version, nonce, ciphertext
15815
15887
  FROM stackbone_platform.secrets
15816
15888
  WHERE name = ${name}
15817
15889
  LIMIT 1
15818
- `;
15890
+ `);
15819
15891
  } catch (cause) {
15820
15892
  return err({
15821
15893
  code: "secrets_unavailable",
@@ -15861,11 +15933,11 @@ var SecretsFacade = class {
15861
15933
  if (sql.error) return err(sql.error);
15862
15934
  let rows;
15863
15935
  try {
15864
- rows = await sql.data`
15936
+ rows = await this._getDatabase().runShared((db) => db`
15865
15937
  SELECT name, version, nonce, ciphertext
15866
15938
  FROM stackbone_platform.secrets
15867
15939
  WHERE name = ANY(${names})
15868
- `;
15940
+ `);
15869
15941
  } catch (cause) {
15870
15942
  return err({
15871
15943
  code: "secrets_unavailable",
@@ -17274,9 +17346,7 @@ var STORED_TO_SDK_ENV = {
17274
17346
  S3_REGION: "STACKBONE_S3_REGION",
17275
17347
  // OpenRouter (`client.ai`) — identity.
17276
17348
  OPENROUTER_API_KEY: "OPENROUTER_API_KEY",
17277
- OPENROUTER_BASE_URL: "OPENROUTER_BASE_URL",
17278
- // Axiom (observability) — identity.
17279
- AXIOM_TOKEN: "AXIOM_TOKEN"
17349
+ OPENROUTER_BASE_URL: "OPENROUTER_BASE_URL"
17280
17350
  };
17281
17351
  function rehydrateSystemSecretsRows(env, rows, cipher, databaseUrl) {
17282
17352
  const applied = [];