@botharness/core 0.1.0-alpha.1 → 1.0.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/dist/index.mjs CHANGED
@@ -2828,6 +2828,114 @@ function handleIntersectionResults(result, left, right) {
2828
2828
  result.value = merged.data;
2829
2829
  return result;
2830
2830
  }
2831
+ const $ZodTuple = /*@__PURE__*/ $constructor("$ZodTuple", (inst, def) => {
2832
+ $ZodType.init(inst, def);
2833
+ const items = def.items;
2834
+ const memo = globalConfig.memoizer;
2835
+ memo?.attach(inst);
2836
+ inst._zod.parse = (payload, ctx) => {
2837
+ const input = payload.value;
2838
+ if (!Array.isArray(input)) {
2839
+ payload.issues.push({
2840
+ input,
2841
+ inst,
2842
+ expected: "tuple",
2843
+ code: "invalid_type"
2844
+ });
2845
+ return payload;
2846
+ }
2847
+ payload.value = memo ? memo.alloc(inst, payload, [], ctx) : [];
2848
+ const proms = [];
2849
+ const optinStart = getTupleOptStart(items, "optin");
2850
+ const optoutStart = getTupleOptStart(items, "optout");
2851
+ if (!def.rest) {
2852
+ if (input.length < optinStart) {
2853
+ payload.issues.push({
2854
+ code: "too_small",
2855
+ minimum: optinStart,
2856
+ inclusive: true,
2857
+ input,
2858
+ inst,
2859
+ origin: "array"
2860
+ });
2861
+ return payload;
2862
+ }
2863
+ if (input.length > items.length) payload.issues.push({
2864
+ code: "too_big",
2865
+ maximum: items.length,
2866
+ inclusive: true,
2867
+ input,
2868
+ inst,
2869
+ origin: "array"
2870
+ });
2871
+ }
2872
+ const itemResults = new Array(items.length);
2873
+ const abortEarly = def.rest ? ctx?.abortEarly : void 0;
2874
+ let itemAborted = false;
2875
+ for (let i = 0; i < items.length; i++) {
2876
+ const r = items[i]._zod.run({
2877
+ value: input[i],
2878
+ issues: []
2879
+ }, ctx);
2880
+ if (r instanceof Promise) proms.push(r.then((rr) => {
2881
+ itemResults[i] = rr;
2882
+ }));
2883
+ else {
2884
+ itemResults[i] = r;
2885
+ if (abortEarly && !itemAborted && r.issues.length) itemAborted = aborted(r);
2886
+ }
2887
+ }
2888
+ if (def.rest && !itemAborted) {
2889
+ let i = items.length - 1;
2890
+ const rest = input.slice(items.length);
2891
+ let seen = payload.issues.length;
2892
+ for (const el of rest) {
2893
+ if (abortEarly && payload.issues.length !== seen) {
2894
+ if (aborted(payload, seen)) break;
2895
+ seen = payload.issues.length;
2896
+ }
2897
+ i++;
2898
+ const result = def.rest._zod.run({
2899
+ value: el,
2900
+ issues: []
2901
+ }, ctx);
2902
+ if (result instanceof Promise) proms.push(result.then((r) => handleTupleResult(r, payload, i)));
2903
+ else handleTupleResult(result, payload, i);
2904
+ }
2905
+ }
2906
+ if (proms.length) return Promise.all(proms).then(() => handleTupleResults(itemResults, payload, items, input, optoutStart));
2907
+ return handleTupleResults(itemResults, payload, items, input, optoutStart);
2908
+ };
2909
+ });
2910
+ function getTupleOptStart(items, key) {
2911
+ for (let i = items.length - 1; i >= 0; i--) if (!(key === "optin" ? items[i]._zod.optin !== void 0 : items[i]._zod.optout === "optional")) return i + 1;
2912
+ return 0;
2913
+ }
2914
+ function handleTupleResult(result, final, index) {
2915
+ if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));
2916
+ final.value[index] = result.value;
2917
+ }
2918
+ function handleTupleResults(itemResults, final, items, input, optoutStart) {
2919
+ for (let i = 0; i < items.length; i++) {
2920
+ const r = itemResults[i];
2921
+ const isPresent = i < input.length;
2922
+ if (!isPresent && i >= optoutStart && items[i]._zod.optin === "optional") {
2923
+ final.value.length = i;
2924
+ break;
2925
+ }
2926
+ if (r.issues.length) {
2927
+ if (!isPresent && i >= optoutStart) {
2928
+ final.value.length = i;
2929
+ break;
2930
+ }
2931
+ final.issues.push(...prefixIssues(i, r.issues));
2932
+ }
2933
+ final.value[i] = r.value;
2934
+ }
2935
+ for (let i = final.value.length - 1; i >= input.length; i--) if (items[i]._zod.optout === "optional" && final.value[i] === void 0) final.value.length = i;
2936
+ else break;
2937
+ return final;
2938
+ }
2831
2939
  const $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
2832
2940
  $ZodType.init(inst, def);
2833
2941
  const values = getEnumValues(def.entries);
@@ -4745,6 +4853,58 @@ const intersectionProcessor = (schema, ctx, json, params) => {
4745
4853
  json.allOf = allOf;
4746
4854
  ctx.intersections.push(allOf);
4747
4855
  };
4856
+ const tupleProcessor = (schema, ctx, _json, params) => {
4857
+ const json = _json;
4858
+ const def = schema._zod.def;
4859
+ json.type = "array";
4860
+ const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
4861
+ const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
4862
+ const prefixItems = def.items.map((x, i) => processSchema(x, ctx, {
4863
+ ...params,
4864
+ path: [
4865
+ ...params.path,
4866
+ prefixPath,
4867
+ i
4868
+ ]
4869
+ }));
4870
+ const rest = def.rest ? processSchema(def.rest, ctx, {
4871
+ ...params,
4872
+ path: [
4873
+ ...params.path,
4874
+ restPath,
4875
+ ...ctx.target === "openapi-3.0" ? [def.items.length] : []
4876
+ ]
4877
+ }) : null;
4878
+ let minItems = def.items.length;
4879
+ while (minItems > 0) {
4880
+ const item = def.items[minItems - 1];
4881
+ if (!(ctx.io === "input" ? inputOptin(item) !== void 0 : item._zod.optout === "optional")) break;
4882
+ minItems--;
4883
+ }
4884
+ const maxItems = def.items.length;
4885
+ const isClosed = !def.rest;
4886
+ if (ctx.target === "draft-2020-12") {
4887
+ json.prefixItems = prefixItems;
4888
+ if (isClosed) json.items = false;
4889
+ else if (rest) json.items = rest;
4890
+ if (minItems > 0) json.minItems = minItems;
4891
+ if (isClosed) json.maxItems = maxItems;
4892
+ } else if (ctx.target === "openapi-3.0") {
4893
+ json.items = { anyOf: prefixItems };
4894
+ if (rest) json.items.anyOf.push(rest);
4895
+ if (minItems > 0) json.minItems = minItems;
4896
+ if (isClosed) json.maxItems = maxItems;
4897
+ } else {
4898
+ json.items = prefixItems;
4899
+ if (isClosed) json.additionalItems = false;
4900
+ else if (rest) json.additionalItems = rest;
4901
+ if (minItems > 0) json.minItems = minItems;
4902
+ if (isClosed) json.maxItems = maxItems;
4903
+ }
4904
+ const { minimum, maximum } = aggregateChecks(schema);
4905
+ if (typeof minimum === "number") json.minItems = minimum;
4906
+ if (typeof maximum === "number") json.maxItems = maximum;
4907
+ };
4748
4908
  const nullableProcessor = (schema, ctx, json, params) => {
4749
4909
  const def = schema._zod.def;
4750
4910
  const inner = processSchema(def.innerType, ctx, params);
@@ -5546,6 +5706,39 @@ function intersection(left, right) {
5546
5706
  right
5547
5707
  });
5548
5708
  }
5709
+ const ZodTuple = /*@__PURE__*/ $constructor("ZodTuple", (inst, def) => {
5710
+ _ensureDefaultMemoizer();
5711
+ $ZodTuple.init(inst, def);
5712
+ ZodType.init(inst, def);
5713
+ inst._zod.processJSONSchema = (ctx, json, params) => tupleProcessor(inst, ctx, json, params);
5714
+ }, {
5715
+ rest(rest) {
5716
+ return this.clone({
5717
+ ...this._zod.def,
5718
+ rest
5719
+ });
5720
+ },
5721
+ partial() {
5722
+ const def = this._zod.def;
5723
+ if (def.checks?.length) throw new Error(".partial() cannot be used on tuple schemas containing refinements");
5724
+ return this.clone({
5725
+ ...def,
5726
+ items: def.items.map((item) => new ZodOptional({
5727
+ type: "optional",
5728
+ innerType: item
5729
+ }))
5730
+ });
5731
+ }
5732
+ });
5733
+ function tuple(items, _paramsOrRest, _params) {
5734
+ const hasRest = _paramsOrRest instanceof $ZodType;
5735
+ return new ZodTuple({
5736
+ type: "tuple",
5737
+ items,
5738
+ rest: hasRest ? _paramsOrRest : null,
5739
+ ...normalizeParams(hasRest ? _params : _paramsOrRest)
5740
+ });
5741
+ }
5549
5742
  const ZodEnum = /*@__PURE__*/ $constructor("ZodEnum", (inst, def) => {
5550
5743
  $ZodEnum.init(inst, def);
5551
5744
  ZodType.init(inst, def);
@@ -6910,7 +7103,7 @@ function createInboundMessaging(options) {
6910
7103
  grantId: value.id,
6911
7104
  grantRevision: value.revision,
6912
7105
  defaultRevision: messagingDefaults(db, value.platform).revision,
6913
- receptionRevision: groupReceptionPolicy(db, value.id).revision,
7106
+ ...event.conversation.kind === "group" ? { receptionRevision: groupReceptionPolicy(db, value.id).revision } : {},
6914
7107
  ...value.channelBridge ? { bridgeRevision: value.channelBridge.revision } : {},
6915
7108
  ...!value.bridgeRoutes && value.receiveTargetChannelId ? { localChannelId: value.receiveTargetChannelId } : {}
6916
7109
  };
@@ -6927,13 +7120,25 @@ function createInboundMessaging(options) {
6927
7120
  if (!row) throw new MessagingError("grant-unavailable");
6928
7121
  return JSON.parse(row.body);
6929
7122
  };
7123
+ const receptionChanged = () => {
7124
+ if (closed) return;
7125
+ try {
7126
+ options.onReceptionChanged?.();
7127
+ } catch {
7128
+ options.warn?.("messaging-reception-publication-failed");
7129
+ }
7130
+ };
6930
7131
  const stop = (id) => {
6931
7132
  clearTimeout(retries.get(id)?.timer);
6932
7133
  retries.delete(id);
6933
7134
  const lease = leases.get(id);
6934
7135
  leases.delete(id);
6935
7136
  lease?.controller.abort();
6936
- lease?.dispose?.();
7137
+ try {
7138
+ lease?.dispose?.();
7139
+ } finally {
7140
+ if (lease) receptionChanged();
7141
+ }
6937
7142
  };
6938
7143
  const targetAvailable = (value) => {
6939
7144
  if (value.bridgeRoutes) return true;
@@ -6980,11 +7185,12 @@ function createInboundMessaging(options) {
6980
7185
  dispose: void 0
6981
7186
  };
6982
7187
  leases.set(value.id, lease);
7188
+ receptionChanged();
6983
7189
  const startedAt = Date.now();
6984
7190
  try {
6985
7191
  const inspected = await entry.provider.inspect(value.accountRef, value.targetRef);
6986
- if (inspected.account.fingerprint !== value.fingerprint || inspected.target.digest !== value.targetDigest || (value.receiveScope ? inspected.target.receiveScope?.conversationId !== value.receiveScope.conversationId : !inspected.target.receiveScope)) throw new MessagingError("rebind-required");
6987
- transaction((db) => initializeGroupReceptionPolicy(db, value.id));
7192
+ if (inspected.account.fingerprint !== value.fingerprint || inspected.target.digest !== value.targetDigest || (value.receiveScope ? inspected.target.receiveScope?.kind !== value.receiveScope.kind || inspected.target.receiveScope?.conversationId !== value.receiveScope.conversationId : !inspected.target.receiveScope)) throw new MessagingError("rebind-required");
7193
+ if (value.receiveScope?.kind === "group") transaction((db) => initializeGroupReceptionPolicy(db, value.id));
6988
7194
  const dispose = await entry.consume({
6989
7195
  accountRef: value.accountRef,
6990
7196
  fingerprint: value.fingerprint,
@@ -7006,7 +7212,25 @@ function createInboundMessaging(options) {
7006
7212
  if (closed || leases.get(value.id) !== lease || providers.get(value.providerId)?.token !== entry.token || latest.revision !== value.revision || latest.revokedAt || latest.suspendedReason || options.bindingAvailable?.(latest.bindingId) === false || !options.isBotActive(value.botSlug)) throw new MessagingError("consumer-unavailable");
7007
7213
  if (event.fingerprint !== value.fingerprint || event.botId !== value.accountRef) throw new MessagingError("untrusted-source");
7008
7214
  if (!latest.receiveScope) return { accepted: true };
7009
- if (event.conversation.kind !== "group" || event.conversation.id !== value.receiveScope.conversationId) return { accepted: true };
7215
+ if (event.conversation.kind !== latest.receiveScope.kind || event.conversation.id !== value.receiveScope.conversationId) return { accepted: true };
7216
+ if (event.conversation.kind === "dm") {
7217
+ if (latest.receiveTargetChannelId || latest.channelBridge || latest.bridgeRoutes) throw new MessagingError("capability-unavailable");
7218
+ const id = transaction((db) => {
7219
+ signal.throwIfAborted();
7220
+ lease.controller.signal.throwIfAborted();
7221
+ if (latest.receiveAfter && Date.parse(event.at) < Date.parse(latest.receiveAfter)) return void 0;
7222
+ const sourceEventId = persistSource(db, latest, event);
7223
+ const policy = options.sourcePolicy.resolveIn(db, latest.botSlug, "human-dm");
7224
+ db.prepare(`INSERT OR IGNORE INTO inbox_admissions
7225
+ (source_event_id, bot_slug, reason, source_policy_revision, source_policy_wake_mode,
7226
+ wake_policy_revision, wake_mode) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(sourceEventId, latest.botSlug, "human-dm", policy.revision, policy.wake, policy.revision, policy.wake === "immediate" ? "all" : policy.wake);
7227
+ return sourceEventId;
7228
+ }, ["source-event", "bot-inbox"]);
7229
+ if (id !== void 0) setImmediate(() => {
7230
+ if (!closed && valid(grant(value.id))) options.onAdmitted(value.botSlug, id);
7231
+ });
7232
+ return { accepted: true };
7233
+ }
7010
7234
  if (!event.mentionedAccount) {
7011
7235
  lease.ordinaryVerified = true;
7012
7236
  if (ordinaryThreadReply(event)) lease.ordinaryThreads.set(event.reply.threadId, event.reply.rootId);
@@ -7183,6 +7407,7 @@ function createInboundMessaging(options) {
7183
7407
  return;
7184
7408
  }
7185
7409
  lease.dispose = dispose;
7410
+ receptionChanged();
7186
7411
  notifyPending(grant(value.id));
7187
7412
  options.warn?.(JSON.stringify({
7188
7413
  event: "messaging-inbound",
@@ -7192,7 +7417,10 @@ function createInboundMessaging(options) {
7192
7417
  }));
7193
7418
  } catch (error) {
7194
7419
  lease.controller.abort();
7195
- if (leases.get(value.id) === lease) leases.delete(value.id);
7420
+ if (leases.get(value.id) === lease) {
7421
+ leases.delete(value.id);
7422
+ receptionChanged();
7423
+ }
7196
7424
  const reason = error instanceof MessagingError ? error.code : "consumer-unavailable";
7197
7425
  const delay = [
7198
7426
  250,
@@ -7315,6 +7543,7 @@ function createInboundMessaging(options) {
7315
7543
  async channelBridge(channelId, rawInput) {
7316
7544
  const input = channelBridgeInput.parse(rawInput);
7317
7545
  const value = grant(input.grantId);
7546
+ if (value.platform === "weixin") throw new MessagingError("capability-unavailable");
7318
7547
  const channel = database.read((db) => bridgeChannel(db, channelId, value.botSlug, true));
7319
7548
  const target = input.delivery === "inbox" ? null : channelId;
7320
7549
  if (target === null && channel.type !== "dm") throw new MessagingError("channel-unavailable");
@@ -7392,6 +7621,7 @@ function createInboundMessaging(options) {
7392
7621
  if (providers.get(value.providerId) !== entry || inspected.account.fingerprint !== value.fingerprint || inspected.target.digest !== value.targetDigest) throw new MessagingError("rebind-required");
7393
7622
  scope = inspected.target.receiveScope;
7394
7623
  if (!scope) throw new MessagingError("group-required");
7624
+ if (scope.kind === "dm" && (value.receiveTargetChannelId || value.channelBridge || value.bridgeRoutes)) throw new MessagingError("capability-unavailable");
7395
7625
  }
7396
7626
  const updated = database.transaction((db) => {
7397
7627
  const latest = grant(id);
@@ -8034,6 +8264,7 @@ function createOutboundMessaging(options) {
8034
8264
  sourcePolicy: options.sourcePolicy ?? createBotSourcePolicyStore(database),
8035
8265
  isBotActive: options.isBotActive,
8036
8266
  onAdmitted: options.onAdmitted ?? (() => void 0),
8267
+ ...options.onReceptionChanged ? { onReceptionChanged: options.onReceptionChanged } : {},
8037
8268
  ...options.onPlaced ? { onPlaced: options.onPlaced } : {},
8038
8269
  ...options.onShared ? { onShared: options.onShared } : {},
8039
8270
  ...options.warn === void 0 ? {} : { warn: options.warn }
@@ -8326,7 +8557,7 @@ function createOutboundMessaging(options) {
8326
8557
  const channel = database.read((db) => humanBridgeChannel(db, channelId));
8327
8558
  const snapshots = await Promise.all(channel.members.map((slug) => service.snapshot(slug)));
8328
8559
  const current = database.read((db) => humanBridgeChannel(db, channelId));
8329
- const grants = snapshots.flatMap((snapshot, index) => current.members.includes(channel.members[index]) ? snapshot.grants : []);
8560
+ const grants = snapshots.flatMap((snapshot, index) => current.members.includes(channel.members[index]) ? snapshot.grants.filter((grant) => grant.platform !== "weixin") : []);
8330
8561
  const source = (g) => ({
8331
8562
  grantId: g.id,
8332
8563
  grantRevision: g.revision,
@@ -8399,7 +8630,7 @@ function createOutboundMessaging(options) {
8399
8630
  reception: inbound.status(value.id),
8400
8631
  canReceive,
8401
8632
  canPost,
8402
- ...current.revokedAt === void 0 && !current.suspendedReason && options.isBotActive(botSlug) ? {
8633
+ ...current.revokedAt === void 0 && !current.suspendedReason && options.isBotActive(botSlug) && current.receiveScope?.kind !== "dm" && current.platform !== "weixin" ? {
8403
8634
  groupPolicy: inbound.policy(botSlug, value.id),
8404
8635
  threadPolicies: inbound.threads(botSlug, value.id),
8405
8636
  ordinaryDelivery: inbound.ordinaryDelivery(value.id)
@@ -8758,7 +8989,8 @@ const inboundSchema = object({
8758
8989
  channel: _enum([
8759
8990
  "feishu",
8760
8991
  "slack",
8761
- "discord"
8992
+ "discord",
8993
+ "weixin"
8762
8994
  ]),
8763
8995
  botId: identifier,
8764
8996
  fingerprint: string().regex(/^[a-f0-9]{64}$/),
@@ -8822,6 +9054,8 @@ function providerFailure(error) {
8822
9054
  "bot-not-connected",
8823
9055
  "bad-request",
8824
9056
  "stale-route",
9057
+ "source-not-found",
9058
+ "reply-permission-denied",
8825
9059
  "consumer-unavailable",
8826
9060
  "file-upload-failed",
8827
9061
  "file-provider-rejected"
@@ -8856,6 +9090,9 @@ function createDshImProvider(value, platform = "feishu") {
8856
9090
  ...platform === "feishu" && target.kind === "group" && typeof target.route.chatId === "string" || platform === "slack" && target.kind === "conversation" && typeof target.route.channelId === "string" || platform === "discord" && target.kind === "channel" && typeof target.route.channelId === "string" ? { receiveScope: {
8857
9091
  kind: "group",
8858
9092
  conversationId: String(platform === "feishu" ? target.route.chatId : target.route.channelId)
9093
+ } } : platform === "weixin" && target.kind === "user" && typeof target.route.toUserId === "string" ? { receiveScope: {
9094
+ kind: "dm",
9095
+ conversationId: target.route.toUserId
8859
9096
  } } : {}
8860
9097
  }));
8861
9098
  return {
@@ -8983,12 +9220,18 @@ function createDshImProvider(value, platform = "feishu") {
8983
9220
  const receipt = object({
8984
9221
  version: literal(1),
8985
9222
  messageId: identifier,
8986
- conversationId: identifier
9223
+ conversationId: identifier,
9224
+ identityKind: literal("client-acknowledgement").optional()
8987
9225
  }).strict().safeParse(result.receipt);
8988
- if (!receipt.success || receipt.data.conversationId !== input.route.conversationId) throw new MessagingProviderError("provider-result-unknown", "unknown");
9226
+ if (!receipt.success || receipt.data.conversationId !== input.route.conversationId || (platform === "weixin" ? receipt.data.identityKind !== "client-acknowledgement" : receipt.data.identityKind !== void 0)) throw new MessagingProviderError("provider-result-unknown", "unknown");
8989
9227
  return {
8990
9228
  accepted: true,
8991
- receipt: receipt.data
9229
+ receipt: {
9230
+ version: 1,
9231
+ messageId: receipt.data.messageId,
9232
+ conversationId: receipt.data.conversationId,
9233
+ ...receipt.data.identityKind ? { identityKind: receipt.data.identityKind } : {}
9234
+ }
8992
9235
  };
8993
9236
  }
8994
9237
  return { accepted: true };
@@ -17383,20 +17626,78 @@ function isModelPreset(value) {
17383
17626
  const preset = value;
17384
17627
  return typeof preset["id"] === "string" && typeof preset["name"] === "string" && Number.isSafeInteger(preset["revision"]) && typeof preset["createdAt"] === "string" && isModelRoute(preset["orchestrator"]) && isModelRoute(preset["assignmentDefault"]) && (preset["assignmentModels"] === void 0 || isAssignmentModelsForDefault(preset["assignmentDefault"], preset["assignmentModels"]));
17385
17628
  }
17386
- function createModelPresetStore(rootDir, now = () => /* @__PURE__ */ new Date()) {
17387
- const file = join(rootDir, "model-presets.json");
17388
- const read = () => {
17389
- let body;
17629
+ function presetRecord(preset) {
17630
+ const route = (value) => ({
17631
+ provider: value.provider,
17632
+ model: value.model,
17633
+ ...value.reasoningEffort === void 0 ? {} : { reasoningEffort: value.reasoningEffort }
17634
+ });
17635
+ return {
17636
+ id: preset.id,
17637
+ name: preset.name,
17638
+ revision: preset.revision,
17639
+ createdAt: preset.createdAt,
17640
+ orchestrator: route(preset.orchestrator),
17641
+ assignmentDefault: route(preset.assignmentDefault),
17642
+ ...preset.assignmentModels === void 0 ? {} : { assignmentModels: preset.assignmentModels.map((option) => ({
17643
+ provider: option.provider,
17644
+ model: option.model,
17645
+ allowedEfforts: [...option.allowedEfforts],
17646
+ defaultEffort: option.defaultEffort
17647
+ })) }
17648
+ };
17649
+ }
17650
+ function createModelPresetStore(options) {
17651
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
17652
+ const reportImport = (event) => {
17390
17653
  try {
17391
- body = readFileSync(file, "utf8");
17654
+ options.onImport?.(event);
17655
+ } catch {}
17656
+ };
17657
+ const port = attachOperationalModule(options.database, "model-presets");
17658
+ const read = () => port.read((database) => {
17659
+ return database.prepare("SELECT body FROM model_presets ORDER BY rowid").all().map((row) => {
17660
+ const parsed = JSON.parse(String(row["body"]));
17661
+ if (!isModelPreset(parsed)) throw new Error("Invalid Model Preset record");
17662
+ return parsed;
17663
+ });
17664
+ });
17665
+ const write = (preset) => {
17666
+ port.transaction((database) => {
17667
+ database.prepare(`INSERT INTO model_presets (id, body) VALUES (?, ?)
17668
+ ON CONFLICT (id) DO UPDATE SET body = excluded.body`).run(preset.id, JSON.stringify(presetRecord(preset)));
17669
+ }, ["model-presets"]);
17670
+ };
17671
+ if (options.database.mode === "ready" && !port.read((database) => database.prepare("SELECT singleton FROM model_presets_import WHERE singleton = 1").get())) {
17672
+ const startedAt = performance.now();
17673
+ try {
17674
+ let legacy;
17675
+ try {
17676
+ legacy = JSON.parse(readFileSync(join(options.rootDir, "model-presets.json"), "utf8"));
17677
+ } catch (error) {
17678
+ if (error.code !== "ENOENT") throw new Error("Invalid Model Preset import source");
17679
+ legacy = [];
17680
+ }
17681
+ if (!Array.isArray(legacy) || !legacy.every(isModelPreset) || legacy.some((preset) => !preset.id.trim() || !preset.name.trim() || preset.revision < 1) || new Set(legacy.map((preset) => preset.id)).size !== legacy.length || new Set(legacy.map((preset) => preset.name.toLowerCase())).size !== legacy.length) throw new Error("Invalid Model Preset import source");
17682
+ port.transaction((database) => {
17683
+ if (database.prepare("SELECT id FROM model_presets LIMIT 1").get()) throw new Error("Model Preset target is populated without an import marker");
17684
+ const insert = database.prepare("INSERT INTO model_presets (id, body) VALUES (?, ?)");
17685
+ for (const preset of legacy) insert.run(preset.id, JSON.stringify(presetRecord(preset)));
17686
+ database.prepare("INSERT INTO model_presets_import (singleton, imported_at) VALUES (1, ?)").run(now().toISOString());
17687
+ }, ["model-presets"]);
17688
+ reportImport({
17689
+ phase: "complete",
17690
+ count: legacy.length,
17691
+ durationMs: performance.now() - startedAt
17692
+ });
17392
17693
  } catch (error) {
17393
- if (error.code === "ENOENT") return [];
17694
+ reportImport({
17695
+ phase: "failed",
17696
+ durationMs: performance.now() - startedAt
17697
+ });
17394
17698
  throw error;
17395
17699
  }
17396
- const parsed = JSON.parse(body);
17397
- if (!Array.isArray(parsed) || !parsed.every(isModelPreset)) throw new Error("Invalid Model Preset store");
17398
- return parsed;
17399
- };
17700
+ }
17400
17701
  return {
17401
17702
  list: read,
17402
17703
  get: (id) => read().find((preset) => preset.id === id),
@@ -17404,8 +17705,7 @@ function createModelPresetStore(rootDir, now = () => /* @__PURE__ */ new Date())
17404
17705
  if (input.assignmentModels !== void 0) validateAssignmentModels(input.assignmentDefault, input.assignmentModels);
17405
17706
  const name = input.name.trim();
17406
17707
  if (name.length === 0 || name.length > 100) throw new Error("Model Preset name is required");
17407
- const presets = read();
17408
- if (presets.some((preset) => preset.name.toLowerCase() === name.toLowerCase())) throw new Error("Model Preset name already exists");
17708
+ if (read().some((preset) => preset.name.toLowerCase() === name.toLowerCase())) throw new Error("Model Preset name already exists");
17409
17709
  const preset = {
17410
17710
  id: randomUUID(),
17411
17711
  name,
@@ -17418,8 +17718,7 @@ function createModelPresetStore(rootDir, now = () => /* @__PURE__ */ new Date())
17418
17718
  })) },
17419
17719
  createdAt: now().toISOString()
17420
17720
  };
17421
- mkdirSync(dirname(file), { recursive: true });
17422
- atomicWriteFile(file, `${JSON.stringify([...presets, preset], null, 2)}\n`);
17721
+ write(preset);
17423
17722
  return preset;
17424
17723
  },
17425
17724
  update(id, input) {
@@ -17444,8 +17743,7 @@ function createModelPresetStore(rootDir, now = () => /* @__PURE__ */ new Date())
17444
17743
  allowedEfforts: [...option.allowedEfforts]
17445
17744
  })) }
17446
17745
  };
17447
- presets[index] = updated;
17448
- atomicWriteFile(file, `${JSON.stringify(presets, null, 2)}\n`);
17746
+ write(updated);
17449
17747
  return updated;
17450
17748
  }
17451
17749
  };
@@ -19324,33 +19622,50 @@ function nextGlobalState(state, patch) {
19324
19622
  if (topOrder !== void 0) next.topOrder = [...topOrder];
19325
19623
  return next;
19326
19624
  }
19625
+ const arrangementRecord = object({
19626
+ state: rosterDomainState,
19627
+ sections: array(tuple([string().min(1), rosterSectionRecord]))
19628
+ });
19629
+ function validateArrangement(value) {
19630
+ const parsed = arrangementRecord.parse(value);
19631
+ const sectionIds = /* @__PURE__ */ new Set();
19632
+ const membership = /* @__PURE__ */ new Set();
19633
+ for (const [id, section] of parsed.sections) {
19634
+ if (sectionIds.has(id)) throw new Error("Duplicate roster section");
19635
+ sectionIds.add(id);
19636
+ for (const channelId of section.channelIds) {
19637
+ if (channelId.length === 0 || membership.has(channelId)) throw new Error("Invalid roster membership");
19638
+ membership.add(channelId);
19639
+ }
19640
+ }
19641
+ return parsed;
19642
+ }
19327
19643
  var RosterStore = class {
19328
19644
  options;
19329
- domain;
19330
- table;
19331
- global;
19645
+ database;
19646
+ imported = false;
19647
+ draft;
19332
19648
  opening;
19333
19649
  closing = false;
19334
19650
  warned = false;
19335
19651
  tail = Promise.resolve();
19336
- constructor(options = {}) {
19652
+ constructor(options) {
19337
19653
  this.options = options;
19654
+ this.database = attachOperationalModule(options.database, "roster");
19655
+ if (options.database.mode === "ready") {
19656
+ this.imported = this.database.read((database) => database.prepare("SELECT singleton FROM roster_arrangement_import WHERE singleton = 1").get() !== void 0);
19657
+ if (this.imported) this.readData();
19658
+ }
19338
19659
  }
19339
19660
  get available() {
19340
- return this.domain !== void 0;
19661
+ return this.imported && !this.closing && this.options.database.mode === "ready";
19341
19662
  }
19342
19663
  async attach(facility) {
19343
19664
  this.closing = false;
19344
- const opening = facility.open(rosterDomainSpec).then(async (domain) => {
19345
- if (this.closing) {
19346
- await domain.close();
19347
- return;
19348
- }
19349
- this.domain = domain;
19350
- this.table = domain.table("sections");
19351
- this.global = domain.global;
19352
- this.warned = false;
19353
- });
19665
+ if (this.imported) return;
19666
+ if (this.opening !== void 0) return this.opening;
19667
+ if (this.options.database.mode !== "ready") throw new RosterUnavailableError();
19668
+ const opening = this.importLegacy(facility);
19354
19669
  this.opening = opening;
19355
19670
  try {
19356
19671
  await opening;
@@ -19358,15 +19673,36 @@ var RosterStore = class {
19358
19673
  if (this.opening === opening) this.opening = void 0;
19359
19674
  }
19360
19675
  }
19676
+ async importLegacy(facility) {
19677
+ const started = performance.now();
19678
+ let domain;
19679
+ try {
19680
+ domain = await facility.open(rosterDomainSpec);
19681
+ if (this.closing) return;
19682
+ const record = validateArrangement({
19683
+ state: domain.global.get(),
19684
+ sections: [...domain.table("sections").entries()]
19685
+ });
19686
+ this.database.transaction((database) => {
19687
+ if (database.prepare("SELECT singleton FROM roster_arrangement WHERE singleton = 1").get() !== void 0) throw new Error("Roster target exists without an import marker");
19688
+ database.prepare("INSERT INTO roster_arrangement(singleton, body) VALUES (1, ?)").run(JSON.stringify(record));
19689
+ database.prepare("INSERT INTO roster_arrangement_import(singleton, imported_at) VALUES (1, ?)").run((/* @__PURE__ */ new Date()).toISOString());
19690
+ });
19691
+ this.imported = true;
19692
+ this.warned = false;
19693
+ this.notifyCommitted();
19694
+ this.warn(`roster-import initiator=host-startup phase=completed count=${record.sections.length} durationMs=${Math.round(performance.now() - started)}`);
19695
+ } catch {
19696
+ this.warn(`roster-import initiator=host-startup phase=failed durationMs=${Math.round(performance.now() - started)}`);
19697
+ throw new RosterUnavailableError();
19698
+ } finally {
19699
+ if (domain !== void 0) await domain.close().catch(() => this.warn("roster-import initiator=host-startup phase=source-close-failed"));
19700
+ }
19701
+ }
19361
19702
  async detach() {
19362
19703
  this.closing = true;
19363
- const opening = this.opening;
19364
- if (opening !== void 0) await opening.catch(() => {});
19365
- const domain = this.domain;
19366
- this.domain = void 0;
19367
- this.table = void 0;
19368
- this.global = void 0;
19369
- if (domain !== void 0) await domain.close();
19704
+ if (this.opening !== void 0) await this.opening.catch(() => {});
19705
+ await this.tail;
19370
19706
  }
19371
19707
  snapshot() {
19372
19708
  const table = this.requireTable();
@@ -19383,27 +19719,22 @@ var RosterStore = class {
19383
19719
  };
19384
19720
  }
19385
19721
  sectionCreate(name) {
19386
- return this.enqueue(async () => {
19722
+ return this.enqueue(() => {
19387
19723
  const table = this.requireTable();
19388
19724
  const global = this.requireGlobal();
19389
19725
  const id = randomUUID();
19390
- await table.put(id, {
19726
+ table.put(id, {
19391
19727
  name,
19392
19728
  channelIds: []
19393
19729
  });
19394
19730
  const state = global.get();
19395
- try {
19396
- await global.set(nextGlobalState(state, {
19397
- sectionOrder: [id, ...state.sectionOrder],
19398
- topOrder: state.topOrder === void 0 ? void 0 : [{
19399
- kind: "section",
19400
- id
19401
- }, ...state.topOrder]
19402
- }));
19403
- } catch (error) {
19404
- await table.delete(id).catch(() => {});
19405
- throw error;
19406
- }
19731
+ global.set(nextGlobalState(state, {
19732
+ sectionOrder: [id, ...state.sectionOrder],
19733
+ topOrder: state.topOrder === void 0 ? void 0 : [{
19734
+ kind: "section",
19735
+ id
19736
+ }, ...state.topOrder]
19737
+ }));
19407
19738
  return {
19408
19739
  id,
19409
19740
  name,
@@ -19412,11 +19743,11 @@ var RosterStore = class {
19412
19743
  });
19413
19744
  }
19414
19745
  sectionRename(sectionId, name) {
19415
- return this.enqueue(async () => {
19746
+ return this.enqueue(() => {
19416
19747
  const table = this.requireTable();
19417
19748
  const current = table.get(sectionId);
19418
19749
  if (current === void 0) throw new RosterUnknownSectionError(sectionId);
19419
- if (current.name !== name) await table.put(sectionId, {
19750
+ if (current.name !== name) table.put(sectionId, {
19420
19751
  name,
19421
19752
  channelIds: [...current.channelIds]
19422
19753
  });
@@ -19428,14 +19759,14 @@ var RosterStore = class {
19428
19759
  });
19429
19760
  }
19430
19761
  sectionRemove(sectionId) {
19431
- return this.enqueue(async () => {
19762
+ return this.enqueue(() => {
19432
19763
  const table = this.requireTable();
19433
19764
  const global = this.requireGlobal();
19434
19765
  const removed = table.get(sectionId);
19435
- if (!await table.delete(sectionId)) return false;
19766
+ if (!table.delete(sectionId)) return false;
19436
19767
  const state = global.get();
19437
19768
  const members = removed === void 0 ? [] : [...removed.channelIds];
19438
- await global.set(nextGlobalState(state, {
19769
+ global.set(nextGlobalState(state, {
19439
19770
  sectionOrder: state.sectionOrder.filter((id) => id !== sectionId),
19440
19771
  topOrder: state.topOrder === void 0 ? void 0 : sanitizeTopOrder(state.topOrder.flatMap((entry) => entry.kind === "section" && entry.id === sectionId ? members.map((id) => ({
19441
19772
  kind: "channel",
@@ -19446,7 +19777,7 @@ var RosterStore = class {
19446
19777
  });
19447
19778
  }
19448
19779
  channelAssign(channelId, sectionId, index) {
19449
- return this.enqueue(async () => {
19780
+ return this.enqueue(() => {
19450
19781
  const table = this.requireTable();
19451
19782
  const global = this.requireGlobal();
19452
19783
  if (sectionId !== void 0 && table.get(sectionId) === void 0) throw new RosterUnknownSectionError(sectionId);
@@ -19472,19 +19803,19 @@ var RosterStore = class {
19472
19803
  channelIds: without
19473
19804
  }]);
19474
19805
  }
19475
- for (const [id, record] of removals) await table.put(id, record);
19476
- if (target !== void 0) await table.put(target[0], target[1]);
19806
+ for (const [id, record] of removals) table.put(id, record);
19807
+ if (target !== void 0) table.put(target[0], target[1]);
19477
19808
  const state = global.get();
19478
19809
  if (state.topOrder === void 0) return;
19479
19810
  const next = sanitizeTopOrder(sectionId === void 0 ? [...state.topOrder, {
19480
19811
  kind: "channel",
19481
19812
  id: channelId
19482
19813
  }] : state.topOrder.filter((entry) => entry.kind !== "channel" || entry.id !== channelId), table);
19483
- if (!sameEntries(next, state.topOrder)) await global.set(nextGlobalState(state, { topOrder: next }));
19814
+ if (!sameEntries(next, state.topOrder)) global.set(nextGlobalState(state, { topOrder: next }));
19484
19815
  });
19485
19816
  }
19486
19817
  sectionReorder(order) {
19487
- return this.enqueue(async () => {
19818
+ return this.enqueue(() => {
19488
19819
  const table = this.requireTable();
19489
19820
  const global = this.requireGlobal();
19490
19821
  const current = this.orderedSections(global.get().sectionOrder).map((section) => section.id);
@@ -19515,7 +19846,7 @@ var RosterStore = class {
19515
19846
  }
19516
19847
  const storedTop = state.topOrder;
19517
19848
  if (sameIds(next, state.sectionOrder) && (topOrder === void 0 || storedTop !== void 0 && sameEntries(topOrder, storedTop))) return next;
19518
- await global.set(nextGlobalState(state, {
19849
+ global.set(nextGlobalState(state, {
19519
19850
  sectionOrder: next,
19520
19851
  topOrder
19521
19852
  }));
@@ -19523,19 +19854,19 @@ var RosterStore = class {
19523
19854
  });
19524
19855
  }
19525
19856
  topReorder(order) {
19526
- return this.enqueue(async () => {
19857
+ return this.enqueue(() => {
19527
19858
  const table = this.requireTable();
19528
19859
  const global = this.requireGlobal();
19529
19860
  const state = global.get();
19530
19861
  const next = sanitizeTopOrder(order, table);
19531
19862
  const current = state.topOrder === void 0 ? void 0 : sanitizeTopOrder(state.topOrder, table);
19532
19863
  if (current !== void 0 && sameEntries(next, current)) return next;
19533
- await global.set(nextGlobalState(state, { topOrder: next }));
19864
+ global.set(nextGlobalState(state, { topOrder: next }));
19534
19865
  return next;
19535
19866
  });
19536
19867
  }
19537
19868
  applyBatch(change, resolvePin = (pin) => pin) {
19538
- return this.enqueue(async () => {
19869
+ return this.enqueue(() => {
19539
19870
  const ids = uniqueStrings(change.channelIds);
19540
19871
  if (ids.length === 0 || change.channelIds.length > 100 || ids.length !== change.channelIds.length) throw new RangeError(`roster batch must contain 1–100 distinct channels`);
19541
19872
  const table = this.requireTable();
@@ -19546,19 +19877,19 @@ var RosterStore = class {
19546
19877
  if (change.action === "pin") {
19547
19878
  const existing = new Set(pins);
19548
19879
  const next = [...pins, ...ids.filter((id) => !existing.has(id))];
19549
- if (!sameIds(next, state.pins)) await global.set(nextGlobalState(state, { pins: next }));
19880
+ if (!sameIds(next, state.pins)) global.set(nextGlobalState(state, { pins: next }));
19550
19881
  return this.snapshot();
19551
19882
  }
19552
19883
  if (change.action === "hide") {
19553
19884
  const hidden = uniqueStrings(state.hidden ?? []);
19554
19885
  const existing = new Set(hidden);
19555
19886
  const next = [...hidden, ...ids.filter((id) => !existing.has(id))];
19556
- if (!sameIds(next, state.hidden ?? [])) await global.set(nextGlobalState(state, { hidden: next }));
19887
+ if (!sameIds(next, state.hidden ?? [])) global.set(nextGlobalState(state, { hidden: next }));
19557
19888
  return this.snapshot();
19558
19889
  }
19559
19890
  const nextPins = pins.filter((id) => !selected.has(id));
19560
19891
  if (change.action === "unpin") {
19561
- if (!sameIds(nextPins, state.pins)) await global.set(nextGlobalState(state, { pins: nextPins }));
19892
+ if (!sameIds(nextPins, state.pins)) global.set(nextGlobalState(state, { pins: nextPins }));
19562
19893
  return this.snapshot();
19563
19894
  }
19564
19895
  const sectionId = change.sectionId;
@@ -19572,12 +19903,12 @@ var RosterStore = class {
19572
19903
  channelIds
19573
19904
  }]);
19574
19905
  }
19575
- for (const [id, record] of updates) await table.put(id, record);
19906
+ for (const [id, record] of updates) table.put(id, record);
19576
19907
  const topOrder = state.topOrder === void 0 ? void 0 : sanitizeTopOrder([...state.topOrder.filter((entry) => entry.kind !== "channel" || !selected.has(entry.id)), ...sectionId === void 0 ? ids.map((id) => ({
19577
19908
  kind: "channel",
19578
19909
  id
19579
19910
  })) : []], table);
19580
- if (!sameIds(nextPins, state.pins) || topOrder !== void 0 && (state.topOrder === void 0 || !sameEntries(topOrder, state.topOrder))) await global.set(nextGlobalState(state, {
19911
+ if (!sameIds(nextPins, state.pins) || topOrder !== void 0 && (state.topOrder === void 0 || !sameEntries(topOrder, state.topOrder))) global.set(nextGlobalState(state, {
19581
19912
  pins: nextPins,
19582
19913
  topOrder
19583
19914
  }));
@@ -19585,20 +19916,20 @@ var RosterStore = class {
19585
19916
  });
19586
19917
  }
19587
19918
  pinsSet(pins) {
19588
- return this.enqueue(async () => {
19919
+ return this.enqueue(() => {
19589
19920
  const global = this.requireGlobal();
19590
19921
  const next = uniqueStrings(pins);
19591
19922
  const state = global.get();
19592
- if (!sameIds(next, state.pins)) await global.set(nextGlobalState(state, { pins: next }));
19923
+ if (!sameIds(next, state.pins)) global.set(nextGlobalState(state, { pins: next }));
19593
19924
  return next;
19594
19925
  });
19595
19926
  }
19596
19927
  hiddenSet(hidden) {
19597
- return this.enqueue(async () => {
19928
+ return this.enqueue(() => {
19598
19929
  const global = this.requireGlobal();
19599
19930
  const next = uniqueStrings(hidden);
19600
19931
  const state = global.get();
19601
- if (!sameIds(next, state.hidden ?? [])) await global.set(nextGlobalState(state, { hidden: next }));
19932
+ if (!sameIds(next, state.hidden ?? [])) global.set(nextGlobalState(state, { hidden: next }));
19602
19933
  return next;
19603
19934
  });
19604
19935
  }
@@ -19626,39 +19957,104 @@ var RosterStore = class {
19626
19957
  return sections;
19627
19958
  }
19628
19959
  enqueue(operation) {
19629
- const result = this.tail.then(async () => {
19630
- const value = await operation();
19960
+ const result = this.tail.then(() => {
19961
+ if (!this.available) {
19962
+ this.reportUnavailable();
19963
+ throw new RosterUnavailableError();
19964
+ }
19965
+ let value;
19631
19966
  try {
19632
- this.options.onCommitted?.();
19967
+ value = this.database.transaction((database) => {
19968
+ this.draft = this.readData();
19969
+ const before = JSON.stringify({
19970
+ state: this.draft.state,
19971
+ sections: [...this.draft.sections]
19972
+ });
19973
+ try {
19974
+ const outcome = operation();
19975
+ const after = JSON.stringify({
19976
+ state: this.draft.state,
19977
+ sections: [...this.draft.sections]
19978
+ });
19979
+ if (before !== after) database.prepare("UPDATE roster_arrangement SET body = ? WHERE singleton = 1").run(after);
19980
+ return outcome;
19981
+ } finally {
19982
+ this.draft = void 0;
19983
+ }
19984
+ }, ["roster"]);
19633
19985
  } catch (error) {
19634
- this.options.warn?.(`botharness: roster committed notification failed: ${String(error)}`);
19986
+ if (error instanceof OperationalDatabaseError && this.options.database.mode === "ready" && (error.cause instanceof RosterUnknownSectionError || error.cause instanceof RangeError)) throw error.cause;
19987
+ if (error instanceof OperationalDatabaseError) throw new RosterUnavailableError();
19988
+ throw error;
19635
19989
  }
19990
+ this.notifyCommitted();
19636
19991
  return value;
19637
19992
  });
19638
19993
  this.tail = result.then(() => {}, () => {});
19639
19994
  return result;
19640
19995
  }
19996
+ notifyCommitted() {
19997
+ try {
19998
+ this.options.onCommitted?.();
19999
+ } catch {
20000
+ this.warn("roster-notification initiator=host phase=observer-failed");
20001
+ }
20002
+ }
20003
+ warn(message) {
20004
+ try {
20005
+ this.options.warn?.(message);
20006
+ } catch {}
20007
+ }
19641
20008
  reportUnavailable() {
19642
20009
  if (this.warned) return;
19643
20010
  this.warned = true;
19644
- this.options.warn?.("botharness: storageDomain is unavailable; the roster arrangement is read-only");
20011
+ this.warn("roster-storage initiator=host phase=unavailable arrangement=read-only");
19645
20012
  }
19646
- requireTable() {
19647
- if (this.table === void 0) {
20013
+ readData() {
20014
+ if (this.draft !== void 0) return this.draft;
20015
+ if (!this.available) {
19648
20016
  this.reportUnavailable();
19649
20017
  throw new RosterUnavailableError();
19650
20018
  }
19651
- return this.table;
19652
- }
19653
- requireGlobal() {
19654
- if (this.global === void 0) {
19655
- this.reportUnavailable();
20019
+ try {
20020
+ return this.database.read((database) => {
20021
+ const row = database.prepare("SELECT body FROM roster_arrangement WHERE singleton = 1").get();
20022
+ if (typeof row?.body !== "string") throw new RosterUnavailableError();
20023
+ const record = validateArrangement(JSON.parse(row.body));
20024
+ return {
20025
+ state: record.state,
20026
+ sections: new Map(record.sections)
20027
+ };
20028
+ });
20029
+ } catch {
19656
20030
  throw new RosterUnavailableError();
19657
20031
  }
19658
- return this.global;
20032
+ }
20033
+ requireDraft() {
20034
+ if (this.draft === void 0) throw new RosterUnavailableError();
20035
+ return this.draft;
20036
+ }
20037
+ requireTable() {
20038
+ const sections = this.readData().sections;
20039
+ return {
20040
+ get: (id) => sections.get(id),
20041
+ entries: () => sections.entries(),
20042
+ put: (id, record) => {
20043
+ this.requireDraft().sections.set(id, record);
20044
+ },
20045
+ delete: (id) => this.requireDraft().sections.delete(id)
20046
+ };
20047
+ }
20048
+ requireGlobal() {
20049
+ return {
20050
+ get: () => this.readData().state,
20051
+ set: (state) => {
20052
+ this.requireDraft().state = state;
20053
+ }
20054
+ };
19659
20055
  }
19660
20056
  };
19661
- function createRosterStore(options = {}) {
20057
+ function createRosterStore(options) {
19662
20058
  return new RosterStore(options);
19663
20059
  }
19664
20060
  //#endregion
@@ -21551,6 +21947,30 @@ function createBridgeMethods(deps) {
21551
21947
  ...humanNickname === void 0 ? {} : { humanNickname }
21552
21948
  };
21553
21949
  };
21950
+ const marketplaceCall = async (operation) => {
21951
+ if (deps.marketplace === void 0) return {
21952
+ ok: false,
21953
+ error: {
21954
+ code: "marketplace-unavailable",
21955
+ message: "Bot Marketplace is unavailable"
21956
+ }
21957
+ };
21958
+ const result = await operation(deps.marketplace);
21959
+ if (result.ok) return result;
21960
+ deps.warn?.(JSON.stringify({
21961
+ module: "marketplace",
21962
+ initiator: "client",
21963
+ phase: "request-refused",
21964
+ reason: result.code
21965
+ }));
21966
+ return {
21967
+ ok: false,
21968
+ error: {
21969
+ code: result.code,
21970
+ message: result.code
21971
+ }
21972
+ };
21973
+ };
21554
21974
  const messagingCall = async (operation) => {
21555
21975
  if (deps.externalMessaging === void 0) return {
21556
21976
  ok: false,
@@ -22139,6 +22559,32 @@ function createBridgeMethods(deps) {
22139
22559
  value: detailOf(result.record)
22140
22560
  };
22141
22561
  },
22562
+ marketplaceList(payload) {
22563
+ const source = asObject(payload);
22564
+ const query = {};
22565
+ for (const key of [
22566
+ "cursor",
22567
+ "q",
22568
+ "topic"
22569
+ ]) {
22570
+ const value = parseOptional(source, key);
22571
+ if (!value.ok) return Promise.resolve(invalidInput(`invalid ${key}`));
22572
+ const trimmed = value.value?.trim();
22573
+ if (trimmed !== void 0 && trimmed.length > 0) query[key] = trimmed;
22574
+ }
22575
+ const sort = source["sort"];
22576
+ if (sort !== void 0 && sort !== "updated" && sort !== "stars") return Promise.resolve(invalidInput("invalid sort"));
22577
+ if (sort !== void 0) query.sort = sort;
22578
+ return marketplaceCall((client) => client.list(query));
22579
+ },
22580
+ marketplaceTopics() {
22581
+ return marketplaceCall((client) => client.topics());
22582
+ },
22583
+ marketplaceSubmit(payload) {
22584
+ const url = asObject(payload)["url"];
22585
+ if (typeof url !== "string" || url.trim().length === 0) return Promise.resolve(invalidInput("url is required"));
22586
+ return marketplaceCall((client) => client.submit(url.trim()));
22587
+ },
22142
22588
  update(payload) {
22143
22589
  const slug = asSlug(payload);
22144
22590
  if (slug === void 0) return invalidInput("slug is required");
@@ -24546,6 +24992,15 @@ var BotharnessBridgeService = class extends TypertRemoteService {
24546
24992
  developerModeSet(enabled) {
24547
24993
  return unwrap(this.methods.developerModeSet({ enabled }));
24548
24994
  }
24995
+ async marketplaceList(query) {
24996
+ return unwrapAsync(this.methods.marketplaceList(query ?? {}));
24997
+ }
24998
+ async marketplaceSubmit(url) {
24999
+ return unwrapAsync(this.methods.marketplaceSubmit({ url }));
25000
+ }
25001
+ async marketplaceTopics() {
25002
+ return unwrapAsync(this.methods.marketplaceTopics());
25003
+ }
24549
25004
  };
24550
25005
  markRemoteMethods(BotharnessBridgeService.prototype, [
24551
25006
  "messagingDefaults",
@@ -24664,12 +25119,155 @@ markRemoteMethods(BotharnessBridgeService.prototype, [
24664
25119
  "browserAccessSet",
24665
25120
  "browserProfileSet",
24666
25121
  "botAvatarSet",
24667
- "botAppearanceSet"
25122
+ "botAppearanceSet",
25123
+ "marketplaceList",
25124
+ "marketplaceSubmit",
25125
+ "marketplaceTopics"
24668
25126
  ]);
24669
25127
  function registerBridge(ctx, methods) {
24670
25128
  return new BotharnessBridgeService(ctx, methods);
24671
25129
  }
24672
25130
  //#endregion
25131
+ //#region packages/core/src/marketplace/client.ts
25132
+ const REQUEST_TIMEOUT_MS = 15e3;
25133
+ function record$1(value) {
25134
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
25135
+ }
25136
+ function parseEntry(value) {
25137
+ const source = record$1(value);
25138
+ if (source === void 0) return void 0;
25139
+ if (![
25140
+ "id",
25141
+ "owner",
25142
+ "name",
25143
+ "fullName",
25144
+ "pushedAt",
25145
+ "htmlUrl",
25146
+ "cloneUrl",
25147
+ "defaultBranch"
25148
+ ].every((key) => typeof source[key] === "string")) return void 0;
25149
+ const description = source["description"];
25150
+ const stars = source["stars"];
25151
+ const topics = source["topics"];
25152
+ const head = record$1(source["headCommit"]);
25153
+ if (description !== null && typeof description !== "string") return void 0;
25154
+ if (typeof stars !== "number" || !Array.isArray(topics)) return void 0;
25155
+ return {
25156
+ id: source["id"],
25157
+ owner: source["owner"],
25158
+ name: source["name"],
25159
+ fullName: source["fullName"],
25160
+ description,
25161
+ topics: topics.filter((topic) => typeof topic === "string"),
25162
+ stars,
25163
+ pushedAt: source["pushedAt"],
25164
+ htmlUrl: source["htmlUrl"],
25165
+ cloneUrl: source["cloneUrl"],
25166
+ defaultBranch: source["defaultBranch"],
25167
+ headCommit: head !== void 0 && typeof head["sha"] === "string" && typeof head["committedAt"] === "string" ? {
25168
+ sha: head["sha"],
25169
+ committedAt: head["committedAt"]
25170
+ } : null
25171
+ };
25172
+ }
25173
+ function parseMarketplacePage(value) {
25174
+ const source = record$1(value);
25175
+ if (source === void 0 || !Array.isArray(source["bots"])) return void 0;
25176
+ const bots = source["bots"].map(parseEntry);
25177
+ if (!bots.every((bot) => bot !== void 0)) return void 0;
25178
+ const nextCursor = source["nextCursor"];
25179
+ return typeof nextCursor === "string" ? {
25180
+ bots,
25181
+ nextCursor
25182
+ } : { bots };
25183
+ }
25184
+ function parseMarketplaceTopics(value) {
25185
+ const topics = record$1(value)?.["topics"];
25186
+ if (!Array.isArray(topics)) return void 0;
25187
+ const parsed = topics.map((item) => {
25188
+ const source = record$1(item);
25189
+ return typeof source?.["topic"] === "string" && typeof source["count"] === "number" ? {
25190
+ topic: source["topic"],
25191
+ count: source["count"]
25192
+ } : void 0;
25193
+ });
25194
+ return parsed.every((item) => item !== void 0) ? parsed : void 0;
25195
+ }
25196
+ function listPath(query) {
25197
+ const params = new URLSearchParams();
25198
+ if (query.sort !== void 0) params.set("sort", query.sort);
25199
+ if (query.q !== void 0) params.set("q", query.q);
25200
+ if (query.topic !== void 0) params.set("topic", query.topic);
25201
+ if (query.cursor !== void 0) params.set("cursor", query.cursor);
25202
+ const search = params.toString();
25203
+ return search.length === 0 ? "/v1/bots" : `/v1/bots?${search}`;
25204
+ }
25205
+ function parseMarketplaceSubmission(value) {
25206
+ const bot = parseEntry(record$1(value)?.["bot"]);
25207
+ return bot === void 0 ? void 0 : { bot };
25208
+ }
25209
+ function createMarketplaceClient(options) {
25210
+ const fetchImpl = options.fetchImpl ?? fetch;
25211
+ const baseUrl = options.baseUrl.replace(/\/+$/u, "");
25212
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
25213
+ const request = async (path, init, parse) => {
25214
+ let response;
25215
+ try {
25216
+ response = await fetchImpl(`${baseUrl}${path}`, {
25217
+ ...init,
25218
+ signal: AbortSignal.timeout(timeoutMs)
25219
+ });
25220
+ } catch {
25221
+ return {
25222
+ ok: false,
25223
+ code: "marketplace-unavailable"
25224
+ };
25225
+ }
25226
+ let body;
25227
+ try {
25228
+ body = await response.json();
25229
+ } catch {
25230
+ return {
25231
+ ok: false,
25232
+ code: "marketplace-unavailable"
25233
+ };
25234
+ }
25235
+ if (!response.ok) {
25236
+ const code = record$1(record$1(body)?.["error"])?.["code"];
25237
+ return {
25238
+ ok: false,
25239
+ code: typeof code === "string" ? code : "marketplace-unavailable"
25240
+ };
25241
+ }
25242
+ const value = parse(body);
25243
+ return value === void 0 ? {
25244
+ ok: false,
25245
+ code: "marketplace-unavailable"
25246
+ } : {
25247
+ ok: true,
25248
+ value
25249
+ };
25250
+ };
25251
+ return {
25252
+ list: (query) => request(listPath(query), {
25253
+ method: "GET",
25254
+ headers: { accept: "application/json" }
25255
+ }, parseMarketplacePage),
25256
+ topics: () => request("/v1/topics", {
25257
+ method: "GET",
25258
+ headers: { accept: "application/json" }
25259
+ }, parseMarketplaceTopics),
25260
+ submit: (url) => request("/v1/submissions", {
25261
+ method: "POST",
25262
+ headers: {
25263
+ accept: "application/json",
25264
+ "content-type": "application/json"
25265
+ },
25266
+ body: JSON.stringify({ url })
25267
+ }, parseMarketplaceSubmission)
25268
+ };
25269
+ }
25270
+ //#endregion
24673
25271
  //#region packages/core/src/bots/avatar-snapshot.ts
24674
25272
  function deriveAvatarAppearance(value) {
24675
25273
  if (!isAvatarRecipe(value)) return void 0;
@@ -24705,39 +25303,117 @@ function createPersonaBotRegistry(options) {
24705
25303
  const botDir = (slug) => join(rootDir, slug);
24706
25304
  const botFile = (slug) => join(botDir(slug), "bot.json");
24707
25305
  const defaultMemoryDir = (slug) => join(botDir(slug), "memory");
25306
+ const port = attachOperationalModule(options.database, "bot-registry");
25307
+ const recordSnapshot = (record) => {
25308
+ const result = {
25309
+ slug: record.slug,
25310
+ displayName: record.displayName,
25311
+ workspaces: [...record.workspaces],
25312
+ createdAt: record.createdAt
25313
+ };
25314
+ for (const key of [
25315
+ "roles",
25316
+ "tag",
25317
+ "description",
25318
+ "avatar",
25319
+ "appearance",
25320
+ "model",
25321
+ "preset",
25322
+ "memoryDir",
25323
+ "paused",
25324
+ "computerAccess",
25325
+ "browserAccess",
25326
+ "browserProfile"
25327
+ ]) if (record[key] !== void 0) Object.assign(result, { [key]: record[key] });
25328
+ const route = (value) => ({
25329
+ provider: value.provider,
25330
+ model: value.model,
25331
+ ...value.reasoningEffort === void 0 ? {} : { reasoningEffort: value.reasoningEffort }
25332
+ });
25333
+ const plan = record.modelPlan;
25334
+ if (plan !== void 0) result.modelPlan = {
25335
+ revision: plan.revision,
25336
+ sourcePresetId: plan.sourcePresetId,
25337
+ sourcePresetName: plan.sourcePresetName,
25338
+ appliedAt: plan.appliedAt,
25339
+ orchestrator: route(plan.orchestrator),
25340
+ assignmentDefault: route(plan.assignmentDefault),
25341
+ ...plan.assignmentModels === void 0 ? {} : { assignmentModels: plan.assignmentModels.map((option) => ({
25342
+ provider: option.provider,
25343
+ model: option.model,
25344
+ allowedEfforts: [...option.allowedEfforts],
25345
+ defaultEffort: option.defaultEffort
25346
+ })) }
25347
+ };
25348
+ return result;
25349
+ };
25350
+ const decode = (text, slug) => {
25351
+ const parsed = JSON.parse(text);
25352
+ if (typeof parsed === "object" && parsed !== null && "appearance" in parsed && !isUsableAvatarAppearance(parsed.appearance, parsed.avatar)) delete parsed.appearance;
25353
+ if (!isPersonaBotRecord(parsed, slug)) throw new Error("Invalid PersonaBot Registry record");
25354
+ return recordSnapshot(parsed);
25355
+ };
24708
25356
  const read = (slug) => {
24709
25357
  if (!isValidSlug(slug)) return void 0;
24710
- let text;
24711
- try {
24712
- text = readFileSync(botFile(slug), "utf8");
24713
- } catch (error) {
24714
- if (isMissing(error)) return void 0;
24715
- throw error;
24716
- }
24717
- let parsed;
24718
- try {
24719
- parsed = JSON.parse(text);
24720
- } catch {
24721
- return;
24722
- }
24723
- if (typeof parsed === "object" && parsed !== null && "appearance" in parsed && !isUsableAvatarAppearance(parsed.appearance, parsed.avatar)) delete parsed.appearance;
24724
- return isPersonaBotRecord(parsed, slug) ? parsed : void 0;
25358
+ return port.read((database) => {
25359
+ const row = database.prepare("SELECT body FROM persona_bots WHERE slug = ?").get(slug);
25360
+ return row === void 0 ? void 0 : decode(String(row["body"]), slug);
25361
+ });
24725
25362
  };
24726
25363
  const write = (record) => {
24727
- const dir = botDir(record.slug);
24728
- mkdirSync(dir, { recursive: true });
24729
- atomicWriteFile(botFile(record.slug), `${JSON.stringify(record, null, 2)}\n`);
24730
- };
24731
- const list = () => {
24732
- let entries;
25364
+ port.transaction((database) => {
25365
+ database.prepare(`INSERT INTO persona_bots (slug, body) VALUES (?, ?)
25366
+ ON CONFLICT (slug) DO UPDATE SET body = excluded.body`).run(record.slug, JSON.stringify(recordSnapshot(record)));
25367
+ }, ["bot-registry"]);
25368
+ };
25369
+ const erase = (slug) => {
25370
+ port.transaction((database) => {
25371
+ database.prepare("DELETE FROM persona_bots WHERE slug = ?").run(slug);
25372
+ }, ["bot-registry"]);
25373
+ };
25374
+ const list = () => port.read((database) => database.prepare("SELECT slug, body FROM persona_bots").all().map((row) => decode(String(row["body"]), String(row["slug"]))).sort((left, right) => left.slug.localeCompare(right.slug)));
25375
+ if (options.database.mode === "ready" && !port.read((database) => database.prepare("SELECT singleton FROM persona_bots_import WHERE singleton = 1").get())) {
25376
+ const startedAt = performance.now();
25377
+ const report = (phase, count) => {
25378
+ try {
25379
+ options.onImport?.({
25380
+ phase,
25381
+ ...count === void 0 ? {} : { count },
25382
+ durationMs: performance.now() - startedAt
25383
+ });
25384
+ } catch {}
25385
+ };
24733
25386
  try {
24734
- entries = readdirSync(rootDir, { withFileTypes: true });
24735
- } catch (error) {
24736
- if (isMissing(error)) return [];
24737
- throw error;
25387
+ let entries;
25388
+ try {
25389
+ entries = readdirSync(rootDir, { withFileTypes: true });
25390
+ } catch (error) {
25391
+ if (!isMissing(error)) throw error;
25392
+ entries = [];
25393
+ }
25394
+ const records = [];
25395
+ for (const entry of entries.filter((entry) => entry.isDirectory() && isValidSlug(entry.name))) {
25396
+ let text;
25397
+ try {
25398
+ text = readFileSync(botFile(entry.name), "utf8");
25399
+ } catch (error) {
25400
+ if (isMissing(error)) continue;
25401
+ throw error;
25402
+ }
25403
+ records.push(decode(text, entry.name));
25404
+ }
25405
+ port.transaction((database) => {
25406
+ if (database.prepare("SELECT slug FROM persona_bots LIMIT 1").get()) throw new Error("PersonaBot Registry target is populated without an import marker");
25407
+ const insert = database.prepare("INSERT INTO persona_bots (slug, body) VALUES (?, ?)");
25408
+ for (const record of records) insert.run(record.slug, JSON.stringify(record));
25409
+ database.prepare("INSERT INTO persona_bots_import (singleton, imported_at) VALUES (1, ?)").run(now().toISOString());
25410
+ }, ["bot-registry"]);
25411
+ report("complete", records.length);
25412
+ } catch {
25413
+ report("failed");
25414
+ throw new Error("PersonaBot Registry import failed; repair retained legacy records before retry");
24738
25415
  }
24739
- return entries.filter((entry) => entry.isDirectory()).map((entry) => read(entry.name)).filter((record) => record !== void 0).sort((left, right) => left.slug.localeCompare(right.slug));
24740
- };
25416
+ }
24741
25417
  const memoryDirOf = (record) => record.memoryDir ?? defaultMemoryDir(record.slug);
24742
25418
  const ensurePersonaFile = (memoryDir, body) => {
24743
25419
  mkdirSync(memoryDir, { recursive: true });
@@ -24850,7 +25526,7 @@ function createPersonaBotRegistry(options) {
24850
25526
  ok: false,
24851
25527
  reason: cloned.code
24852
25528
  };
24853
- if (existsSync(botDir(input.slug))) return {
25529
+ if (read(input.slug) !== void 0 || existsSync(botDir(input.slug))) return {
24854
25530
  ok: false,
24855
25531
  reason: "duplicate"
24856
25532
  };
@@ -24872,10 +25548,12 @@ function createPersonaBotRegistry(options) {
24872
25548
  recursive: true,
24873
25549
  force: true
24874
25550
  });
24875
- if (ownsBotDir && !created && !existsSync(botFile(input.slug))) rmSync(botDir(input.slug), {
24876
- recursive: true,
24877
- force: true
24878
- });
25551
+ if (ownsBotDir && !created && options.database.mode === "ready") try {
25552
+ if (read(input.slug) === void 0) rmSync(botDir(input.slug), {
25553
+ recursive: true,
25554
+ force: true
25555
+ });
25556
+ } catch {}
24879
25557
  }
24880
25558
  },
24881
25559
  get(slug) {
@@ -24889,7 +25567,7 @@ function createPersonaBotRegistry(options) {
24889
25567
  },
24890
25568
  remove(slug, removeOptions) {
24891
25569
  if (!isValidSlug(slug)) return false;
24892
- const exists = existsSync(botDir(slug));
25570
+ const exists = read(slug) !== void 0 || existsSync(botDir(slug));
24893
25571
  if (removeOptions?.purge === true) {
24894
25572
  const removeFiles = () => {
24895
25573
  if (exists) rmSync(botDir(slug), {
@@ -24899,10 +25577,11 @@ function createPersonaBotRegistry(options) {
24899
25577
  };
24900
25578
  if (options.onPurge === void 0) removeFiles();
24901
25579
  else options.onPurge(slug, removeFiles);
25580
+ erase(slug);
24902
25581
  return exists;
24903
25582
  }
24904
25583
  if (!exists) return false;
24905
- rmSync(botFile(slug), { force: true });
25584
+ erase(slug);
24906
25585
  return true;
24907
25586
  },
24908
25587
  memoryDirFor(slug) {
@@ -28585,6 +29264,49 @@ const BOT_HARNESS_SCHEMA_PLAN = defineSchemaPlan([
28585
29264
  BEGIN SELECT RAISE(ABORT, 'Messaging defaults revisions are immutable'); END;
28586
29265
  CREATE TRIGGER messaging_defaults_no_delete BEFORE DELETE ON messaging_default_revisions
28587
29266
  BEGIN SELECT RAISE(ABORT, 'Messaging defaults revisions are immutable'); END;
29267
+ `);
29268
+ }
29269
+ },
29270
+ {
29271
+ generation: 54,
29272
+ module: "model-presets",
29273
+ description: "Own reusable Model Presets and their one-time import marker in the Profile database",
29274
+ migrate(database) {
29275
+ database.exec(`
29276
+ CREATE TABLE model_presets (id TEXT PRIMARY KEY, body TEXT NOT NULL CHECK (json_valid(body)));
29277
+ CREATE TABLE model_presets_import (
29278
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1), imported_at TEXT NOT NULL
29279
+ );
29280
+ `);
29281
+ }
29282
+ },
29283
+ {
29284
+ generation: 55,
29285
+ module: "bot-registry",
29286
+ description: "Own PersonaBot identities and applied plans with a durable Registry import marker",
29287
+ migrate(database) {
29288
+ database.exec(`
29289
+ CREATE TABLE persona_bots (slug TEXT PRIMARY KEY, body TEXT NOT NULL CHECK (json_valid(body)));
29290
+ CREATE TABLE persona_bots_import (
29291
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1), imported_at TEXT NOT NULL
29292
+ );
29293
+ `);
29294
+ }
29295
+ },
29296
+ {
29297
+ generation: 56,
29298
+ module: "roster",
29299
+ description: "Own Channel sections and roster arrangement in the Profile database",
29300
+ migrate(database) {
29301
+ database.exec(`
29302
+ CREATE TABLE roster_arrangement (
29303
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
29304
+ body TEXT NOT NULL CHECK (json_valid(body))
29305
+ );
29306
+ CREATE TABLE roster_arrangement_import (
29307
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
29308
+ imported_at TEXT NOT NULL
29309
+ );
28588
29310
  `);
28589
29311
  }
28590
29312
  }
@@ -37089,7 +37811,7 @@ var BotRuntimeImplementation = class {
37089
37811
  }
37090
37812
  if (!row || !["pending", "retryable"].includes(row.attempt_state)) return;
37091
37813
  const active = this.#activeTurns.get(botSlug);
37092
- if (row.reason === "group-mention" && this.#delivery(botSlug, "group-mention") === "steer" && active && this.#agents.steerOrchestrator) {
37814
+ if ((row.reason === "group-mention" || row.reason === "human-dm") && this.#delivery(botSlug, row.reason) === "steer" && active && this.#agents.steerOrchestrator) {
37093
37815
  const source = this.#externalMessaging.inbound.read(botSlug, sourceEventId);
37094
37816
  const unit = {
37095
37817
  sourceEventId,
@@ -38467,13 +39189,16 @@ var BotRuntimeImplementation = class {
38467
39189
  WHERE source_kind IN ('memory-change', 'bridge-message')
38468
39190
  AND source_event_id IN (
38469
39191
  SELECT source_event_id FROM inbox_admissions
38470
- WHERE reason IN ('memory-change', 'group-mention', 'group-ordinary') AND attempt_state = 'retryable'
39192
+ WHERE reason IN ('memory-change', 'group-mention', 'group-ordinary', 'human-dm') AND attempt_state = 'retryable'
38471
39193
  AND side_effect_started_at IS NULL
38472
39194
  )
38473
39195
  `).run();
38474
39196
  database.prepare(`
38475
39197
  UPDATE inbox_admissions SET observed_at = NULL
38476
- WHERE reason IN ('memory-change', 'group-mention', 'group-ordinary') AND attempt_state = 'retryable'
39198
+ WHERE (reason IN ('memory-change', 'group-mention', 'group-ordinary') OR
39199
+ (reason = 'human-dm' AND source_event_id IN (
39200
+ SELECT source_event_id FROM source_events WHERE source_kind = 'bridge-message'
39201
+ ))) AND attempt_state = 'retryable'
38477
39202
  AND side_effect_started_at IS NULL
38478
39203
  `).run();
38479
39204
  }, ["source-event", "bot-inbox"]);
@@ -39779,7 +40504,7 @@ var BotRuntimeImplementation = class {
39779
40504
  AND a.attempt_state IN ('pending', 'retryable') AND a.observed_at IS NULL
39780
40505
  WINDOW policy AS (PARTITION BY g.id, a.wake_policy_revision, a.source_policy_revision, a.external_default_revision, a.reason, CASE WHEN json_extract(tp.body, '$.mode') = 'follow' THEN tp.thread_id ELSE '' END, CASE WHEN json_extract(tp.body, '$.mode') = 'follow' THEN tp.revision ELSE 0 END)
39781
40506
  ) SELECT source_event_id, body, created_at, attempt_state FROM pending
39782
- WHERE reason = 'group-mention' OR (reason = 'group-ordinary' AND (
40507
+ WHERE reason = 'group-mention' OR (reason = 'human-dm' AND wake_mode = 'all') OR (reason = 'group-ordinary' AND (
39783
40508
  wake_mode = 'all' OR (wake_mode = 'digest' AND (pending_count >= wake_count OR
39784
40509
  (julianday(?) - julianday(first_at)) * 86400000 >= wake_interval_ms)) OR
39785
40510
  (? = 1 AND has_mention = 1 AND wake_mode IN ('digest', 'mentions'))))
@@ -39907,7 +40632,7 @@ var BotRuntimeImplementation = class {
39907
40632
  this.#database.transaction((db) => {
39908
40633
  for (const id of ids) if (db.prepare(`UPDATE inbox_admissions SET observed_at = COALESCE(observed_at, ?),
39909
40634
  side_effect_started_at = COALESCE(side_effect_started_at, ?), attempt_state = 'running', last_error = NULL
39910
- WHERE bot_slug = ? AND source_event_id = ? AND reason IN ('group-mention', 'group-ordinary')
40635
+ WHERE bot_slug = ? AND source_event_id = ? AND reason IN ('group-mention', 'group-ordinary', 'human-dm')
39911
40636
  AND attempt_state IN ('pending', 'retryable') RETURNING source_event_id`).get(at, at, botSlug, id)) readAdmissions.add(id);
39912
40637
  }, ["bot-inbox"]);
39913
40638
  }
@@ -39963,7 +40688,7 @@ var BotRuntimeImplementation = class {
39963
40688
  this.#database.transaction((database) => database.prepare(`
39964
40689
  UPDATE inbox_admissions
39965
40690
  SET side_effect_started_at = COALESCE(side_effect_started_at, ?)
39966
- WHERE bot_slug = ? AND reason IN ('assignment-report', 'assignment-lifecycle', 'memory-change', 'group-mention', 'group-ordinary')
40691
+ WHERE bot_slug = ? AND reason IN ('assignment-report', 'assignment-lifecycle', 'memory-change', 'group-mention', 'group-ordinary', 'human-dm')
39967
40692
  AND attempt_state = 'running'
39968
40693
  AND source_event_id IN (${placeholders})
39969
40694
  `).run(this.#now().toISOString(), botSlug, ...sourceEventIds), ["bot-inbox"]);
@@ -39982,7 +40707,7 @@ var BotRuntimeImplementation = class {
39982
40707
  WHEN side_effect_started_at IS NULL THEN 'retryable'
39983
40708
  ELSE 'needs-repair'
39984
40709
  END
39985
- WHERE reason IN ('assignment-report', 'assignment-lifecycle', 'memory-change', 'group-mention', 'group-ordinary')
40710
+ WHERE reason IN ('assignment-report', 'assignment-lifecycle', 'memory-change', 'group-mention', 'group-ordinary', 'human-dm')
39986
40711
  AND attempt_state IN ('pending', 'retryable', 'running')
39987
40712
  AND (? IS NULL OR bot_slug = ?)
39988
40713
  AND source_event_id IN (${placeholders})
@@ -39994,7 +40719,7 @@ var BotRuntimeImplementation = class {
39994
40719
  const placeholders = sourceEventIds.map(() => "?").join(", ");
39995
40720
  this.#database.transaction((database) => database.prepare(`
39996
40721
  UPDATE inbox_admissions SET attempt_state = 'handled', handled_at = ?
39997
- WHERE reason IN ('assignment-report', 'assignment-lifecycle', 'memory-change', 'group-mention', 'group-ordinary')
40722
+ WHERE reason IN ('assignment-report', 'assignment-lifecycle', 'memory-change', 'group-mention', 'group-ordinary', 'human-dm')
39998
40723
  AND attempt_state = 'running'
39999
40724
  AND (? IS NULL OR bot_slug = ?)
40000
40725
  AND source_event_id IN (${placeholders})
@@ -48176,6 +48901,7 @@ function handleCompactionEvent(sink, sessionId, eventType) {
48176
48901
  }
48177
48902
  }
48178
48903
  const DEFAULT_AGENT_PRESET = "standard";
48904
+ const DEFAULT_MARKETPLACE_URL = "https://market.botharness.ai";
48179
48905
  const DEFAULT_CONFIG = {
48180
48906
  enabled: true,
48181
48907
  agentPreset: DEFAULT_AGENT_PRESET
@@ -48183,7 +48909,8 @@ const DEFAULT_CONFIG = {
48183
48909
  const Config = Schema.object({
48184
48910
  enabled: Schema.boolean().default(DEFAULT_CONFIG.enabled).description("启用 BotHarness core"),
48185
48911
  activityDetailConsumers: Schema.array(Schema.string()).default([]).description("Explicitly trusted Host Plugin names allowed to read Activity Tool details"),
48186
- agentPreset: Schema.string().default(DEFAULT_AGENT_PRESET).description("PersonaBot 会话加入的 DSH agent preset(提供 file/Shell/grep 等普通工具)")
48912
+ agentPreset: Schema.string().default(DEFAULT_AGENT_PRESET).description("PersonaBot 会话加入的 DSH agent preset(提供 file/Shell/grep 等普通工具)"),
48913
+ marketplaceUrl: Schema.string().default(DEFAULT_MARKETPLACE_URL).description("Bot Marketplace 服务地址")
48187
48914
  });
48188
48915
  function unavailableAgentAdapter() {
48189
48916
  const unavailable = () => Promise.reject(/* @__PURE__ */ new Error("BotHarness Agent runtime is unavailable outside a DSH Host"));
@@ -48199,45 +48926,65 @@ function unavailableAgentAdapter() {
48199
48926
  function createCore(options = {}) {
48200
48927
  const dshHome = options.dshHome ?? resolveDshHome();
48201
48928
  const rootDir = join(dshHome, "botharness", "bots");
48929
+ const operationalDatabase = mountOperationalDatabase({
48930
+ dshHome,
48931
+ schemaPlan: BOT_HARNESS_SCHEMA_PLAN
48932
+ });
48202
48933
  let usage;
48203
- const registry = createPersonaBotRegistry({
48204
- rootDir,
48205
- onDisplayNameChanged: () => {
48206
- try {
48207
- live?.publishRosterCommitted();
48208
- } catch {
48209
- options.warn?.("bot-name-publication-failed");
48934
+ let registry;
48935
+ let modelPresets;
48936
+ let roster;
48937
+ let live;
48938
+ try {
48939
+ registry = createPersonaBotRegistry({
48940
+ rootDir,
48941
+ database: operationalDatabase,
48942
+ onImport: (event) => options.warn?.(`bot-registry-import initiator=host-startup phase=${event.phase} count=${event.count ?? 0} durationMs=${Math.round(event.durationMs)}`),
48943
+ onDisplayNameChanged: () => {
48944
+ try {
48945
+ live?.publishRosterCommitted();
48946
+ } catch {
48947
+ options.warn?.("bot-name-publication-failed");
48948
+ }
48949
+ },
48950
+ onPurge: (slug, removeFiles) => {
48951
+ if (usage === void 0) throw new Error("Usage purge requires a ready operational database");
48952
+ usage.purgeBot(slug, removeFiles);
48953
+ },
48954
+ cloneMemory: (destination, url) => cloneMemoryRepository({
48955
+ destination,
48956
+ url
48957
+ }),
48958
+ initializeMemory: (memoryDir) => {
48959
+ const repository = ensureMemoryRepository({ memoryDir });
48960
+ return repository.ok ? { ok: true } : {
48961
+ ok: false,
48962
+ ...repository.code === "git-not-found" ? { code: "git-not-found" } : {},
48963
+ message: `${repository.code}: ${repository.message}`
48964
+ };
48210
48965
  }
48211
- },
48212
- onPurge: (slug, removeFiles) => {
48213
- if (usage === void 0) throw new Error("Usage purge requires a ready operational database");
48214
- usage.purgeBot(slug, removeFiles);
48215
- },
48216
- cloneMemory: (destination, url) => cloneMemoryRepository({
48217
- destination,
48218
- url
48219
- }),
48220
- initializeMemory: (memoryDir) => {
48221
- const repository = ensureMemoryRepository({ memoryDir });
48222
- return repository.ok ? { ok: true } : {
48223
- ok: false,
48224
- ...repository.code === "git-not-found" ? { code: "git-not-found" } : {},
48225
- message: `${repository.code}: ${repository.message}`
48226
- };
48227
- }
48228
- });
48229
- const modelPresets = createModelPresetStore(join(dshHome, "botharness"));
48966
+ });
48967
+ roster = createRosterStore({
48968
+ database: operationalDatabase,
48969
+ warn: options.warn,
48970
+ onCommitted: () => live?.publishRosterCommitted()
48971
+ });
48972
+ modelPresets = createModelPresetStore({
48973
+ rootDir: join(dshHome, "botharness"),
48974
+ database: operationalDatabase,
48975
+ onImport: (event) => options.warn?.(`model-presets-import initiator=host-startup phase=${event.phase} count=${event.count ?? 0} durationMs=${Math.round(event.durationMs)}`)
48976
+ });
48977
+ } catch (error) {
48978
+ operationalDatabase.close();
48979
+ throw error;
48980
+ }
48230
48981
  const states = createBotStateTracker();
48231
- let live;
48232
48982
  let runtime;
48233
48983
  const attachments = createAttachmentStore({ rootDir: join(dshHome, "botharness", "attachments") });
48234
- const operationalDatabase = mountOperationalDatabase({
48235
- dshHome,
48236
- schemaPlan: BOT_HARNESS_SCHEMA_PLAN
48237
- });
48238
48984
  const sourcePolicy = createBotSourcePolicyStore(attachOperationalModule(operationalDatabase, "bot-inbox"));
48239
48985
  const externalMessaging = createOutboundMessaging({
48240
48986
  onDefaultsChanged: () => live?.publishRosterCommitted(),
48987
+ onReceptionChanged: () => live?.publishRosterCommitted(),
48241
48988
  attachments,
48242
48989
  database: attachOperationalModule(operationalDatabase, "messaging"),
48243
48990
  sourcePolicy,
@@ -48302,6 +49049,7 @@ function createCore(options = {}) {
48302
49049
  const refreshDurableAttention = () => states.replaceDurableAttention(humanAttention.durableAttention().filter((row) => registry.get(row.botSlug) !== void 0));
48303
49050
  refreshDurableAttention();
48304
49051
  operationalDatabase.subscribe(({ topics }) => {
49052
+ if (topics.some((topic) => topic === "bindings" || topic === "grants")) live?.publishRosterCommitted();
48305
49053
  if (topics.some((topic) => [
48306
49054
  "assignments",
48307
49055
  "source-event",
@@ -48409,10 +49157,7 @@ function createCore(options = {}) {
48409
49157
  humanAttentionDecisions,
48410
49158
  attachments,
48411
49159
  live,
48412
- roster: createRosterStore({
48413
- warn: options.warn,
48414
- onCommitted: () => live?.publishRosterCommitted()
48415
- }),
49160
+ roster,
48416
49161
  runtime
48417
49162
  };
48418
49163
  }
@@ -48484,6 +49229,7 @@ function apply(ctx, config) {
48484
49229
  });
48485
49230
  publishDraft = (event) => core.live.publishDraft(event);
48486
49231
  ctx.effect(() => () => core.operationalDatabase.close(), "botharness: operational database");
49232
+ ctx.effect(() => () => core.roster.detach(), "botharness: roster");
48487
49233
  ctx.effect(() => {
48488
49234
  const controller = new AbortController();
48489
49235
  if (core.operationalDatabase.mode === "ready") core.channels.migrateAttachments?.(controller.signal).catch(() => {
@@ -48499,7 +49245,8 @@ function apply(ctx, config) {
48499
49245
  for (const platform of [
48500
49246
  "feishu",
48501
49247
  "slack",
48502
- "discord"
49248
+ "discord",
49249
+ "weixin"
48503
49250
  ]) {
48504
49251
  const provider = createDshImProvider(child.get("dshIm"), platform);
48505
49252
  if (provider !== void 0) child.effect(() => core.externalMessaging.register(provider));
@@ -48702,6 +49449,7 @@ function apply(ctx, config) {
48702
49449
  userQuestions,
48703
49450
  toolRules: core.toolRules,
48704
49451
  assignmentAccess: core.assignmentAccess,
49452
+ marketplace: createMarketplaceClient({ baseUrl: config.marketplaceUrl ?? "https://market.botharness.ai" }),
48705
49453
  developerMode: { set: (enabled) => developerModeTarget.gate?.set(enabled) },
48706
49454
  computerAccess: { changed: (slug) => {
48707
49455
  (ctx.get("botharnessComputerTools")?.reconcileBot?.(slug))?.catch((error) => {
@@ -48847,10 +49595,9 @@ function apply(ctx, config) {
48847
49595
  });
48848
49596
  }
48849
49597
  ctx.inject(["storageDomain"], (storageCtx) => {
48850
- core.roster.attach(storageCtx.storageDomain).catch((error) => {
48851
- ctx.logger.warn(`botharness: failed to open the roster domain: ${String(error)}`);
49598
+ core.roster.attach(storageCtx.storageDomain).catch(() => {
49599
+ ctx.logger.warn("roster-import initiator=host-startup phase=unavailable arrangement=read-only");
48852
49600
  });
48853
- storageCtx.effect(() => () => core.roster.detach(), "botharness: roster domain");
48854
49601
  });
48855
49602
  ctx.systemPrompt.section({
48856
49603
  name: "botharness:persona",