@rivus/gateway 0.18.0 → 0.18.2

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.
@@ -1,10 +1,11 @@
1
1
  import { S as deepFreeze$1, _ as loadMergedLocalEnvFile, a as createRivusModelManagementSocketServer, d as gatewayPackageDirectory, h as loadRivusDeploymentManifest, i as RIVUS_RELEASE_DESCRIPTOR, n as disposeRuntimeCacheEntries, o as createRivusModelManagementFailure, p as gatewaySkillsDirectory, r as invokeRuntimeControl, t as createRuntimeCache, v as resolveFeishuEndpointCredentials } from "../chunks/runtime-cache.js";
2
2
  import { t as createBackgroundSessionToolContracts } from "../chunks/background-session-authority.js";
3
+ import { a as openTerminalBindingsStore, c as classifyFeishuCardOwnership, d as parseFeishuTerminalMessage, i as openTerminalMessageClaims, l as createFeishuTerminalDelivery, n as embeddedHerdrRouteSchema, o as terminalAgentSchema, s as createHerdrSocketClient, t as createHerdrFeishuBridgeRuntime, u as parseFeishuTerminalAction } from "../chunks/herdr-feishu-runtime.js";
3
4
  import { createRandomId, createSha256Digest, createStableId } from "@rivus/platform/identity";
4
5
  import { ATTACHMENT_RECOVERY_COMMAND, AgentEventHandlerFailed, AgentEventSinkFailed, AgentLoopFailed, AgentRunCancelled, ModelChangeBoundaryDeadlineError, RivusToolInputRejected, ToolExecutorInputRejected, ToolInvocationDenied, createAgentHarness, createAgentsMdInstructionsSource, createInvocationAuthority, createJsonlAgentEventLog, createLangfuseAgentTelemetry, createMemoryService, createProjectMemoryPromptPreparer, createRecoveryAction, createRivusMemoryToolDescriptor, createRunAdmission, createTelemetryContentRedactor, createToolBroker, createToolInputDigest, createWorkspaceInstructionsResolver, createWorkspaceRootHandle, normalizeAgentInvocation, openJsonlAgentMemoryRepository, openJsonlToolOperationLedger, remainingUntil, resolveInvocationAuthority, resolveLangfuseTelemetryConfig, restoreAgentHistory } from "@rivus/runtime";
5
6
  import { homedir, tmpdir } from "node:os";
6
7
  import { delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
- import { Cause, Deferred, Effect, Exit, Fiber, Result, Semaphore } from "effect";
8
+ import { Cause, Deferred, Effect, Exit, Fiber, Result, Schema, Semaphore } from "effect";
8
9
  import { access, appendFile, chmod, constants, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
9
10
  import { createHash, randomUUID } from "node:crypto";
10
11
  import { appendJsonlRecord, isRecord, loadJsonlSnapshotStore, readPersistenceFile, writeAtomicTextFile, writePersistenceFile } from "@rivus/platform/persistence";
@@ -12909,6 +12910,982 @@ function settleCleanup(steps) {
12909
12910
  });
12910
12911
  }
12911
12912
  //#endregion
12913
+ //#region src/adapters/feishu/inbound-delivery/jsonl-inbound-delivery-store.ts
12914
+ function openJsonlInboundDeliveryStore(options) {
12915
+ const codecOptions = {
12916
+ ...options.acceptLegacySameAttemptClaims === void 0 ? {} : { acceptLegacySameAttemptClaims: options.acceptLegacySameAttemptClaims },
12917
+ label: options.label ?? "Inbound Delivery",
12918
+ payload: options.payload
12919
+ };
12920
+ return Effect.gen(function* () {
12921
+ const raw = yield* Effect.tryPromise({
12922
+ try: () => readPersistenceFile(options.filePath),
12923
+ catch: (cause) => cause
12924
+ });
12925
+ return createInMemoryDurableQueueStore({
12926
+ initial: yield* Effect.try({
12927
+ try: () => decodeInboundDeliverySnapshots(raw, codecOptions),
12928
+ catch: (cause) => cause
12929
+ }),
12930
+ persist: (delivery) => appendInboundDeliverySnapshot(options.filePath, delivery)
12931
+ });
12932
+ });
12933
+ }
12934
+ function decodeInboundDeliverySnapshots(raw, options) {
12935
+ if (raw === void 0) return [];
12936
+ const latest = /* @__PURE__ */ new Map();
12937
+ for (const [index, line] of raw.split("\n").entries()) {
12938
+ if (line.trim().length === 0) continue;
12939
+ let parsed;
12940
+ try {
12941
+ parsed = JSON.parse(line);
12942
+ } catch {
12943
+ throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
12944
+ }
12945
+ if (!isEnvelope(parsed)) throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
12946
+ let delivery;
12947
+ try {
12948
+ delivery = decodeDelivery(parsed.delivery, options.payload);
12949
+ } catch {
12950
+ throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
12951
+ }
12952
+ const previous = latest.get(delivery.id);
12953
+ if (delivery.revision !== (previous?.revision ?? 0) + 1) throw new Error(`invalid ${options.label} revision at line ${index + 1}`);
12954
+ try {
12955
+ if (previous) {
12956
+ if (!isValidPersistedTransition(previous, delivery, options.acceptLegacySameAttemptClaims ?? false)) throw new Error("transition");
12957
+ } else assertNewInboundDelivery(delivery);
12958
+ } catch {
12959
+ throw new Error(`invalid ${options.label} transition at line ${index + 1}`);
12960
+ }
12961
+ latest.set(delivery.id, delivery);
12962
+ }
12963
+ return [...latest.values()];
12964
+ }
12965
+ function encodeInboundDeliverySnapshot(delivery) {
12966
+ const snapshot = readInboundDeliverySnapshot(delivery);
12967
+ const persisted = {
12968
+ acceptedAt: snapshot.acceptedAt,
12969
+ attempts: snapshot.attempts,
12970
+ id: snapshot.id,
12971
+ laneKey: snapshot.laneKey,
12972
+ ...snapshot.metadata === void 0 ? {} : { options: snapshot.metadata },
12973
+ payload: snapshot.payload,
12974
+ ...snapshot.recovery === void 0 ? {} : { recovery: snapshot.recovery },
12975
+ revision: snapshot.revision,
12976
+ state: snapshot.state
12977
+ };
12978
+ return JSON.stringify({
12979
+ delivery: persisted,
12980
+ version: 1
12981
+ });
12982
+ }
12983
+ function decodeDelivery(value, payloadCodec) {
12984
+ if (!isRecord(value) || typeof value.acceptedAt !== "string" || typeof value.attempts !== "number" || typeof value.id !== "string" || typeof value.laneKey !== "string" || typeof value.revision !== "number") throw new Error("invalid Inbound Delivery snapshot");
12985
+ const metadata = payloadCodec.decodeMetadata(value.options);
12986
+ return readInboundDeliverySnapshot({
12987
+ acceptedAt: value.acceptedAt,
12988
+ attempts: value.attempts,
12989
+ id: value.id,
12990
+ laneKey: value.laneKey,
12991
+ ...metadata === void 0 ? {} : { metadata },
12992
+ payload: payloadCodec.decodePayload(value.payload),
12993
+ ...value.recovery === void 0 ? {} : { recovery: value.recovery },
12994
+ revision: value.revision,
12995
+ state: value.state
12996
+ });
12997
+ }
12998
+ function isEnvelope(value) {
12999
+ return isRecord(value) && value.version === 1 && "delivery" in value;
13000
+ }
13001
+ function isValidPersistedTransition(previous, next, acceptLegacySameAttemptClaims) {
13002
+ if (isValidInboundDeliveryTransition(previous, next)) return true;
13003
+ if (!acceptLegacySameAttemptClaims || previous.state.status !== "pending" || next.state.status !== "leased" || next.attempts !== previous.attempts) return false;
13004
+ return isValidInboundDeliveryTransition(previous, {
13005
+ ...next,
13006
+ attempts: next.attempts + 1
13007
+ });
13008
+ }
13009
+ function appendInboundDeliverySnapshot(filePath, delivery) {
13010
+ return Effect.tryPromise({
13011
+ try: async () => {
13012
+ await mkdir(dirname(filePath), { recursive: true });
13013
+ await appendFile(filePath, `${encodeInboundDeliverySnapshot(delivery)}\n`, "utf8");
13014
+ },
13015
+ catch: (cause) => cause
13016
+ });
13017
+ }
13018
+ //#endregion
13019
+ //#region src/adapters/feishu/routing/feishu-conversation-router.ts
13020
+ const CAPACITY_REFUSAL = "队列已满,请用新消息重发;本条未提交。";
13021
+ const UNSUPPORTED_TYPE_REFUSAL = "会话模式暂不支持该消息类型。";
13022
+ /**
13023
+ * Single-entry Feishu conversation router: every message and callback is fenced
13024
+ * by the durable owner ledger before any destination runs, and the mode capture
13025
+ * plus owner admission share one bridge admission turn.
13026
+ */
13027
+ function createFeishuConversationRouter(options) {
13028
+ const mode = options.bridge.conversation?.mode;
13029
+ const allowed = (identity) => options.actors.includes(identity.actorId) && options.chats.includes(identity.chatId);
13030
+ const freeze = (reason, messageId) => Effect.sync(() => {
13031
+ mode?.freeze();
13032
+ options.onObservation?.({
13033
+ ...messageId === void 0 ? {} : { messageId },
13034
+ type: "route_frozen"
13035
+ });
13036
+ console.error(`Herdr route frozen: ${reason}`);
13037
+ });
13038
+ const freezeVerdict = (reason, messageId) => Effect.gen(function* () {
13039
+ yield* freeze(reason, messageId);
13040
+ return { kind: "frozen" };
13041
+ });
13042
+ /**
13043
+ * Refusals for owner uncertainty use a stable internal claim key: claiming the
13044
+ * original id would fabricate a Herdr fence that conflicts with a durable Pi
13045
+ * owner after restart, so the original owner identity stays untouched.
13046
+ */
13047
+ const refusalClaimId = (identity) => `route-refusal:${JSON.stringify([options.appId, identity.messageId])}`;
13048
+ const refusalMessage = (identity) => ({
13049
+ id: refusalClaimId(identity),
13050
+ actorId: identity.actorId,
13051
+ chatId: identity.chatId,
13052
+ sentAt: identity.sentAt,
13053
+ text: ""
13054
+ });
13055
+ /** One admission turn: legacy fences → owner lookup → capture → durable owner → admit captured. */
13056
+ const admitSequence = (input) => Effect.gen(function* () {
13057
+ const { identity } = input;
13058
+ const legacy = yield* options.legacyOwnerOf(identity.messageId);
13059
+ if (legacy === "conflict") return yield* freezeVerdict("legacy Pi inbox and Herdr claim conflict", identity.messageId);
13060
+ const lookup = yield* Effect.result(options.ledger.lookup(identity.messageId));
13061
+ if (Result.isFailure(lookup)) return yield* freezeVerdict("owner ledger lookup failed", identity.messageId);
13062
+ if (lookup.success.kind === "opaque-fence") return yield* freezeVerdict("owner record is an opaque fence", identity.messageId);
13063
+ if (lookup.success.kind === "record") {
13064
+ const record = lookup.success.record;
13065
+ if (record.actorId !== identity.actorId || record.chatId !== identity.chatId) return yield* freezeVerdict("owner record does not match the incoming message source", identity.messageId);
13066
+ if (legacy === "pi" && record.decision === "herdr" || legacy === "herdr" && record.decision === "pi") return yield* freezeVerdict("historical fence and owner record disagree", identity.messageId);
13067
+ options.onObservation?.({
13068
+ messageId: identity.messageId,
13069
+ type: "route_skipped"
13070
+ });
13071
+ return record.decision === "pi" ? { kind: "pi" } : { kind: "skip" };
13072
+ }
13073
+ if (legacy === "herdr") {
13074
+ options.onObservation?.({
13075
+ messageId: identity.messageId,
13076
+ type: "route_skipped"
13077
+ });
13078
+ return { kind: "skip" };
13079
+ }
13080
+ let captured;
13081
+ let unsupportedType = false;
13082
+ if (input.parsed) captured = options.bridge.captureInAdmission(input.parsed);
13083
+ else if (legacy === "none" && mode?.isFrozen() && allowed(identity)) return { kind: "frozen" };
13084
+ else if (legacy === "none" && mode?.stateOf(identity.actorId, identity.chatId)) unsupportedType = true;
13085
+ const decision = legacy === "pi" || !unsupportedType && (captured === void 0 || captured.route.kind === "pass-through") ? "pi" : "herdr";
13086
+ const record = {
13087
+ appId: options.appId,
13088
+ messageId: identity.messageId,
13089
+ decision,
13090
+ actorId: identity.actorId,
13091
+ chatId: identity.chatId,
13092
+ at: Date.now(),
13093
+ ...decision === "herdr" && captured?.route.kind === "locked-send" ? { target: captured.route.agent } : {}
13094
+ };
13095
+ const admitted = yield* Effect.result(options.ledger.admit(record));
13096
+ if (Result.isFailure(admitted)) return yield* freezeVerdict("owner admission failed", identity.messageId);
13097
+ if (admitted.success.kind === "existing-opaque") return yield* freezeVerdict("owner admission found an opaque fence", identity.messageId);
13098
+ if (admitted.success.kind === "existing") {
13099
+ const existing = admitted.success.record;
13100
+ if (legacy === "pi" && existing.decision === "herdr") return yield* freezeVerdict("historical fence and owner record disagree", identity.messageId);
13101
+ options.onObservation?.({
13102
+ messageId: identity.messageId,
13103
+ type: "route_skipped"
13104
+ });
13105
+ return existing.decision === "pi" ? { kind: "pi" } : { kind: "skip" };
13106
+ }
13107
+ if (decision === "pi") return { kind: "pi" };
13108
+ if (unsupportedType || captured === void 0) return {
13109
+ kind: "unsupported-type",
13110
+ chatId: identity.chatId
13111
+ };
13112
+ const outcome = yield* Effect.result(options.bridge.admitCapturedInAdmission(captured));
13113
+ if (Result.isFailure(outcome)) return yield* freezeVerdict("herdr event admission failed", identity.messageId);
13114
+ return {
13115
+ kind: "herdr",
13116
+ entry: {
13117
+ message: captured.message,
13118
+ outcome: outcome.success
13119
+ }
13120
+ };
13121
+ });
13122
+ /**
13123
+ * Runs the sequence inside one admission turn and publishes the shared freeze
13124
+ * for every full cause BEFORE the permit is released, so already queued work
13125
+ * and later captures observe the exact flag.
13126
+ */
13127
+ const admitInAdmission = (input) => options.bridge.withAdmission(Effect.gen(function* () {
13128
+ const exit = yield* Effect.exit(admitSequence(input));
13129
+ if (Exit.isFailure(exit)) return yield* freezeVerdict("admission sequence failed", input.identity.messageId);
13130
+ return exit.value;
13131
+ }));
13132
+ /**
13133
+ * Owner uncertainty freezes; the visible refusal is admitted in a separate,
13134
+ * non-nested turn via the bridge's own frozen-refusal capture, so freshness,
13135
+ * authorization and the event claim stay with existing semantics. Delivery
13136
+ * happens only outside admission through the tracked reply lane. A failing
13137
+ * refusal claim keeps the freeze and reports a diagnostic: there is no
13138
+ * direct undeduped chat fallback and no destination retry.
13139
+ */
13140
+ const deliverOwnerUncertainRefusal = async (identity) => {
13141
+ let outcome;
13142
+ try {
13143
+ outcome = await Effect.runPromise(options.bridge.withAdmission(Effect.gen(function* () {
13144
+ const captured = options.bridge.captureInAdmission(refusalMessage(identity));
13145
+ return yield* options.bridge.admitCapturedInAdmission(captured);
13146
+ })));
13147
+ } catch {
13148
+ outcome = void 0;
13149
+ }
13150
+ if (outcome !== void 0 && outcome.kind === "done" && outcome.result.kind === "reply") {
13151
+ await options.target.reply(identity.chatId, outcome.result.reply.text);
13152
+ return true;
13153
+ }
13154
+ options.onObservation?.({
13155
+ messageId: identity.messageId,
13156
+ type: "refusal_undelivered"
13157
+ });
13158
+ console.error(`Herdr owner-uncertain refusal not delivered for ${identity.messageId}`);
13159
+ return false;
13160
+ };
13161
+ const route = async (identity, parsed) => {
13162
+ let verdict;
13163
+ try {
13164
+ verdict = await Effect.runPromise(admitInAdmission({
13165
+ identity,
13166
+ parsed
13167
+ }));
13168
+ } catch {
13169
+ verdict = { kind: "frozen" };
13170
+ }
13171
+ switch (verdict.kind) {
13172
+ case "pi": return { kind: "pi" };
13173
+ case "skip": return { kind: "skip" };
13174
+ case "frozen":
13175
+ await deliverOwnerUncertainRefusal(identity);
13176
+ return { kind: "frozen" };
13177
+ case "unsupported-type":
13178
+ await options.target.reply(verdict.chatId, UNSUPPORTED_TYPE_REFUSAL);
13179
+ return { kind: "consumed" };
13180
+ case "herdr":
13181
+ if (!await options.target.execute(verdict.entry)) await options.target.reply(verdict.entry.message.chatId, CAPACITY_REFUSAL);
13182
+ return { kind: "consumed" };
13183
+ }
13184
+ };
13185
+ const routeAwaited = async (identity, parsed) => {
13186
+ if (options.target.runAdmission === void 0) return route(identity, parsed);
13187
+ const admission = await options.target.runAdmission(() => route(identity, parsed));
13188
+ if (admission.kind === "stopped") throw new Error("Herdr intake is stopped; event not admitted");
13189
+ return admission.value;
13190
+ };
13191
+ const acceptedResult = (messageId) => ({
13192
+ accepted: true,
13193
+ messageId,
13194
+ pending: 0
13195
+ });
13196
+ const skippedResult = (messageId) => ({
13197
+ accepted: false,
13198
+ messageId,
13199
+ pending: 0,
13200
+ reason: "duplicate"
13201
+ });
13202
+ const refusedResult = (messageId) => ({
13203
+ accepted: false,
13204
+ messageId,
13205
+ pending: 0,
13206
+ reason: "ignored"
13207
+ });
13208
+ const deadCardResponse = () => ({ toast: {
13209
+ content: "This card cannot be handled; run /herdr again for a fresh one.",
13210
+ i18n: {
13211
+ en_us: "This card cannot be handled; run /herdr again for a fresh one.",
13212
+ zh_cn: "此卡片已失效或无法识别;请重新执行 /herdr 获取新卡片。"
13213
+ },
13214
+ type: "error"
13215
+ } });
13216
+ const notReceivedCardResponse = () => ({ toast: {
13217
+ content: "The intake is full; this click was not received. Try again in a moment.",
13218
+ i18n: {
13219
+ en_us: "The intake is full; this click was not received. Try again in a moment.",
13220
+ zh_cn: "处理通道已满,本次点击未接收;请稍后重试。"
13221
+ },
13222
+ type: "error"
13223
+ } });
13224
+ const frozenCardResponse = () => ({ toast: {
13225
+ content: "Routing is paused; this click was not executed.",
13226
+ i18n: {
13227
+ en_us: "Routing is paused; this click was not executed.",
13228
+ zh_cn: "会话路由已暂停;本次点击未执行。"
13229
+ },
13230
+ type: "warning"
13231
+ } });
13232
+ const receivedCardResponse = () => ({ toast: {
13233
+ content: "Received; the result arrives as a chat reply.",
13234
+ i18n: {
13235
+ en_us: "Received; the result arrives as a chat reply.",
13236
+ zh_cn: "已接收,结果将以会话回复送达。"
13237
+ },
13238
+ type: "info"
13239
+ } });
13240
+ const duplicateCardResponse = () => ({ toast: {
13241
+ content: "Already handled",
13242
+ i18n: {
13243
+ en_us: "Already handled",
13244
+ zh_cn: "该操作已处理过"
13245
+ },
13246
+ type: "info"
13247
+ } });
13248
+ const piCardErrorResponse = () => ({ toast: {
13249
+ content: "Card action failed",
13250
+ i18n: {
13251
+ en_us: "Card action failed",
13252
+ zh_cn: "卡片操作失败"
13253
+ },
13254
+ type: "error"
13255
+ } });
13256
+ return {
13257
+ /**
13258
+ * Message entry for the merged dispatcher table. Decides ownership before
13259
+ * any destination; the original Pi handler runs only for Pi-owned events.
13260
+ */
13261
+ routeMessage: (piReceive) => async (payload) => {
13262
+ const normalized = normalizeReceiveMessagePayload(payload);
13263
+ if (!shouldAcceptFeishuEndpointMessage(normalized, options.groupPolicy, options.botOpenId)) return piReceive(payload);
13264
+ const identity = readMessageIdentity(normalized.event);
13265
+ if (!identity) return piReceive(payload);
13266
+ const parsed = parseFeishuTerminalMessage(normalized.event);
13267
+ switch ((await routeAwaited(identity, parsed)).kind) {
13268
+ case "pi": return piReceive(payload);
13269
+ case "skip": return skippedResult(identity.messageId);
13270
+ case "frozen": return refusedResult(identity.messageId);
13271
+ case "consumed": return acceptedResult(identity.messageId);
13272
+ }
13273
+ },
13274
+ /**
13275
+ * Card callback entry: ownership comes only from explicit markers. Pi cards
13276
+ * always reach the original handler, in every mode and while frozen.
13277
+ */
13278
+ routeCardAction: (piCardAction) => async (event) => {
13279
+ const ownership = classifyFeishuCardOwnership(event);
13280
+ if (ownership === "pi") {
13281
+ if (!piCardAction) return piCardErrorResponse();
13282
+ return piCardAction(event);
13283
+ }
13284
+ if (ownership !== "herdr") return deadCardResponse();
13285
+ const message = parseFeishuTerminalAction(event, {});
13286
+ if (!message) return deadCardResponse();
13287
+ const identity = {
13288
+ actorId: message.actorId,
13289
+ chatId: message.chatId,
13290
+ messageId: message.id,
13291
+ messageType: "text",
13292
+ sentAt: message.sentAt
13293
+ };
13294
+ if (options.target.scheduleAdmission !== void 0) {
13295
+ const scheduled = await options.target.scheduleAdmission(() => route(identity, message).then(() => void 0));
13296
+ if (scheduled === "accepted") return receivedCardResponse();
13297
+ if (scheduled === "full") return notReceivedCardResponse();
13298
+ return frozenCardResponse();
13299
+ }
13300
+ switch ((await route(identity, message)).kind) {
13301
+ case "pi": return piCardAction ? piCardAction(event) : piCardErrorResponse();
13302
+ case "skip": return duplicateCardResponse();
13303
+ case "frozen": return frozenCardResponse();
13304
+ case "consumed": return receivedCardResponse();
13305
+ }
13306
+ },
13307
+ /** Bot menu events are Herdr-owned by their configured event_key. */
13308
+ routeMenuEvent: async (event, menuChats) => {
13309
+ const message = parseFeishuTerminalAction(event, menuChats);
13310
+ if (!message) return;
13311
+ await routeAwaited({
13312
+ actorId: message.actorId,
13313
+ chatId: message.chatId,
13314
+ messageId: message.id,
13315
+ messageType: "text",
13316
+ sentAt: message.sentAt
13317
+ }, message);
13318
+ }
13319
+ };
13320
+ }
13321
+ /**
13322
+ * Wraps the endpoint's real event dispatcher: Pi handlers stay under their own
13323
+ * keys, the three routed keys are replaced once, every other key is preserved,
13324
+ * and the underlying `register` return value reaches the WebSocket start
13325
+ * unchanged.
13326
+ */
13327
+ function createRoutedFeishuEventDispatcher(inner, route, menuChats) {
13328
+ return { register: (handlers) => {
13329
+ const merged = {
13330
+ ...handlers,
13331
+ "im.message.receive_v1": (payload) => route.message(handlers["im.message.receive_v1"])(payload),
13332
+ "card.action.trigger": (payload) => route.card(handlers["card.action.trigger"])(payload),
13333
+ "application.bot.menu_v6": (payload) => route.menu(payload, menuChats)
13334
+ };
13335
+ return inner.register(merged);
13336
+ } };
13337
+ }
13338
+ /** Normalized event shape: {message, sender} at the top level. */
13339
+ const messageIdentitySchema = Schema.Struct({
13340
+ message: Schema.optional(Schema.Struct({
13341
+ chat_id: Schema.optional(Schema.String),
13342
+ message_id: Schema.optional(Schema.String),
13343
+ message_type: Schema.optional(Schema.String),
13344
+ create_time: Schema.optional(Schema.String)
13345
+ })),
13346
+ sender: Schema.optional(Schema.Struct({ sender_id: Schema.optional(Schema.Struct({ open_id: Schema.optional(Schema.String) })) }))
13347
+ });
13348
+ const decodeMessageIdentity = Schema.decodeUnknownSync(messageIdentitySchema);
13349
+ /** Builds the gateway owner record identity from a normalized event payload. */
13350
+ function readMessageIdentity(event) {
13351
+ let decoded;
13352
+ try {
13353
+ decoded = decodeMessageIdentity(event);
13354
+ } catch {
13355
+ return;
13356
+ }
13357
+ const message = decoded.message;
13358
+ const messageId = message?.message_id;
13359
+ const chatId = message?.chat_id;
13360
+ const actorId = decoded.sender?.sender_id?.open_id;
13361
+ if (!messageId || !chatId || !actorId) return;
13362
+ return {
13363
+ actorId,
13364
+ chatId,
13365
+ messageId,
13366
+ messageType: message?.message_type ?? "",
13367
+ sentAt: Number(message?.create_time)
13368
+ };
13369
+ }
13370
+ //#endregion
13371
+ //#region src/adapters/outbound/persistence/message-gateway/file-route-admission-ledger.ts
13372
+ const admissionRecordSchema = Schema.Struct({
13373
+ version: Schema.Literal(1),
13374
+ appId: Schema.String,
13375
+ messageId: Schema.String,
13376
+ decision: Schema.Literals(["pi", "herdr"]),
13377
+ actorId: Schema.String,
13378
+ chatId: Schema.String,
13379
+ at: Schema.Number,
13380
+ target: Schema.optional(terminalAgentSchema)
13381
+ });
13382
+ const decodeAdmissionRecord = Schema.decodeUnknownSync(admissionRecordSchema);
13383
+ const isErrnoCode$1 = Schema.Struct({ code: Schema.String });
13384
+ /** Admissions and state directories reject any group/other permission or symlink. */
13385
+ function assertPrivateDirectory$1(info, label) {
13386
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new Error(`${label} is not a regular directory`);
13387
+ if (info.uid !== process.getuid?.() || (info.mode & 63) !== 0) throw new Error(`Unsafe ${label} permissions or owner`);
13388
+ }
13389
+ function assertPrivateOwnerFile(info) {
13390
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error("Route admission owner is not a regular file");
13391
+ if (info.uid !== process.getuid?.() || (info.mode & 127) !== 0) throw new Error("Unsafe route admission owner permissions or owner");
13392
+ }
13393
+ /**
13394
+ * Create the final-level admissions child inside an already-provisioned state
13395
+ * root (the shared claims opener owns the root) and sync the root entry durably.
13396
+ * A missing root is a startup failure with guidance — never an ancestor walk.
13397
+ */
13398
+ async function createPrivateChildDirectory(path, mode, label, stateRoot) {
13399
+ try {
13400
+ await mkdir(path, {
13401
+ recursive: false,
13402
+ mode
13403
+ });
13404
+ } catch (error) {
13405
+ if (Schema.is(isErrnoCode$1)(error) && error.code === "ENOENT") throw new Error(`Cannot create ${label}: the state directory ${stateRoot} does not exist; open the shared claims store first`);
13406
+ if (!(Schema.is(isErrnoCode$1)(error) && error.code === "EEXIST")) throw error;
13407
+ }
13408
+ assertPrivateDirectory$1(await lstat(path), label);
13409
+ }
13410
+ async function syncDirectoryEntry(path) {
13411
+ const handle = await open(path, "r");
13412
+ try {
13413
+ await handle.sync();
13414
+ } finally {
13415
+ await handle.close();
13416
+ }
13417
+ }
13418
+ async function syncStateDirectory(path, label) {
13419
+ try {
13420
+ await syncDirectoryEntry(path);
13421
+ } catch (error) {
13422
+ throw new Error(`Cannot sync ${label}`, { cause: error });
13423
+ }
13424
+ }
13425
+ /**
13426
+ * File-backed cross-route admission ledger (one irrevocable owner file per
13427
+ * appId/messageId).
13428
+ *
13429
+ * Lock ownership: the caller must already hold the shared state-directory lifetime
13430
+ * lock (`openTerminalMessageClaims`) for this directory and keep it for the
13431
+ * store's lifetime; this factory never acquires or releases a lock itself and
13432
+ * never opens a connection. Crash recovery is manual: lock files and
13433
+ * owner files are preserved; a lock is removed only after its owner is verified
13434
+ * stopped, never by PID/age-based theft.
13435
+ */
13436
+ async function openFileRouteAdmissionLedger(directory, appId) {
13437
+ const admissionsDirectory = join(directory, "route-admissions");
13438
+ await createPrivateChildDirectory(admissionsDirectory, 448, "route admission directory", directory);
13439
+ await syncStateDirectory(directory, "state directory");
13440
+ const ownerPath = (messageId) => join(admissionsDirectory, `${createHash("sha256").update(JSON.stringify([appId, messageId])).digest("hex")}.json`);
13441
+ const syncAdmissionsDirectory = async () => {
13442
+ const handle = await open(admissionsDirectory, "r");
13443
+ try {
13444
+ await handle.sync();
13445
+ } finally {
13446
+ await handle.close();
13447
+ }
13448
+ };
13449
+ const safeStatOwner = async (messageId) => {
13450
+ const path = ownerPath(messageId);
13451
+ assertPrivateOwnerFile(await lstat(path));
13452
+ return path;
13453
+ };
13454
+ const decodeOwner = (raw, messageId) => {
13455
+ let parsed;
13456
+ try {
13457
+ parsed = JSON.parse(raw);
13458
+ } catch {
13459
+ return { kind: "opaque-fence" };
13460
+ }
13461
+ let record;
13462
+ try {
13463
+ const validated = decodeAdmissionRecord(parsed);
13464
+ record = {
13465
+ appId: validated.appId,
13466
+ messageId: validated.messageId,
13467
+ decision: validated.decision,
13468
+ actorId: validated.actorId,
13469
+ chatId: validated.chatId,
13470
+ at: validated.at,
13471
+ ...validated.target === void 0 ? {} : { target: validated.target }
13472
+ };
13473
+ } catch {
13474
+ return { kind: "opaque-fence" };
13475
+ }
13476
+ if (record.appId !== appId || record.messageId !== messageId) throw new Error("Route admission owner belongs to another app or event");
13477
+ return {
13478
+ kind: "record",
13479
+ record
13480
+ };
13481
+ };
13482
+ /**
13483
+ * One safe flow for lookup and EEXIST: an existing owner is stat-validated
13484
+ * first (unsafe files fail before being read or synced); only a valid complete
13485
+ * record is re-synchronized (owner file, admissions directory, closes) before
13486
+ * it is returned, because visibility alone is not durable proof. Partial or
13487
+ * corrupt owners return their opaque fence immediately; failures propagate —
13488
+ * once an owner has been observed by lstat, even ENOENT on the read or during
13489
+ * recovery is a failure, never a downgrade to missing.
13490
+ */
13491
+ /** Shared read/decode/fsync/finally-close plus admissions-dir resync for an already stat-validated owner. */
13492
+ const readAndSyncOwner = async (validatedPath, messageId) => {
13493
+ const lookup = decodeOwner(await readFile(validatedPath, "utf8"), messageId);
13494
+ if (lookup.kind !== "record") return lookup;
13495
+ const handle = await open(validatedPath, "r+");
13496
+ try {
13497
+ await handle.sync();
13498
+ } finally {
13499
+ await handle.close();
13500
+ }
13501
+ await syncAdmissionsDirectory();
13502
+ return lookup;
13503
+ };
13504
+ const loadExisting = async (messageId) => {
13505
+ const path = await safeStatOwner(messageId);
13506
+ return readAndSyncOwner(path, messageId);
13507
+ };
13508
+ return {
13509
+ lookup: (messageId) => Effect.uninterruptible(Effect.tryPromise({
13510
+ try: async () => {
13511
+ let path;
13512
+ try {
13513
+ path = await safeStatOwner(messageId);
13514
+ } catch (error) {
13515
+ if (Schema.is(isErrnoCode$1)(error) && error.code === "ENOENT") return { kind: "missing" };
13516
+ throw error;
13517
+ }
13518
+ return await readAndSyncOwner(path, messageId);
13519
+ },
13520
+ catch: (error) => error
13521
+ })),
13522
+ admit: (input) => Effect.uninterruptible(Effect.tryPromise({
13523
+ try: async () => {
13524
+ const record = decodeAdmissionRecord({
13525
+ ...input,
13526
+ version: 1
13527
+ });
13528
+ if (record.appId !== appId) throw new Error("Route admission record belongs to another app");
13529
+ if (record.decision === "pi" && record.target !== void 0) throw new Error("A pi route admission never carries a Herdr target");
13530
+ const path = ownerPath(record.messageId);
13531
+ let handle;
13532
+ try {
13533
+ handle = await open(path, "wx", 384);
13534
+ } catch (error) {
13535
+ if (Schema.is(isErrnoCode$1)(error) && error.code === "EEXIST") {
13536
+ const lookup = await loadExisting(record.messageId);
13537
+ return lookup.kind === "record" ? {
13538
+ kind: "existing",
13539
+ record: lookup.record
13540
+ } : { kind: "existing-opaque" };
13541
+ }
13542
+ throw error;
13543
+ }
13544
+ try {
13545
+ await handle.writeFile(JSON.stringify({
13546
+ ...record,
13547
+ version: 1
13548
+ }), "utf8");
13549
+ await handle.sync();
13550
+ await syncAdmissionsDirectory();
13551
+ } catch (error) {
13552
+ await handle.close().catch(() => void 0);
13553
+ throw error;
13554
+ }
13555
+ await handle.close();
13556
+ return {
13557
+ kind: "created",
13558
+ record
13559
+ };
13560
+ },
13561
+ catch: (error) => error
13562
+ }))
13563
+ };
13564
+ }
13565
+ //#endregion
13566
+ //#region src/adapters/outbound/persistence/terminal-session/terminal-conversation-states.ts
13567
+ const modeStateSchema = Schema.Union([Schema.Struct({
13568
+ kind: Schema.Literal("selection"),
13569
+ actorId: Schema.String,
13570
+ chatId: Schema.String,
13571
+ since: Schema.Number
13572
+ }), Schema.Struct({
13573
+ kind: Schema.Literal("locked"),
13574
+ actorId: Schema.String,
13575
+ chatId: Schema.String,
13576
+ since: Schema.Number,
13577
+ handle: Schema.String,
13578
+ agent: terminalAgentSchema,
13579
+ stale: Schema.Boolean
13580
+ })]);
13581
+ const snapshotSchema = Schema.Struct({
13582
+ version: Schema.Literal(1),
13583
+ appId: Schema.String,
13584
+ states: Schema.Array(modeStateSchema)
13585
+ });
13586
+ const decodeSnapshot = Schema.decodeUnknownSync(snapshotSchema);
13587
+ const isErrnoCode = Schema.Struct({ code: Schema.String });
13588
+ function assertStateCollection(states) {
13589
+ if (states.length > 64) throw new Error("Conversation state snapshot exceeds capacity");
13590
+ if (new Set(states.map((state) => `${state.actorId}\u0000${state.chatId}`)).size !== states.length) throw new Error("Conversation state snapshot contains duplicate conversations");
13591
+ }
13592
+ function assertPrivateDirectory(info) {
13593
+ if (info.isSymbolicLink() || !info.isDirectory()) throw new Error("Conversation state directory is not a regular directory");
13594
+ if (info.uid !== process.getuid?.() || (info.mode & 63) !== 0) throw new Error("Unsafe conversation state directory permissions or owner");
13595
+ }
13596
+ function assertPrivateFile(info, label) {
13597
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error(`${label} is not a regular file`);
13598
+ if (info.uid !== process.getuid?.() || (info.mode & 127) !== 0) throw new Error(`Unsafe ${label} permissions or owner`);
13599
+ }
13600
+ /**
13601
+ * Versioned per-app conversation mode snapshot (selection/locked states) with a
13602
+ * durable initialization marker.
13603
+ *
13604
+ * Lock ownership: the caller must already hold the shared state-directory lifetime
13605
+ * lock (`openTerminalMessageClaims`) for this directory and keep it for the
13606
+ * store's lifetime; this factory never acquires a lock, releases one, or opens a
13607
+ * connection. Crash recovery is manual: an interrupted
13608
+ * initialization or a snapshot missing after initialization refuses startup
13609
+ * (fail closed) instead of erasing modes; an operator removes a stale lock only
13610
+ * after the previous owner is verified stopped.
13611
+ */
13612
+ async function openTerminalConversationStates(directory, appId) {
13613
+ assertPrivateDirectory(await lstat(directory));
13614
+ const digest = createHash("sha256").update(appId).digest("hex");
13615
+ const snapshotPath = join(directory, `conversation-states-${digest}.json`);
13616
+ const markerPath = join(directory, `.conversation-states-initialized-${digest}`);
13617
+ const safeFile = async (path, label) => {
13618
+ const info = await lstat(path);
13619
+ if (!info.isFile()) throw new Error(`${label} is not a regular file`);
13620
+ assertPrivateFile(info, label);
13621
+ };
13622
+ const readStates = async () => {
13623
+ await safeFile(snapshotPath, "Conversation state snapshot");
13624
+ let snapshot;
13625
+ try {
13626
+ snapshot = decodeSnapshot(JSON.parse(await readFile(snapshotPath, "utf8")));
13627
+ } catch (error) {
13628
+ throw new Error("Cannot restore conversation states", { cause: error });
13629
+ }
13630
+ if (snapshot.appId !== appId) throw new Error("Conversation state snapshot belongs to another app");
13631
+ const states = snapshot.states;
13632
+ assertStateCollection(states);
13633
+ return states;
13634
+ };
13635
+ const durableSyncDirectory = async () => {
13636
+ const handle = await open(directory, "r");
13637
+ try {
13638
+ await handle.sync();
13639
+ } finally {
13640
+ await handle.close();
13641
+ }
13642
+ };
13643
+ const createMarker = async () => {
13644
+ const marker = await open(markerPath, "wx", 384);
13645
+ try {
13646
+ await marker.writeFile(JSON.stringify({
13647
+ version: 1,
13648
+ appId
13649
+ }));
13650
+ await marker.sync();
13651
+ await durableSyncDirectory();
13652
+ await marker.close();
13653
+ } catch (error) {
13654
+ await marker.close().catch(() => void 0);
13655
+ throw error;
13656
+ }
13657
+ };
13658
+ const markerStat = await lstat(markerPath).catch((error) => {
13659
+ if (Schema.is(isErrnoCode)(error) && error.code === "ENOENT") return;
13660
+ throw error;
13661
+ });
13662
+ if (markerStat !== void 0) {
13663
+ if (!markerStat.isFile()) throw new Error("Conversation state initialization marker is not a regular file");
13664
+ assertPrivateFile(markerStat, "Conversation state initialization marker");
13665
+ }
13666
+ const snapshotStat = await lstat(snapshotPath).catch((error) => {
13667
+ if (Schema.is(isErrnoCode)(error) && error.code === "ENOENT") return;
13668
+ throw error;
13669
+ });
13670
+ let initial;
13671
+ if (markerStat === void 0 && snapshotStat === void 0) {
13672
+ await createMarker();
13673
+ initial = [];
13674
+ await writeAtomicTextFile(snapshotPath, JSON.stringify({
13675
+ version: 1,
13676
+ appId,
13677
+ states: []
13678
+ }), {
13679
+ mode: 384,
13680
+ durable: true
13681
+ });
13682
+ } else {
13683
+ if (markerStat === void 0) throw new Error("Conversation state initialization is ambiguous; refusing startup");
13684
+ if (snapshotStat === void 0) throw new Error("Conversation state snapshot is missing after initialization");
13685
+ initial = await readStates();
13686
+ await writeAtomicTextFile(snapshotPath, JSON.stringify({
13687
+ version: 1,
13688
+ appId,
13689
+ states: initial
13690
+ }), {
13691
+ mode: 384,
13692
+ durable: true
13693
+ });
13694
+ }
13695
+ return {
13696
+ initial,
13697
+ save: (states) => Effect.uninterruptible(Effect.tryPromise({
13698
+ try: async () => {
13699
+ assertStateCollection(states);
13700
+ const validated = decodeSnapshot({
13701
+ version: 1,
13702
+ appId,
13703
+ states
13704
+ });
13705
+ await writeAtomicTextFile(snapshotPath, JSON.stringify({
13706
+ version: 1,
13707
+ appId,
13708
+ states: validated.states
13709
+ }), {
13710
+ mode: 384,
13711
+ durable: true
13712
+ });
13713
+ },
13714
+ catch: (error) => error
13715
+ }))
13716
+ };
13717
+ }
13718
+ //#endregion
13719
+ //#region src/bootstrap/feishu/embedded-herdr-route.ts
13720
+ const herdrRouteConfigSchema = embeddedHerdrRouteSchema;
13721
+ /**
13722
+ * Parses RIVUS_HERDR_ROUTE_CONFIG and fails the assembly unless exactly one
13723
+ * enabled endpoint resolves to the configured appId. A missing variable keeps
13724
+ * the deployment entirely on the Pi path.
13725
+ */
13726
+ async function resolveHerdrRouteConfig(env, manifest) {
13727
+ const configPath = env.RIVUS_HERDR_ROUTE_CONFIG?.trim();
13728
+ if (!configPath) return;
13729
+ if (!isAbsolute(configPath)) throw new Error(`RIVUS_HERDR_ROUTE_CONFIG must be an absolute path: ${configPath}`);
13730
+ let config;
13731
+ try {
13732
+ const raw = await readFile(configPath, "utf8");
13733
+ config = Schema.decodeUnknownSync(herdrRouteConfigSchema)(JSON.parse(raw));
13734
+ } catch (error) {
13735
+ throw new Error(`Invalid RIVUS_HERDR_ROUTE_CONFIG at ${configPath}`, { cause: error });
13736
+ }
13737
+ if ((config.sessions?.length ?? 0) > 0 === (config.discoverSessionsIn !== void 0)) throw new Error("RIVUS_HERDR_ROUTE_CONFIG requires exactly one of sessions or discoverSessionsIn");
13738
+ for (const [actor, chat] of Object.entries(config.menuChats ?? {})) if (!config.allowedActors.includes(actor) || !config.allowedChats.includes(chat)) throw new Error("Herdr route menu destinations must be allowlisted");
13739
+ const matched = manifest.endpoints.filter((endpoint) => {
13740
+ if (!endpoint.enabled) return false;
13741
+ try {
13742
+ return resolveFeishuEndpointCredentials(endpoint.credentialRef, env).appId === config.appId;
13743
+ } catch {
13744
+ return false;
13745
+ }
13746
+ });
13747
+ if (matched.length !== 1) throw new Error(`RIVUS_HERDR_ROUTE_CONFIG appId must match exactly one enabled endpoint (matched ${matched.length})`);
13748
+ return {
13749
+ appId: config.appId,
13750
+ endpointId: matched[0].id,
13751
+ allowedActors: config.allowedActors,
13752
+ allowedChats: config.allowedChats,
13753
+ stateDirectory: config.stateDirectory,
13754
+ writable: config.writable === true,
13755
+ ...config.sessions === void 0 ? {} : { sessions: config.sessions },
13756
+ ...config.discoverSessionsIn === void 0 ? {} : { discoverSessionsIn: config.discoverSessionsIn },
13757
+ ...config.menuChats === void 0 ? {} : { menuChats: config.menuChats },
13758
+ ...config.fontFiles === void 0 ? {} : { fontFiles: config.fontFiles }
13759
+ };
13760
+ }
13761
+ /**
13762
+ * Startup-built pre-upgrade Pi fence: the same inbox file decoded with the same
13763
+ * codec the live store uses, reduced to received ids of any status. Corruption
13764
+ * fails the endpoint assembly before any WebSocket starts.
13765
+ */
13766
+ async function buildLegacyPiInboxIndex(filePath) {
13767
+ const deliveries = decodeInboundDeliverySnapshots(await readPersistenceFile(filePath), {
13768
+ acceptLegacySameAttemptClaims: true,
13769
+ label: "Feishu inbox",
13770
+ payload: feishuInboundDeliveryPayloadCodec
13771
+ });
13772
+ return new Set(deliveries.map((delivery) => delivery.id));
13773
+ }
13774
+ /**
13775
+ * Opens every embedded Herdr store (claims lock, bindings, mode snapshot,
13776
+ * owner ledger) plus the connection-less runtime and router. The opener
13777
+ * Promise is allocated lazily inside the Effect and joined on interruption:
13778
+ * a cancelled hand still waits for the stores it may have acquired and
13779
+ * releases them instead of leaving an ownerless lock holder behind.
13780
+ */
13781
+ function openEmbeddedHerdrRoute(input) {
13782
+ return Effect.suspend(() => {
13783
+ const opener = buildEmbeddedHerdrRoute(input);
13784
+ return Effect.onInterrupt(Effect.tryPromise({
13785
+ catch: (error) => error,
13786
+ try: () => opener
13787
+ }), () => Effect.promise(() => opener.then((opened) => opened.runtime.stop().then(() => void 0, () => void 0), () => void 0)));
13788
+ });
13789
+ }
13790
+ async function buildEmbeddedHerdrRoute(input) {
13791
+ const { herdr } = input;
13792
+ return (async () => {
13793
+ const legacyPiIds = await buildLegacyPiInboxIndex(join(input.endpointStateDirectory, "feishu-inbox.jsonl"));
13794
+ const claims = await openTerminalMessageClaims(herdr.stateDirectory, herdr.appId);
13795
+ const closeClaims = () => claims.close().catch(() => void 0);
13796
+ let bindings;
13797
+ let states;
13798
+ let ledger;
13799
+ let stopState;
13800
+ let runtime;
13801
+ let router;
13802
+ try {
13803
+ bindings = await openTerminalBindingsStore(herdr.stateDirectory, herdr.appId);
13804
+ states = await openTerminalConversationStates(herdr.stateDirectory, herdr.appId);
13805
+ ledger = await openFileRouteAdmissionLedger(herdr.stateDirectory, herdr.appId);
13806
+ stopState = { stopped: false };
13807
+ runtime = createHerdrFeishuBridgeRuntime({
13808
+ sessions: createHerdrSocketClient(herdr.sessions ?? [], 5e3, herdr.discoverSessionsIn),
13809
+ actors: herdr.allowedActors,
13810
+ chats: herdr.allowedChats,
13811
+ writable: herdr.writable,
13812
+ claim: claims.claim,
13813
+ bindings,
13814
+ conversation: {
13815
+ epoch: randomUUID(),
13816
+ states
13817
+ },
13818
+ delivery: createFeishuTerminalDelivery(new Lark.Client({
13819
+ appId: herdr.appId,
13820
+ appSecret: input.appSecret,
13821
+ domain: input.baseUrl,
13822
+ loggerLevel: Lark.LoggerLevel.error
13823
+ }), { isStopped: () => stopState.stopped }),
13824
+ ...herdr.fontFiles === void 0 ? {} : { fontFiles: herdr.fontFiles },
13825
+ stopState,
13826
+ closeStores: () => claims.close()
13827
+ });
13828
+ router = createFeishuConversationRouter({
13829
+ appId: herdr.appId,
13830
+ bridge: runtime.bridge,
13831
+ ledger,
13832
+ actors: herdr.allowedActors,
13833
+ chats: herdr.allowedChats,
13834
+ legacyOwnerOf: (messageId) => Effect.gen(function* () {
13835
+ const claimed = yield* claims.hasClaim(messageId);
13836
+ const pi = legacyPiIds.has(messageId);
13837
+ if (pi && claimed) return "conflict";
13838
+ if (pi) return "pi";
13839
+ return claimed ? "herdr" : "none";
13840
+ }),
13841
+ groupPolicy: input.groupPolicy,
13842
+ botOpenId: input.botOpenId,
13843
+ target: runtime
13844
+ });
13845
+ } catch (error) {
13846
+ await closeClaims();
13847
+ throw error;
13848
+ }
13849
+ return {
13850
+ runtime,
13851
+ routedDispatcher: () => createRoutedFeishuEventDispatcher(new Lark.EventDispatcher({}), {
13852
+ message: router.routeMessage,
13853
+ card: router.routeCardAction,
13854
+ menu: router.routeMenuEvent
13855
+ }, herdr.menuChats ?? {})
13856
+ };
13857
+ })();
13858
+ }
13859
+ function wrapEmbeddedHerdrEndpointLifecycle(endpoint, runtime) {
13860
+ return {
13861
+ running: () => endpoint.running(),
13862
+ start: () => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
13863
+ yield* Effect.tryPromise({
13864
+ catch: (error) => error,
13865
+ try: () => runtime.start()
13866
+ });
13867
+ const startExit = yield* Effect.exit(restore(Effect.suspend(() => endpoint.start())));
13868
+ if (Exit.isFailure(startExit)) {
13869
+ yield* Effect.exit(Effect.tryPromise({
13870
+ catch: () => void 0,
13871
+ try: () => runtime.stop()
13872
+ }));
13873
+ return yield* Effect.failCause(startExit.cause);
13874
+ }
13875
+ })),
13876
+ stop: () => Effect.uninterruptible(Effect.gen(function* () {
13877
+ yield* Effect.sync(() => runtime.stopIntake());
13878
+ const endpointExit = yield* Effect.exit(endpoint.stop());
13879
+ const drainExit = yield* Effect.exit(Effect.tryPromise({
13880
+ catch: (error) => error,
13881
+ try: () => runtime.completeStop()
13882
+ }));
13883
+ if (Exit.isFailure(endpointExit)) return yield* Effect.failCause(endpointExit.cause);
13884
+ if (Exit.isFailure(drainExit)) return yield* Effect.failCause(drainExit.cause);
13885
+ }))
13886
+ };
13887
+ }
13888
+ //#endregion
12912
13889
  //#region src/adapters/feishu/run-presentation/feishu-presentation-preparation.ts
12913
13890
  function createFeishuPresentationPreparation(options) {
12914
13891
  return (run) => Effect.gen(function* () {
@@ -14769,7 +15746,7 @@ function createAutomationScheduler(options) {
14769
15746
  running = false;
14770
15747
  loopFiber = void 0;
14771
15748
  })));
14772
- return Effect.forkDetach(loop, { uninterruptible: "inherit" }).pipe(Effect.tap((fiber) => Effect.sync(() => {
15749
+ return Effect.forkDetach(loop, { uninterruptible: false }).pipe(Effect.tap((fiber) => Effect.sync(() => {
14773
15750
  if (running) loopFiber = fiber;
14774
15751
  })), Effect.asVoid);
14775
15752
  }));
@@ -15146,112 +16123,6 @@ function append(filePath, record) {
15146
16123
  });
15147
16124
  }
15148
16125
  //#endregion
15149
- //#region src/adapters/feishu/inbound-delivery/jsonl-inbound-delivery-store.ts
15150
- function openJsonlInboundDeliveryStore(options) {
15151
- const codecOptions = {
15152
- ...options.acceptLegacySameAttemptClaims === void 0 ? {} : { acceptLegacySameAttemptClaims: options.acceptLegacySameAttemptClaims },
15153
- label: options.label ?? "Inbound Delivery",
15154
- payload: options.payload
15155
- };
15156
- return Effect.gen(function* () {
15157
- const raw = yield* Effect.tryPromise({
15158
- try: () => readPersistenceFile(options.filePath),
15159
- catch: (cause) => cause
15160
- });
15161
- return createInMemoryDurableQueueStore({
15162
- initial: yield* Effect.try({
15163
- try: () => decodeInboundDeliverySnapshots(raw, codecOptions),
15164
- catch: (cause) => cause
15165
- }),
15166
- persist: (delivery) => appendInboundDeliverySnapshot(options.filePath, delivery)
15167
- });
15168
- });
15169
- }
15170
- function decodeInboundDeliverySnapshots(raw, options) {
15171
- if (raw === void 0) return [];
15172
- const latest = /* @__PURE__ */ new Map();
15173
- for (const [index, line] of raw.split("\n").entries()) {
15174
- if (line.trim().length === 0) continue;
15175
- let parsed;
15176
- try {
15177
- parsed = JSON.parse(line);
15178
- } catch {
15179
- throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
15180
- }
15181
- if (!isEnvelope(parsed)) throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
15182
- let delivery;
15183
- try {
15184
- delivery = decodeDelivery(parsed.delivery, options.payload);
15185
- } catch {
15186
- throw new Error(`invalid ${options.label} snapshot at line ${index + 1}`);
15187
- }
15188
- const previous = latest.get(delivery.id);
15189
- if (delivery.revision !== (previous?.revision ?? 0) + 1) throw new Error(`invalid ${options.label} revision at line ${index + 1}`);
15190
- try {
15191
- if (previous) {
15192
- if (!isValidPersistedTransition(previous, delivery, options.acceptLegacySameAttemptClaims ?? false)) throw new Error("transition");
15193
- } else assertNewInboundDelivery(delivery);
15194
- } catch {
15195
- throw new Error(`invalid ${options.label} transition at line ${index + 1}`);
15196
- }
15197
- latest.set(delivery.id, delivery);
15198
- }
15199
- return [...latest.values()];
15200
- }
15201
- function encodeInboundDeliverySnapshot(delivery) {
15202
- const snapshot = readInboundDeliverySnapshot(delivery);
15203
- const persisted = {
15204
- acceptedAt: snapshot.acceptedAt,
15205
- attempts: snapshot.attempts,
15206
- id: snapshot.id,
15207
- laneKey: snapshot.laneKey,
15208
- ...snapshot.metadata === void 0 ? {} : { options: snapshot.metadata },
15209
- payload: snapshot.payload,
15210
- ...snapshot.recovery === void 0 ? {} : { recovery: snapshot.recovery },
15211
- revision: snapshot.revision,
15212
- state: snapshot.state
15213
- };
15214
- return JSON.stringify({
15215
- delivery: persisted,
15216
- version: 1
15217
- });
15218
- }
15219
- function decodeDelivery(value, payloadCodec) {
15220
- if (!isRecord(value) || typeof value.acceptedAt !== "string" || typeof value.attempts !== "number" || typeof value.id !== "string" || typeof value.laneKey !== "string" || typeof value.revision !== "number") throw new Error("invalid Inbound Delivery snapshot");
15221
- const metadata = payloadCodec.decodeMetadata(value.options);
15222
- return readInboundDeliverySnapshot({
15223
- acceptedAt: value.acceptedAt,
15224
- attempts: value.attempts,
15225
- id: value.id,
15226
- laneKey: value.laneKey,
15227
- ...metadata === void 0 ? {} : { metadata },
15228
- payload: payloadCodec.decodePayload(value.payload),
15229
- ...value.recovery === void 0 ? {} : { recovery: value.recovery },
15230
- revision: value.revision,
15231
- state: value.state
15232
- });
15233
- }
15234
- function isEnvelope(value) {
15235
- return isRecord(value) && value.version === 1 && "delivery" in value;
15236
- }
15237
- function isValidPersistedTransition(previous, next, acceptLegacySameAttemptClaims) {
15238
- if (isValidInboundDeliveryTransition(previous, next)) return true;
15239
- if (!acceptLegacySameAttemptClaims || previous.state.status !== "pending" || next.state.status !== "leased" || next.attempts !== previous.attempts) return false;
15240
- return isValidInboundDeliveryTransition(previous, {
15241
- ...next,
15242
- attempts: next.attempts + 1
15243
- });
15244
- }
15245
- function appendInboundDeliverySnapshot(filePath, delivery) {
15246
- return Effect.tryPromise({
15247
- try: async () => {
15248
- await mkdir(dirname(filePath), { recursive: true });
15249
- await appendFile(filePath, `${encodeInboundDeliverySnapshot(delivery)}\n`, "utf8");
15250
- },
15251
- catch: (cause) => cause
15252
- });
15253
- }
15254
- //#endregion
15255
16126
  //#region src/adapters/feishu/conversation-session/json-feishu-session-store.ts
15256
16127
  function openJsonFeishuSessionStore(options) {
15257
16128
  return Effect.tryPromise({
@@ -17017,6 +17888,7 @@ async function createRivusDeploymentAdapters(context) {
17017
17888
  manifest
17018
17889
  };
17019
17890
  })));
17891
+ const herdrRoute = await resolveHerdrRouteConfig(context.env, manifest);
17020
17892
  const memoryTenantId = context.env.RIVUS_MEMORY_TENANT_ID?.trim() || "local";
17021
17893
  const request = createJsonFetchRequest();
17022
17894
  const attachmentCapabilities = /* @__PURE__ */ new Map();
@@ -17323,6 +18195,15 @@ async function createRivusDeploymentAdapters(context) {
17323
18195
  config
17324
18196
  });
17325
18197
  const prepareCardTarget = (run) => cardRollover.prepareRun(run);
18198
+ const herdr = herdrRoute !== void 0 && input.endpointId === herdrRoute.endpointId ? herdrRoute : void 0;
18199
+ const embedded = herdr ? yield* openEmbeddedHerdrRoute({
18200
+ herdr,
18201
+ appSecret: credentials.appSecret,
18202
+ baseUrl: input.definition.baseUrl,
18203
+ endpointStateDirectory: endpointState,
18204
+ groupPolicy: input.definition.groupPolicy,
18205
+ botOpenId
18206
+ }) : void 0;
17326
18207
  const endpoint = createFeishuDeploymentEndpoint({
17327
18208
  agentId: input.agentId,
17328
18209
  acknowledgeSteering: (messageId) => reactions.add(messageId),
@@ -17333,7 +18214,7 @@ async function createRivusDeploymentAdapters(context) {
17333
18214
  finalizeRun: (runId) => cardRollover.rollover.releaseRun(runId),
17334
18215
  cancel: input.cancel,
17335
18216
  endpointId: input.endpointId,
17336
- eventDispatcher: createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
18217
+ eventDispatcher: embedded ? createLazyFeishuWebSocketEventDispatcher(embedded.routedDispatcher) : createLazyFeishuWebSocketEventDispatcher(() => new Lark.EventDispatcher({})),
17337
18218
  groupPolicy: input.definition.groupPolicy,
17338
18219
  handle: input.handle,
17339
18220
  inboxStore: inbox,
@@ -17361,7 +18242,8 @@ async function createRivusDeploymentAdapters(context) {
17361
18242
  workerConcurrency: 4
17362
18243
  });
17363
18244
  interactionRegistry.register(input.endpointId, interactions);
17364
- return endpoint;
18245
+ if (embedded === void 0) return endpoint;
18246
+ return wrapEmbeddedHerdrEndpointLifecycle(endpoint, embedded.runtime);
17365
18247
  }).pipe(Effect.catchDefect((error) => Effect.fail(error))),
17366
18248
  createRuntime: (input) => Effect.gen(function* () {
17367
18249
  const instanceState = join(STATE_DIR, "instances", input.instanceId);