@rivus/gateway 0.17.0 → 0.18.1
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/bootstrap/herdr-feishu.js +24 -797
- package/dist/bootstrap/pi-feishu.js +1161 -279
- package/dist/chunks/herdr-feishu-runtime.js +1993 -0
- package/dist/chunks/rivus-node-entrypoint.js +46 -39
- package/dist/chunks/runtime-cache.js +10 -9
- package/package.json +5 -4
|
@@ -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,
|
|
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";
|
|
@@ -976,7 +977,7 @@ var ModelStateRepositoryConflict = class extends Error {
|
|
|
976
977
|
};
|
|
977
978
|
function createInMemoryModelStateRepository(options) {
|
|
978
979
|
let current = cloneAndValidate$1(options.initial);
|
|
979
|
-
const serial =
|
|
980
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
980
981
|
return {
|
|
981
982
|
load: () => Effect.sync(() => structuredClone(current)),
|
|
982
983
|
persist: (input) => serial.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
|
|
@@ -1071,9 +1072,9 @@ function createModelChangeRunBoundary(options) {
|
|
|
1071
1072
|
admission.fence();
|
|
1072
1073
|
const remaining = remainingUntil(options.clock, input.deadlineAt);
|
|
1073
1074
|
if (remaining <= 0) return yield* Effect.fail(new ModelChangeBoundaryDeadlineError("Model change boundary deadline has passed"));
|
|
1074
|
-
yield* admission.whenDrained().pipe(Effect.
|
|
1075
|
+
yield* admission.whenDrained().pipe(Effect.timeoutOrElse({
|
|
1075
1076
|
duration: Math.min(remaining, 2147483647),
|
|
1076
|
-
|
|
1077
|
+
orElse: () => Effect.fail(new ModelChangeBoundaryDeadlineError("Active Runs did not drain before the model change deadline"))
|
|
1077
1078
|
}));
|
|
1078
1079
|
return {
|
|
1079
1080
|
activeRuns: 0,
|
|
@@ -1099,7 +1100,7 @@ const TOOL_VERSION = "1";
|
|
|
1099
1100
|
/** Reuses Broker policy, exact approval binding and its durable operation ledger. */
|
|
1100
1101
|
function createModelChangeAuthorization(options) {
|
|
1101
1102
|
if (typeof options.managementAgentId !== "string" || options.managementAgentId.trim() === "") throw new Error("managementAgentId must be a non-empty string");
|
|
1102
|
-
const serial =
|
|
1103
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
1103
1104
|
const digest = (value) => createToolInputDigest(value, options.digest);
|
|
1104
1105
|
const admissions = /* @__PURE__ */ new Map();
|
|
1105
1106
|
const broker = createToolBroker({
|
|
@@ -1666,46 +1667,46 @@ function executeModelChangePending(options, initialState, initialPending) {
|
|
|
1666
1667
|
phase: "waiting-for-boundary"
|
|
1667
1668
|
});
|
|
1668
1669
|
boundaryHeld = true;
|
|
1669
|
-
const boundary = yield* Effect.
|
|
1670
|
+
const boundary = yield* Effect.result(options.boundary.waitForSafeBoundary({
|
|
1670
1671
|
deadlineAt: pending.budget.deadlineAt,
|
|
1671
1672
|
requestId: pending.request.requestId
|
|
1672
1673
|
}));
|
|
1673
|
-
if (
|
|
1674
|
-
const authorization = yield* Effect.
|
|
1675
|
-
if (
|
|
1676
|
-
if (authorization.
|
|
1674
|
+
if (Result.isFailure(boundary)) return yield* saveFailed(modelErrorFrom(boundary.failure, "boundary"));
|
|
1675
|
+
const authorization = yield* Effect.result(revalidateAcceptedBinding());
|
|
1676
|
+
if (Result.isFailure(authorization)) return yield* saveFailed(modelErrorFrom(authorization.failure, "boundary"));
|
|
1677
|
+
if (authorization.success.status === "rejected") return yield* saveFailed(modelError(authorization.success.code, authorization.success.reason, "boundary"));
|
|
1677
1678
|
yield* savePending({
|
|
1678
1679
|
...pending,
|
|
1679
1680
|
phase: "validating"
|
|
1680
1681
|
});
|
|
1681
1682
|
const context = runtimeContext(state, pending);
|
|
1682
|
-
const drained = yield* Effect.
|
|
1683
|
+
const drained = yield* Effect.result(options.runtime.drain({
|
|
1683
1684
|
...context,
|
|
1684
1685
|
deadlineAt: pending.budget.deadlineAt
|
|
1685
1686
|
}));
|
|
1686
|
-
if (
|
|
1687
|
+
if (Result.isFailure(drained)) return yield* restoreAfterFailure({
|
|
1687
1688
|
context,
|
|
1688
|
-
error: modelErrorFrom(drained.
|
|
1689
|
+
error: modelErrorFrom(drained.failure, "activation"),
|
|
1689
1690
|
options,
|
|
1690
1691
|
saveFinal,
|
|
1691
1692
|
savePending,
|
|
1692
1693
|
state: () => state,
|
|
1693
1694
|
pending: () => pending
|
|
1694
1695
|
});
|
|
1695
|
-
const validated = yield* Effect.
|
|
1696
|
+
const validated = yield* Effect.result(options.runtime.validate({
|
|
1696
1697
|
context,
|
|
1697
1698
|
budget: executionBudget
|
|
1698
1699
|
}));
|
|
1699
|
-
if (
|
|
1700
|
+
if (Result.isFailure(validated)) return yield* restoreAfterFailure({
|
|
1700
1701
|
context,
|
|
1701
|
-
error: modelErrorFrom(validated.
|
|
1702
|
+
error: modelErrorFrom(validated.failure, "validation"),
|
|
1702
1703
|
options,
|
|
1703
1704
|
saveFinal,
|
|
1704
1705
|
savePending,
|
|
1705
1706
|
state: () => state,
|
|
1706
1707
|
pending: () => pending
|
|
1707
1708
|
});
|
|
1708
|
-
if (!validated.
|
|
1709
|
+
if (!validated.success.passed || !sameTarget(validated.success.model, target)) return yield* restoreAfterFailure({
|
|
1709
1710
|
context,
|
|
1710
1711
|
error: modelError("validation_failed", "Model validation did not prove the requested target", "validation"),
|
|
1711
1712
|
options,
|
|
@@ -1714,25 +1715,25 @@ function executeModelChangePending(options, initialState, initialPending) {
|
|
|
1714
1715
|
state: () => state,
|
|
1715
1716
|
pending: () => pending
|
|
1716
1717
|
});
|
|
1717
|
-
const activationAuthorization = yield* Effect.
|
|
1718
|
-
if (
|
|
1719
|
-
if (activationAuthorization.
|
|
1718
|
+
const activationAuthorization = yield* Effect.result(revalidateAcceptedBinding());
|
|
1719
|
+
if (Result.isFailure(activationAuthorization)) return yield* saveFailed(modelErrorFrom(activationAuthorization.failure, "activation"));
|
|
1720
|
+
if (activationAuthorization.success.status === "rejected") return yield* saveFailed(modelError(activationAuthorization.success.code, activationAuthorization.success.reason, "activation"));
|
|
1720
1721
|
yield* savePending({
|
|
1721
1722
|
...pending,
|
|
1722
1723
|
phase: "activating"
|
|
1723
1724
|
});
|
|
1724
|
-
const activated = yield* Effect.
|
|
1725
|
+
const activated = yield* Effect.result(options.runtime.activate({
|
|
1725
1726
|
beforeApply: () => Effect.gen(function* () {
|
|
1726
1727
|
const guard = yield* revalidateAcceptedBinding();
|
|
1727
1728
|
if (guard.status === "rejected") return yield* Effect.fail(new Error(guard.reason));
|
|
1728
1729
|
}),
|
|
1729
1730
|
context,
|
|
1730
|
-
validation: validated.
|
|
1731
|
+
validation: validated.success,
|
|
1731
1732
|
budget: executionBudget
|
|
1732
1733
|
}));
|
|
1733
|
-
if (
|
|
1734
|
+
if (Result.isFailure(activated)) return yield* restoreAfterFailure({
|
|
1734
1735
|
context,
|
|
1735
|
-
error: modelErrorFrom(activated.
|
|
1736
|
+
error: modelErrorFrom(activated.failure, "activation"),
|
|
1736
1737
|
options,
|
|
1737
1738
|
saveFinal,
|
|
1738
1739
|
savePending,
|
|
@@ -1755,7 +1756,7 @@ function executeModelChangePending(options, initialState, initialPending) {
|
|
|
1755
1756
|
const now = requireNow(options);
|
|
1756
1757
|
const applied = {
|
|
1757
1758
|
...pendingReceipt(state, pending, now),
|
|
1758
|
-
current: validated.
|
|
1759
|
+
current: validated.success.model,
|
|
1759
1760
|
phase: options.notification ? "notifying" : "completed",
|
|
1760
1761
|
previous: state.current,
|
|
1761
1762
|
revision: state.revision + 1,
|
|
@@ -1768,7 +1769,7 @@ function executeModelChangePending(options, initialState, initialPending) {
|
|
|
1768
1769
|
const { pending: _pending, recoveryRequired: _recoveryRequired, ...stateWithoutTransient } = state;
|
|
1769
1770
|
const appliedState = {
|
|
1770
1771
|
...stateWithoutTransient,
|
|
1771
|
-
current: validated.
|
|
1772
|
+
current: validated.success.model,
|
|
1772
1773
|
previous: state.current,
|
|
1773
1774
|
requests: {
|
|
1774
1775
|
...state.requests,
|
|
@@ -1777,28 +1778,28 @@ function executeModelChangePending(options, initialState, initialPending) {
|
|
|
1777
1778
|
revision: state.revision + 1,
|
|
1778
1779
|
updatedAt: now
|
|
1779
1780
|
};
|
|
1780
|
-
const committed = yield* Effect.
|
|
1781
|
-
if (
|
|
1781
|
+
const committed = yield* Effect.result(persistState(options, appliedState));
|
|
1782
|
+
if (Result.isFailure(committed)) return yield* restoreAfterFailure({
|
|
1782
1783
|
context,
|
|
1783
|
-
error: modelErrorFrom(committed.
|
|
1784
|
+
error: modelErrorFrom(committed.failure, "commit"),
|
|
1784
1785
|
options,
|
|
1785
1786
|
saveFinal,
|
|
1786
1787
|
savePending,
|
|
1787
1788
|
state: () => state,
|
|
1788
1789
|
pending: () => pending
|
|
1789
1790
|
});
|
|
1790
|
-
state = committed.
|
|
1791
|
+
state = committed.success;
|
|
1791
1792
|
pending = {
|
|
1792
1793
|
...pending,
|
|
1793
1794
|
phase: "completed",
|
|
1794
1795
|
updatedAt: now
|
|
1795
1796
|
};
|
|
1796
|
-
const release = yield* Effect.
|
|
1797
|
+
const release = yield* Effect.result(options.runtime.releasePrevious({
|
|
1797
1798
|
...context,
|
|
1798
1799
|
deadlineAt: pending.budget.deadlineAt
|
|
1799
1800
|
}));
|
|
1800
|
-
if (
|
|
1801
|
-
const releaseError = modelErrorFrom(release.
|
|
1801
|
+
if (Result.isFailure(release)) {
|
|
1802
|
+
const releaseError = modelErrorFrom(release.failure, "activation");
|
|
1802
1803
|
const withReleaseError = {
|
|
1803
1804
|
...applied,
|
|
1804
1805
|
error: releaseError,
|
|
@@ -1834,16 +1835,16 @@ function restoreAfterFailure(input) {
|
|
|
1834
1835
|
const { unknownPaidCallId: _unknownPaidCallId, ...withoutUnknown } = marked;
|
|
1835
1836
|
yield* input.savePending({ ...withoutUnknown });
|
|
1836
1837
|
}
|
|
1837
|
-
const restored = yield* Effect.
|
|
1838
|
+
const restored = yield* Effect.result(input.options.runtime.restore({
|
|
1838
1839
|
budget: executionBudgetForRecovery(input),
|
|
1839
1840
|
context: input.context,
|
|
1840
1841
|
reason: input.error.message
|
|
1841
1842
|
}));
|
|
1842
|
-
if (
|
|
1843
|
+
if (Result.isSuccess(restored) && !input.pending().unknownPaidCallId && Object.keys(input.pending().budget.reservations).length === 0) {
|
|
1843
1844
|
const receipt = restoredReceipt(input.state(), input.pending(), input.error, requireNow(input.options));
|
|
1844
1845
|
return yield* input.saveFinal(receipt);
|
|
1845
1846
|
}
|
|
1846
|
-
const recoveryError =
|
|
1847
|
+
const recoveryError = Result.isFailure(restored) ? modelErrorFrom(restored.failure, "restore") : modelError("recovery_unknown", "Model restore outcome is unknown", "restore", "unknown");
|
|
1847
1848
|
const receipt = recoveryReceipt(input.state(), input.pending(), recoveryError, requireNow(input.options));
|
|
1848
1849
|
return yield* input.saveFinal(receipt, true);
|
|
1849
1850
|
});
|
|
@@ -1950,7 +1951,7 @@ function recoverModelChangePending(options, initialState, initialPending) {
|
|
|
1950
1951
|
updatedAt: requireNow(options)
|
|
1951
1952
|
};
|
|
1952
1953
|
state = yield* persistState(options, stateWithPending(state, pending, pendingReceipt(state, pending, pending.updatedAt)));
|
|
1953
|
-
const restored = yield* Effect.
|
|
1954
|
+
const restored = yield* Effect.result(options.runtime.restore({
|
|
1954
1955
|
budget: executionBudgetForRecovery({
|
|
1955
1956
|
options,
|
|
1956
1957
|
pending: () => pending,
|
|
@@ -1964,11 +1965,11 @@ function recoverModelChangePending(options, initialState, initialPending) {
|
|
|
1964
1965
|
context,
|
|
1965
1966
|
reason: "recovering an interrupted Model change"
|
|
1966
1967
|
}));
|
|
1967
|
-
if (
|
|
1968
|
+
if (Result.isSuccess(restored) && !pending.unknownPaidCallId && Object.keys(pending.budget.reservations).length === 0) {
|
|
1968
1969
|
const receipt = restoredReceipt(state, pending, void 0, requireNow(options));
|
|
1969
1970
|
return yield* persistRecoveryReceipt(options, state, receipt);
|
|
1970
1971
|
}
|
|
1971
|
-
const error =
|
|
1972
|
+
const error = Result.isFailure(restored) ? modelErrorFrom(restored.failure, "restore") : modelError("recovery_unknown", "interrupted Model restore outcome is unknown", "restore", "unknown");
|
|
1972
1973
|
const receipt = recoveryReceipt(state, pending, error, requireNow(options));
|
|
1973
1974
|
return yield* persistRecoveryReceipt(options, state, receipt, error);
|
|
1974
1975
|
});
|
|
@@ -2022,7 +2023,7 @@ function notifyModelChangeReceipt(options, requestId, retry, withModelSerial) {
|
|
|
2022
2023
|
return Effect.gen(function* () {
|
|
2023
2024
|
const attempted = yield* withModelSerial(beginNotificationAttempt(options, requestId));
|
|
2024
2025
|
if (!attempted) return;
|
|
2025
|
-
return yield* withModelSerial(finishNotificationAttempt(options, requestId, attempted, yield* Effect.
|
|
2026
|
+
return yield* withModelSerial(finishNotificationAttempt(options, requestId, attempted, yield* Effect.result(notification.notify({
|
|
2026
2027
|
receipt: toPublicReceipt(attempted),
|
|
2027
2028
|
retry
|
|
2028
2029
|
}))));
|
|
@@ -2051,7 +2052,7 @@ function finishNotificationAttempt(options, requestId, attempted, delivered) {
|
|
|
2051
2052
|
const latest = state.requests[requestId];
|
|
2052
2053
|
if (!latest || !latest.notification || latest.notification.status === "sent") return latest;
|
|
2053
2054
|
const updatedAt = requireNow(options);
|
|
2054
|
-
return (yield* persistReceiptPatch(options, state,
|
|
2055
|
+
return (yield* persistReceiptPatch(options, state, Result.isSuccess(delivered) ? {
|
|
2055
2056
|
...latest,
|
|
2056
2057
|
notification: {
|
|
2057
2058
|
...latest.notification,
|
|
@@ -2083,8 +2084,8 @@ function persistReceiptPatch(options, state, receipt) {
|
|
|
2083
2084
|
//#endregion
|
|
2084
2085
|
//#region src/core/application/deployment/model/model-change-coordinator.ts
|
|
2085
2086
|
function createModelChangeCoordinator(options) {
|
|
2086
|
-
const serial =
|
|
2087
|
-
const notificationSerial =
|
|
2087
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
2088
|
+
const notificationSerial = Semaphore.makeUnsafe(1);
|
|
2088
2089
|
return {
|
|
2089
2090
|
accept: (submission, transport) => serial.withPermits(1)(Effect.uninterruptible(acceptRequest(options, submission, transport))).pipe(Effect.flatMap((receipt) => notifyPendingReceipt(options, receipt, serial, notificationSerial))),
|
|
2090
2091
|
recover: () => serial.withPermits(1)(Effect.uninterruptible(recoverModelChangeState(options))).pipe(Effect.flatMap(() => retryPendingNotifications(options, serial, notificationSerial))),
|
|
@@ -2099,7 +2100,7 @@ function createModelChangeCoordinator(options) {
|
|
|
2099
2100
|
}),
|
|
2100
2101
|
status: (requestId) => Effect.gen(function* () {
|
|
2101
2102
|
const state = yield* loadState$1(options);
|
|
2102
|
-
const actual = options.runtime.current ? yield* options.runtime.current().pipe(Effect.
|
|
2103
|
+
const actual = options.runtime.current ? yield* options.runtime.current().pipe(Effect.catch(() => Effect.succeed(void 0))) : void 0;
|
|
2103
2104
|
const receipt = requestId === void 0 ? void 0 : state.requests[requestId];
|
|
2104
2105
|
if (requestId !== void 0 && !receipt) return yield* Effect.fail(new ModelChangeNotFound(`unknown Model change request: ${requestId}`));
|
|
2105
2106
|
return toStatusView(state, actual, receipt);
|
|
@@ -2735,14 +2736,14 @@ function createBackgroundSessionRunStep(input) {
|
|
|
2735
2736
|
})));
|
|
2736
2737
|
return yield* Effect.raceFirst(run, waitForAbort$2(signal));
|
|
2737
2738
|
}).pipe(Effect.onExit((exit) => Effect.suspend(() => {
|
|
2738
|
-
cancellationRequested = signal.aborted || Exit.
|
|
2739
|
+
cancellationRequested = signal.aborted || Exit.hasInterrupts(exit);
|
|
2739
2740
|
return cancellationRequested ? cancelAcceptedRun() : Effect.void;
|
|
2740
2741
|
})));
|
|
2741
2742
|
});
|
|
2742
2743
|
};
|
|
2743
2744
|
}
|
|
2744
2745
|
function waitForAbort$2(signal) {
|
|
2745
|
-
return Effect.
|
|
2746
|
+
return Effect.callback((resume) => {
|
|
2746
2747
|
const aborted = () => resume(Effect.fail(signal.reason ?? /* @__PURE__ */ new Error("background session step aborted")));
|
|
2747
2748
|
if (signal.aborted) {
|
|
2748
2749
|
aborted();
|
|
@@ -4494,7 +4495,7 @@ function createFeishuCardRollover(options) {
|
|
|
4494
4495
|
const latestTextByRun = /* @__PURE__ */ new Map();
|
|
4495
4496
|
const generationBaselineByRun = /* @__PURE__ */ new Map();
|
|
4496
4497
|
const liveRuns = /* @__PURE__ */ new Map();
|
|
4497
|
-
const semaphore =
|
|
4498
|
+
const semaphore = Semaphore.makeUnsafe(1);
|
|
4498
4499
|
const exclusive = (effect) => semaphore.withPermits(1)(effect);
|
|
4499
4500
|
const exclusiveDeferred = (factory) => semaphore.withPermits(1)(Effect.suspend(factory));
|
|
4500
4501
|
const record = (event, at) => {
|
|
@@ -4508,7 +4509,7 @@ function createFeishuCardRollover(options) {
|
|
|
4508
4509
|
const failHandoffAndRecord = (input) => options.store.failHandoff({
|
|
4509
4510
|
failedAt: input.failedAt,
|
|
4510
4511
|
runId: input.runId
|
|
4511
|
-
}).pipe(Effect.
|
|
4512
|
+
}).pipe(Effect.catch((storeError) => recordNow({
|
|
4512
4513
|
error: storeError,
|
|
4513
4514
|
runId: input.runId,
|
|
4514
4515
|
type: "presentation_write_failed"
|
|
@@ -4548,7 +4549,7 @@ function createFeishuCardRollover(options) {
|
|
|
4548
4549
|
if (run && acceptsProgress) {
|
|
4549
4550
|
const now = yield* options.clock.now;
|
|
4550
4551
|
if (visibleChanged && isCardPresentationHandoffDue(chain, now.toISOString())) {
|
|
4551
|
-
if (yield* options.publisher.flush(action.runId).pipe(Effect.map(() => true), Effect.
|
|
4552
|
+
if (yield* options.publisher.flush(action.runId).pipe(Effect.map(() => true), Effect.catch((error) => recordNow({
|
|
4552
4553
|
error,
|
|
4553
4554
|
runId: action.runId,
|
|
4554
4555
|
type: "presentation_write_failed"
|
|
@@ -4605,7 +4606,7 @@ function createFeishuCardRollover(options) {
|
|
|
4605
4606
|
})), Effect.tap(() => options.store.markTerminal({
|
|
4606
4607
|
runId: action.runId,
|
|
4607
4608
|
terminalReceiptId: `${action.runId}:${action.type}`
|
|
4608
|
-
}).pipe(Effect.
|
|
4609
|
+
}).pipe(Effect.catch((error) => recordNow({
|
|
4609
4610
|
error,
|
|
4610
4611
|
runId: action.runId,
|
|
4611
4612
|
type: "presentation_write_failed"
|
|
@@ -4643,7 +4644,7 @@ function createFeishuCardRollover(options) {
|
|
|
4643
4644
|
const start = yield* options.store.beginHandoff({
|
|
4644
4645
|
handoffAt: startedAt.toISOString(),
|
|
4645
4646
|
runId
|
|
4646
|
-
}).pipe(Effect.
|
|
4647
|
+
}).pipe(Effect.catch(() => Effect.succeed(void 0)));
|
|
4647
4648
|
if (!start) return {
|
|
4648
4649
|
reason: "not-streaming",
|
|
4649
4650
|
status: "skipped"
|
|
@@ -4667,7 +4668,7 @@ function createFeishuCardRollover(options) {
|
|
|
4667
4668
|
generation,
|
|
4668
4669
|
presentationId: successorId,
|
|
4669
4670
|
run
|
|
4670
|
-
}).pipe(Effect.map((target) => ({ target })), Effect.
|
|
4671
|
+
}).pipe(Effect.map((target) => ({ target })), Effect.catch((error) => failHandoffAndRecord({
|
|
4671
4672
|
error,
|
|
4672
4673
|
failedAt: startedAt.toISOString(),
|
|
4673
4674
|
generation,
|
|
@@ -4689,7 +4690,7 @@ function createFeishuCardRollover(options) {
|
|
|
4689
4690
|
leaseDeadlineAt: leaseDeadline(adoptedAt),
|
|
4690
4691
|
presentationId: successorId
|
|
4691
4692
|
}
|
|
4692
|
-
}).pipe(Effect.map((presentation) => ({ presentation })), Effect.
|
|
4693
|
+
}).pipe(Effect.map((presentation) => ({ presentation })), Effect.catch((error) => failHandoffAndRecord({
|
|
4693
4694
|
error,
|
|
4694
4695
|
failedAt: adoptedAt.toISOString(),
|
|
4695
4696
|
generation,
|
|
@@ -4980,7 +4981,7 @@ function createCoalescingFeishuPublisher(options) {
|
|
|
4980
4981
|
//#region src/adapters/feishu/card-presentation/streaming/feishu-rate-limited-publisher.ts
|
|
4981
4982
|
function createRateLimitedFeishuPublisher(options) {
|
|
4982
4983
|
let hasPublished = false;
|
|
4983
|
-
const semaphore =
|
|
4984
|
+
const semaphore = Semaphore.makeUnsafe(1);
|
|
4984
4985
|
return { publish: (action) => semaphore.withPermits(1)(Effect.gen(function* () {
|
|
4985
4986
|
if (hasPublished) yield* options.sleep(options.minIntervalMs);
|
|
4986
4987
|
yield* options.publisher.publish(action);
|
|
@@ -5156,7 +5157,7 @@ function createFeishuCardKitOpenApiClient(options) {
|
|
|
5156
5157
|
const updateTerminal = (request, status) => updateWholeCard(baseUrl, agentName, request, status, call, options.progressDisplay).pipe(Effect.tap(() => Effect.sync(() => streamingClosedCards.delete(request.cardId))));
|
|
5157
5158
|
const publishStreamingUpdate = (cardId, update) => {
|
|
5158
5159
|
if (streamingClosedCards.has(cardId)) return Effect.void;
|
|
5159
|
-
return update.pipe(Effect.
|
|
5160
|
+
return update.pipe(Effect.catch((error) => {
|
|
5160
5161
|
if (!isStreamingClosed(error)) return Effect.fail(error);
|
|
5161
5162
|
return Effect.sync(() => {
|
|
5162
5163
|
streamingClosedCards.add(cardId);
|
|
@@ -5313,7 +5314,7 @@ function createCardDeliveryLedger(repository) {
|
|
|
5313
5314
|
//#region src/adapters/outbound/persistence/card-delivery/in-memory-card-delivery-repository.ts
|
|
5314
5315
|
function createInMemoryCardDeliveryRepository(options = {}) {
|
|
5315
5316
|
const records = new Map(options.initial?.map((record) => [record.runId, cloneRecord(record)]) ?? []);
|
|
5316
|
-
const semaphore =
|
|
5317
|
+
const semaphore = Semaphore.makeUnsafe(1);
|
|
5317
5318
|
return {
|
|
5318
5319
|
get: (runId) => cloneOptional(records.get(runId)),
|
|
5319
5320
|
transact: (runId, decide) => semaphore.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
|
|
@@ -5812,7 +5813,7 @@ function createFeishuCardDeliveryReconciler(options) {
|
|
|
5812
5813
|
let skipped = 0;
|
|
5813
5814
|
for (const [runId, action] of terminalByRun) {
|
|
5814
5815
|
if (options.ledger.isTerminalPublished(runId)) continue;
|
|
5815
|
-
yield* options.publish(action).pipe(Effect.
|
|
5816
|
+
yield* options.publish(action).pipe(Effect.catch((error) => {
|
|
5816
5817
|
if (isMissingPresentationError(error)) {
|
|
5817
5818
|
skipped += 1;
|
|
5818
5819
|
return Effect.void;
|
|
@@ -6046,7 +6047,7 @@ function createConfiguredFeishuCardRolloverRuntime(options) {
|
|
|
6046
6047
|
transport: {
|
|
6047
6048
|
running: () => supervisor.running() && streaming.running(),
|
|
6048
6049
|
start: async () => {
|
|
6049
|
-
await Effect.runPromise(supervisor.recover().pipe(Effect.
|
|
6050
|
+
await Effect.runPromise(supervisor.recover().pipe(Effect.catch((error) => {
|
|
6050
6051
|
const reported = options.onError?.(error);
|
|
6051
6052
|
return reported instanceof Promise ? Effect.promise(() => reported) : Effect.void;
|
|
6052
6053
|
})));
|
|
@@ -6262,7 +6263,7 @@ function readResource(baseUrl, fetchImpl, getTenantAccessToken, input) {
|
|
|
6262
6263
|
message: "Feishu tenant access token is unavailable"
|
|
6263
6264
|
})));
|
|
6264
6265
|
if (input.signal?.aborted === true) return yield* Effect.fail(cancelledError$1());
|
|
6265
|
-
return yield* Effect.
|
|
6266
|
+
return yield* Effect.callback((resume, effectSignal) => {
|
|
6266
6267
|
const controller = new AbortController();
|
|
6267
6268
|
const onAbort = () => controller.abort();
|
|
6268
6269
|
const cleanupCaller = addAbortListener(input.signal, onAbort);
|
|
@@ -7481,14 +7482,14 @@ function readPages(readPage, input, limits) {
|
|
|
7481
7482
|
...pageToken === void 0 ? {} : { pageToken },
|
|
7482
7483
|
...input.signal ? { signal: input.signal } : {}
|
|
7483
7484
|
};
|
|
7484
|
-
const pageResult = yield* Effect.
|
|
7485
|
+
const pageResult = yield* Effect.result(readPageWithSignal(readPage, request, input.signal));
|
|
7485
7486
|
pageCount += 1;
|
|
7486
|
-
if (pageResult._tag === "
|
|
7487
|
+
if (pageResult._tag === "Failure") {
|
|
7487
7488
|
if (input.signal !== void 0 && input.signal.aborted) return yield* Effect.fail(cancelledError());
|
|
7488
7489
|
notices.push(contextNotice("unavailable", "无法读取引用消息的完整内容"));
|
|
7489
7490
|
break;
|
|
7490
7491
|
}
|
|
7491
|
-
const parsed = parsePage(pageResult.
|
|
7492
|
+
const parsed = parsePage(pageResult.success);
|
|
7492
7493
|
if (parsed === void 0) {
|
|
7493
7494
|
notices.push(contextNotice("invalid_content", "引用消息接口返回结构无效"));
|
|
7494
7495
|
break;
|
|
@@ -7530,7 +7531,7 @@ function readPageWithSignal(readPage, request, signal) {
|
|
|
7530
7531
|
return signal === void 0 ? invoked : Effect.raceFirst(invoked, waitForAbort$1(signal));
|
|
7531
7532
|
}
|
|
7532
7533
|
function waitForAbort$1(signal) {
|
|
7533
|
-
return Effect.
|
|
7534
|
+
return Effect.callback((resume) => {
|
|
7534
7535
|
const cancelled = Effect.fail(signal.reason ?? /* @__PURE__ */ new Error("Feishu message context was cancelled"));
|
|
7535
7536
|
const onAbort = () => resume(cancelled);
|
|
7536
7537
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
@@ -8591,7 +8592,7 @@ function writeExclusiveReplacing(rootDirectory, path, bytes) {
|
|
|
8591
8592
|
flag: "wx",
|
|
8592
8593
|
mode: 384
|
|
8593
8594
|
}));
|
|
8594
|
-
return Effect.tryPromise(write).pipe(Effect.
|
|
8595
|
+
return Effect.tryPromise(write).pipe(Effect.catch((error) => error.code === "EEXIST" ? Effect.tryPromise(async () => rm(await resolveSafeWorkspacePath(rootDirectory, path), { force: true })).pipe(Effect.flatMap(() => Effect.tryPromise(write))) : Effect.fail(error)));
|
|
8595
8596
|
}
|
|
8596
8597
|
function verifyStoredPath(stored, rootDirectory, additionalRootDirectory) {
|
|
8597
8598
|
return Effect.tryPromise(async () => {
|
|
@@ -8669,7 +8670,7 @@ function materializeRecoveryOutput(input) {
|
|
|
8669
8670
|
outputPath: relativePath === "" ? outputPath : relativePath,
|
|
8670
8671
|
sizeBytes: bytes.byteLength
|
|
8671
8672
|
};
|
|
8672
|
-
}).pipe(Effect.
|
|
8673
|
+
}).pipe(Effect.catch((error) => Effect.fail(attachConsumedBytes(error, input.output.sizeBytes))));
|
|
8673
8674
|
}
|
|
8674
8675
|
/** The stored record of a recovery output; the caller states where its bytes are and what they measured. */
|
|
8675
8676
|
function recoveredAttachment(grant, mimeType, path, size, digest) {
|
|
@@ -8903,13 +8904,13 @@ function createAttachmentAccess(input) {
|
|
|
8903
8904
|
output,
|
|
8904
8905
|
scope: input.scope,
|
|
8905
8906
|
workspaceDirectory: input.workspaceDirectory
|
|
8906
|
-
})), Effect.
|
|
8907
|
+
})), Effect.catch((error) => Effect.fail(attachConsumedBytes(error, output.sizeBytes))))), Effect.result, Effect.flatMap((result) => {
|
|
8907
8908
|
const activeTimeMs = Math.min(reservation.timeReservationMs, Math.max(0, Date.now() - startedAt));
|
|
8908
8909
|
return grant.ledger.complete(reservation, {
|
|
8909
8910
|
activeTimeMs,
|
|
8910
|
-
bytes: result._tag === "
|
|
8911
|
-
status: result._tag === "
|
|
8912
|
-
}).pipe(Effect.flatMap(() => result._tag === "
|
|
8911
|
+
bytes: result._tag === "Success" ? result.success.sizeBytes : consumedBytesOf(result.failure, reservation.byteReservation),
|
|
8912
|
+
status: result._tag === "Success" ? "stored" : signal.aborted ? "cancelled" : "failed"
|
|
8913
|
+
}).pipe(Effect.flatMap(() => result._tag === "Success" ? Effect.succeed(result.success) : Effect.fail(result.failure)));
|
|
8913
8914
|
}))));
|
|
8914
8915
|
} : void 0;
|
|
8915
8916
|
const readBytes = (attachmentId) => {
|
|
@@ -9080,14 +9081,14 @@ function downloadAndPublish(resource, request, reader, rootDirectory, signal, bu
|
|
|
9080
9081
|
yield* writeExclusiveReplacing(rootDirectory, metadataTempPath, new TextEncoder().encode(JSON.stringify(metadata)));
|
|
9081
9082
|
yield* Effect.tryPromise(async () => rename(await resolveSafeWorkspacePath(rootDirectory, metadataTempPath), await resolveSafeWorkspacePath(rootDirectory, metadataPath)));
|
|
9082
9083
|
return stored;
|
|
9083
|
-
}).pipe(Effect.
|
|
9084
|
-
if (publication._tag === "
|
|
9084
|
+
}).pipe(Effect.result);
|
|
9085
|
+
if (publication._tag === "Failure") return yield* failPrepared(publication.failure);
|
|
9085
9086
|
yield* ledger.complete(prepared.reservation, {
|
|
9086
9087
|
activeTimeMs: activeTimeMs(),
|
|
9087
9088
|
bytes: bytes.byteLength,
|
|
9088
9089
|
status: "stored"
|
|
9089
9090
|
});
|
|
9090
|
-
return publication.
|
|
9091
|
+
return publication.success;
|
|
9091
9092
|
}).pipe(Effect.ensuring(Effect.tryPromise(async () => rm(await resolveSafeWorkspacePath(rootDirectory, temporaryPath), { force: true })).pipe(Effect.ignore)));
|
|
9092
9093
|
}
|
|
9093
9094
|
function readWithRetries(reader, input, outerSignal, budget) {
|
|
@@ -9112,27 +9113,27 @@ function readWithRetries(reader, input, outerSignal, budget) {
|
|
|
9112
9113
|
timeReservationMs: remainingMs
|
|
9113
9114
|
});
|
|
9114
9115
|
const attemptStarted = Date.now();
|
|
9115
|
-
const result = yield* readAttempt(reader, input, outerSignal, Math.min(attemptTimeoutMs, reservation.timeReservationMs)).pipe(Effect.
|
|
9116
|
-
if (result._tag === "
|
|
9116
|
+
const result = yield* readAttempt(reader, input, outerSignal, Math.min(attemptTimeoutMs, reservation.timeReservationMs)).pipe(Effect.result);
|
|
9117
|
+
if (result._tag === "Success") {
|
|
9117
9118
|
const activeTimeMs = Math.min(reservation.timeReservationMs, Math.max(0, Date.now() - attemptStarted));
|
|
9118
|
-
if (result.
|
|
9119
|
+
if (result.success.bytes.byteLength > maxBytes) {
|
|
9119
9120
|
yield* ledger.complete(reservation, {
|
|
9120
9121
|
activeTimeMs,
|
|
9121
|
-
bytes: Math.min(result.
|
|
9122
|
+
bytes: Math.min(result.success.bytes.byteLength, reservation.byteReservation),
|
|
9122
9123
|
status: "failed"
|
|
9123
9124
|
});
|
|
9124
9125
|
return yield* Effect.fail(new FeishuAttachmentLimitExceeded("attachment byte limit exceeded"));
|
|
9125
9126
|
}
|
|
9126
9127
|
return {
|
|
9127
9128
|
activeTimeMs,
|
|
9128
|
-
read: result.
|
|
9129
|
+
read: result.success,
|
|
9129
9130
|
reservation,
|
|
9130
9131
|
startedAtMs: attemptStarted
|
|
9131
9132
|
};
|
|
9132
9133
|
}
|
|
9133
|
-
lastError = result.
|
|
9134
|
+
lastError = result.failure;
|
|
9134
9135
|
const delay = retryBackoffMs[attempt] ?? retryBackoffMs.at(-1) ?? 0;
|
|
9135
|
-
const retry = isRetryableDownloadError(result.
|
|
9136
|
+
const retry = isRetryableDownloadError(result.failure) && attempt + 1 < maxAttempts;
|
|
9136
9137
|
if (retry && delay > 0) yield* sleep(delay);
|
|
9137
9138
|
if (outerSignal.aborted) {
|
|
9138
9139
|
yield* ledger.cancel().pipe(Effect.ignore);
|
|
@@ -9140,8 +9141,8 @@ function readWithRetries(reader, input, outerSignal, budget) {
|
|
|
9140
9141
|
}
|
|
9141
9142
|
yield* ledger.complete(reservation, {
|
|
9142
9143
|
activeTimeMs: Math.min(reservation.timeReservationMs, Math.max(0, Date.now() - attemptStarted)),
|
|
9143
|
-
bytes: consumedBytesOf(result.
|
|
9144
|
-
retryableFailure: isRetryableDownloadError(result.
|
|
9144
|
+
bytes: consumedBytesOf(result.failure, reservation.byteReservation),
|
|
9145
|
+
retryableFailure: isRetryableDownloadError(result.failure),
|
|
9145
9146
|
status: "failed"
|
|
9146
9147
|
});
|
|
9147
9148
|
if (!retry) break;
|
|
@@ -9163,9 +9164,9 @@ function readAttempt(reader, input, outerSignal, timeoutMs) {
|
|
|
9163
9164
|
const result = yield* reader.read({
|
|
9164
9165
|
...input,
|
|
9165
9166
|
signal: controller.signal
|
|
9166
|
-
}).pipe(Effect.
|
|
9167
|
-
if (result._tag === "
|
|
9168
|
-
return yield* result;
|
|
9167
|
+
}).pipe(Effect.result);
|
|
9168
|
+
if (result._tag === "Failure" && timedOut) return yield* Effect.fail(attachConsumedBytes(/* @__PURE__ */ new Error("attachment download timeout"), consumedBytesOf(result.failure, input.maxBytes)));
|
|
9169
|
+
return yield* Effect.fromResult(result);
|
|
9169
9170
|
} finally {
|
|
9170
9171
|
clearTimeout(timer);
|
|
9171
9172
|
outerSignal.removeEventListener("abort", onAbort);
|
|
@@ -9377,13 +9378,13 @@ function createFeishuAttachmentPreparation(options) {
|
|
|
9377
9378
|
retryBackoffMs,
|
|
9378
9379
|
sleep,
|
|
9379
9380
|
startedAt
|
|
9380
|
-
}).pipe(Effect.
|
|
9381
|
-
if (stored._tag === "
|
|
9381
|
+
}).pipe(Effect.result);
|
|
9382
|
+
if (stored._tag === "Failure") {
|
|
9382
9383
|
if (signal.aborted) return yield* Effect.fail(new FeishuAttachmentPreparationCancelled());
|
|
9383
|
-
destination.push(yield* failureNotice(resource, fallbackMessageId, classifyFailure(stored.
|
|
9384
|
+
destination.push(yield* failureNotice(resource, fallbackMessageId, classifyFailure(stored.failure), outputPath, maxBytes));
|
|
9384
9385
|
continue;
|
|
9385
9386
|
}
|
|
9386
|
-
accept(destination, attachmentId, resource, stored.
|
|
9387
|
+
accept(destination, attachmentId, resource, stored.success);
|
|
9387
9388
|
}
|
|
9388
9389
|
});
|
|
9389
9390
|
yield* processParts(request.decoded.parts, parts, request.messageId);
|
|
@@ -9430,7 +9431,7 @@ function createFeishuAttachmentPreparation(options) {
|
|
|
9430
9431
|
}),
|
|
9431
9432
|
userInput
|
|
9432
9433
|
};
|
|
9433
|
-
}).pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort()).pipe(Effect.
|
|
9434
|
+
}).pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort()).pipe(Effect.andThen(ledger.cancel().pipe(Effect.ignore)))), Effect.ensuring(Effect.sync(() => {
|
|
9434
9435
|
unregisterPreparation(request.sessionKey, controller);
|
|
9435
9436
|
})));
|
|
9436
9437
|
}
|
|
@@ -9796,7 +9797,7 @@ function effectForCandidate(operation) {
|
|
|
9796
9797
|
return Effect.tryPromise({
|
|
9797
9798
|
try: operation,
|
|
9798
9799
|
catch: () => /* @__PURE__ */ new Error("attachment retention candidate inspection failed")
|
|
9799
|
-
}).pipe(Effect.
|
|
9800
|
+
}).pipe(Effect.catch(() => Effect.succeed("error")));
|
|
9800
9801
|
}
|
|
9801
9802
|
function inspectPreparedCandidate(input) {
|
|
9802
9803
|
return effectForCandidate(async () => {
|
|
@@ -10575,7 +10576,7 @@ function createFeishuCotPublisher(options) {
|
|
|
10575
10576
|
url: `${baseUrl}/open-apis/im/v1/message_cot/complete/${encodeURIComponent(target.cotId)}?message_id=${encodeURIComponent(target.messageId)}&reason=${reason}`
|
|
10576
10577
|
}, "Failed to complete Feishu COT message").pipe(Effect.timeout(COT_REQUEST_TIMEOUT), Effect.asVoid);
|
|
10577
10578
|
const failTarget = (runId, target) => complete(target, "error").pipe(Effect.ensuring(Effect.sync(() => targets.delete(runId))));
|
|
10578
|
-
const abandonOnError = (runId, target, operation) => operation.pipe(Effect.
|
|
10579
|
+
const abandonOnError = (runId, target, operation) => operation.pipe(Effect.catch((error) => failTarget(runId, target).pipe(Effect.catch(() => Effect.void), Effect.flatMap(() => Effect.fail(error)))));
|
|
10579
10580
|
const flushReasoning = (target, timestamp) => {
|
|
10580
10581
|
if (target.pendingReasoning.length === 0) return [];
|
|
10581
10582
|
const delta = readRedactedString(redact(target.pendingReasoning));
|
|
@@ -10987,7 +10988,7 @@ function createMessageGateway(options) {
|
|
|
10987
10988
|
case "resolve_interaction": return yield* resolveInteraction(action);
|
|
10988
10989
|
case "prompt":
|
|
10989
10990
|
markSeen(key);
|
|
10990
|
-
return yield* executePrompt(execution, observation, presentation, action).pipe(Effect.
|
|
10991
|
+
return yield* executePrompt(execution, observation, presentation, action).pipe(Effect.catch((error) => {
|
|
10991
10992
|
if (error instanceof AgentRunCancelled) return Effect.succeed({
|
|
10992
10993
|
cancelled: true,
|
|
10993
10994
|
messageId: action.messageId,
|
|
@@ -10997,7 +10998,7 @@ function createMessageGateway(options) {
|
|
|
10997
10998
|
return Effect.fail(error);
|
|
10998
10999
|
}));
|
|
10999
11000
|
}
|
|
11000
|
-
}).pipe(Effect.
|
|
11001
|
+
}).pipe(Effect.catch((error) => {
|
|
11001
11002
|
if (error instanceof AgentRunCancelled && (direct || action.type === "prompt")) return Effect.succeed({
|
|
11002
11003
|
cancelled: true,
|
|
11003
11004
|
messageId: action.messageId,
|
|
@@ -11450,7 +11451,7 @@ function parseCancelRunCommand(messageId, text) {
|
|
|
11450
11451
|
//#region src/adapters/feishu/conversation-session/feishu-session-store.ts
|
|
11451
11452
|
function createFeishuSessionStore(options = {}) {
|
|
11452
11453
|
const records = recoverFeishuSessionRecords(options.initial ?? []);
|
|
11453
|
-
const serial =
|
|
11454
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
11454
11455
|
return {
|
|
11455
11456
|
current: (baseSessionKey) => {
|
|
11456
11457
|
validateFeishuBaseSessionKey(baseSessionKey);
|
|
@@ -11666,25 +11667,25 @@ function createFeishuAgentDaemon(options) {
|
|
|
11666
11667
|
replyReset: (messageId, hadActiveRun) => sideEffectsDisabled || !options.reply ? Effect.void : options.reply(messageId, hadActiveRun ? "已停止当前任务并开启新会话。请发送下一条指令。" : "已开启新会话。请发送下一条指令。"),
|
|
11667
11668
|
replySteered: (messageId) => {
|
|
11668
11669
|
if (sideEffectsDisabled) return Effect.void;
|
|
11669
|
-
if (options.acknowledgeSteering) return options.acknowledgeSteering(messageId).pipe(Effect.
|
|
11670
|
+
if (options.acknowledgeSteering) return options.acknowledgeSteering(messageId).pipe(Effect.catch(() => options.reply ? options.reply(messageId, STEERING_ACK_TEXT) : Effect.void));
|
|
11670
11671
|
return options.reply ? options.reply(messageId, STEERING_ACK_TEXT) : Effect.void;
|
|
11671
11672
|
}
|
|
11672
11673
|
}
|
|
11673
11674
|
};
|
|
11674
11675
|
};
|
|
11675
11676
|
const trySteerMessage = (payload) => Effect.gen(function* () {
|
|
11676
|
-
const resetDirective = yield* readFeishuSessionResetDirective(payload, messageIntakeOptions).pipe(Effect.
|
|
11677
|
-
if (
|
|
11678
|
-
cancelAttachmentPreparation(resetDirective.
|
|
11677
|
+
const resetDirective = yield* readFeishuSessionResetDirective(payload, messageIntakeOptions).pipe(Effect.result);
|
|
11678
|
+
if (Result.isSuccess(resetDirective) && resetDirective.success?.prompt) {
|
|
11679
|
+
cancelAttachmentPreparation(resetDirective.success.previousSessionKey);
|
|
11679
11680
|
yield* execution.cancelActiveRun({
|
|
11680
11681
|
reason: "Feishu new session command",
|
|
11681
|
-
sessionKey: resetDirective.
|
|
11682
|
+
sessionKey: resetDirective.success.previousSessionKey
|
|
11682
11683
|
});
|
|
11683
11684
|
return;
|
|
11684
11685
|
}
|
|
11685
|
-
const parsed = yield* createAgentCommandFromFeishuMessage(payload, messageIntakeOptions).pipe(Effect.
|
|
11686
|
-
if (
|
|
11687
|
-
const inbound = parsed.
|
|
11686
|
+
const parsed = yield* createAgentCommandFromFeishuMessage(payload, messageIntakeOptions).pipe(Effect.result);
|
|
11687
|
+
if (Result.isFailure(parsed)) return;
|
|
11688
|
+
const inbound = parsed.success;
|
|
11688
11689
|
const { observation, presentation, reply } = buildPorts(false);
|
|
11689
11690
|
if (inbound.type === "cancel_run") {
|
|
11690
11691
|
cancelPreparationForCommand(inbound);
|
|
@@ -11712,27 +11713,27 @@ function createFeishuAgentDaemon(options) {
|
|
|
11712
11713
|
return steered ? fromGatewayResult(steered) : void 0;
|
|
11713
11714
|
});
|
|
11714
11715
|
const prepareControlMessage = (payload, phase = "admission") => Effect.gen(function* () {
|
|
11715
|
-
const resetDirective = yield* readFeishuSessionResetDirective(payload, messageIntakeOptions).pipe(Effect.
|
|
11716
|
+
const resetDirective = yield* readFeishuSessionResetDirective(payload, messageIntakeOptions).pipe(Effect.result);
|
|
11716
11717
|
const prepared = () => ({ execute: () => trySteerMessage(payload) });
|
|
11717
|
-
if (
|
|
11718
|
-
if (
|
|
11719
|
-
if (resetDirective.
|
|
11718
|
+
if (Result.isFailure(resetDirective)) yield* failCausedIntake(resetDirective.failure);
|
|
11719
|
+
if (Result.isSuccess(resetDirective) && resetDirective.success) {
|
|
11720
|
+
if (resetDirective.success.prompt.length > 0) {
|
|
11720
11721
|
if (phase === "processing") return;
|
|
11721
|
-
const activeRunId = execution.getActiveRun ? yield* execution.getActiveRun({ sessionKey: resetDirective.
|
|
11722
|
-
const preparing = options.attachmentPreparation?.hasActive?.(resetDirective.
|
|
11722
|
+
const activeRunId = execution.getActiveRun ? yield* execution.getActiveRun({ sessionKey: resetDirective.success.previousSessionKey }) : void 0;
|
|
11723
|
+
const preparing = options.attachmentPreparation?.hasActive?.(resetDirective.success.previousSessionKey) ?? false;
|
|
11723
11724
|
return activeRunId || preparing ? prepared() : void 0;
|
|
11724
11725
|
}
|
|
11725
11726
|
return prepared();
|
|
11726
11727
|
}
|
|
11727
11728
|
if (feishuMessageRequiresWorkerPreparation(payload)) return;
|
|
11728
|
-
const intake = yield* describeFeishuMessageIntake(payload, messageIntakeOptions).pipe(Effect.
|
|
11729
|
-
if (
|
|
11730
|
-
yield* failCausedIntake(intake.
|
|
11729
|
+
const intake = yield* describeFeishuMessageIntake(payload, messageIntakeOptions).pipe(Effect.result);
|
|
11730
|
+
if (Result.isFailure(intake)) {
|
|
11731
|
+
yield* failCausedIntake(intake.failure);
|
|
11731
11732
|
return;
|
|
11732
11733
|
}
|
|
11733
|
-
if (intake.
|
|
11734
|
+
if (intake.success.commandType !== "prompt") return prepared();
|
|
11734
11735
|
if (!execution.getActiveRun) return;
|
|
11735
|
-
return (yield* execution.getActiveRun({ sessionKey: intake.
|
|
11736
|
+
return (yield* execution.getActiveRun({ sessionKey: intake.success.sessionKey })) ? prepared() : void 0;
|
|
11736
11737
|
});
|
|
11737
11738
|
return {
|
|
11738
11739
|
describeMessageIntake: (payload) => describeFeishuMessageIntake(payload, {
|
|
@@ -11815,7 +11816,7 @@ function createFeishuAgentDaemon(options) {
|
|
|
11815
11816
|
});
|
|
11816
11817
|
if (!result) return yield* Effect.fail(/* @__PURE__ */ new Error("gateway returned no result for prompt"));
|
|
11817
11818
|
return fromGatewayResult(result);
|
|
11818
|
-
}).pipe(Effect.
|
|
11819
|
+
}).pipe(Effect.catch((error) => {
|
|
11819
11820
|
if (error instanceof AgentRunCancelled) return Effect.succeed({
|
|
11820
11821
|
cancelled: true,
|
|
11821
11822
|
messageId: payload.event.message.message_id,
|
|
@@ -11975,7 +11976,7 @@ function createDurableQueue(options) {
|
|
|
11975
11976
|
const maxAttempts = positiveInteger(options.maxAttempts ?? 5, "maximum attempts");
|
|
11976
11977
|
const maxPending = positiveInteger(options.maxPending ?? 100, "capacity");
|
|
11977
11978
|
const retryDelayMs = nonNegativeInteger(options.retryDelayMs ?? 0, "retry delay");
|
|
11978
|
-
const executePreparedAction = (prepared, deliveryId, leaseId) => prepared.execute().pipe(Effect.
|
|
11979
|
+
const executePreparedAction = (prepared, deliveryId, leaseId) => prepared.execute().pipe(Effect.catch((error) => options.clock.now().pipe(Effect.flatMap((failedAt) => options.store.fail({
|
|
11979
11980
|
failedAt,
|
|
11980
11981
|
id: deliveryId,
|
|
11981
11982
|
leaseId,
|
|
@@ -12068,7 +12069,7 @@ function createDurableQueue(options) {
|
|
|
12068
12069
|
const actionResult = yield* executePreparedAction(preparedAction, delivery.id, claimedLeaseId);
|
|
12069
12070
|
if (actionResult !== void 0) return yield* completeHandled(actionResult);
|
|
12070
12071
|
}
|
|
12071
|
-
const result = yield* options.handleDelivery(delivery.payload, delivery.metadata).pipe(Effect.
|
|
12072
|
+
const result = yield* options.handleDelivery(delivery.payload, delivery.metadata).pipe(Effect.catch((error) => options.clock.now().pipe(Effect.flatMap((failedAt) => options.store.fail({
|
|
12072
12073
|
availableAt: new Date(Date.parse(failedAt) + retryDelayMs).toISOString(),
|
|
12073
12074
|
failedAt,
|
|
12074
12075
|
id: delivery.id,
|
|
@@ -12171,10 +12172,10 @@ function createDurableQueueWorker(options) {
|
|
|
12171
12172
|
const results = [];
|
|
12172
12173
|
while (options.queue.pending() > 0) {
|
|
12173
12174
|
const batchSize = Math.min(concurrency, options.queue.pending());
|
|
12174
|
-
const outcomes = yield* Effect.all(Array.from({ length: batchSize }, () => options.queue.drainOne().pipe(Effect.
|
|
12175
|
-
const failures = outcomes.filter(
|
|
12175
|
+
const outcomes = yield* Effect.all(Array.from({ length: batchSize }, () => options.queue.drainOne().pipe(Effect.result)), { concurrency: "unbounded" });
|
|
12176
|
+
const failures = outcomes.filter(Result.isFailure).map((outcome) => outcome.failure);
|
|
12176
12177
|
if (failures.length > 0) return yield* Effect.fail(failures.length === 1 ? failures[0] : new AggregateError(failures, "Inbound Delivery batch failed"));
|
|
12177
|
-
const batch = outcomes.filter(
|
|
12178
|
+
const batch = outcomes.filter(Result.isSuccess).map((outcome) => outcome.success);
|
|
12178
12179
|
if (batch.every((result) => !result.handled)) break;
|
|
12179
12180
|
for (const result of batch) if (result.handled) results.push(result);
|
|
12180
12181
|
}
|
|
@@ -12187,7 +12188,7 @@ function createDurableQueueWorker(options) {
|
|
|
12187
12188
|
function createDurableQueueWorkerLoop(options) {
|
|
12188
12189
|
if (!Number.isSafeInteger(options.intervalMs) || options.intervalMs < 0) throw new Error("Inbound Delivery worker interval must be a non-negative integer");
|
|
12189
12190
|
const report = (error) => options.onError?.(error) ?? Effect.void;
|
|
12190
|
-
const continueAfterReporting = (effect) => effect.pipe(Effect.asVoid, Effect.
|
|
12191
|
+
const continueAfterReporting = (effect) => effect.pipe(Effect.asVoid, Effect.catch(report));
|
|
12191
12192
|
return Effect.forever(Effect.suspend(() => continueAfterReporting(options.drainAvailable()).pipe(Effect.flatMap(() => continueAfterReporting(options.sleep(options.intervalMs))))));
|
|
12192
12193
|
}
|
|
12193
12194
|
//#endregion
|
|
@@ -12321,7 +12322,7 @@ function createInMemoryDurableQueueStore(options = {}) {
|
|
|
12321
12322
|
if (deliveries.has(restored.id)) throw new Error(`duplicate Inbound Delivery snapshot: ${restored.id}`);
|
|
12322
12323
|
deliveries.set(restored.id, restored);
|
|
12323
12324
|
}
|
|
12324
|
-
const serial =
|
|
12325
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
12325
12326
|
let lastLaneKey;
|
|
12326
12327
|
const save = (current, delivery) => {
|
|
12327
12328
|
const snapshot = readInboundDeliverySnapshot(current === void 0 ? delivery : resolveUpdate(current, delivery));
|
|
@@ -12473,7 +12474,7 @@ function createFeishuEventHandlers(options) {
|
|
|
12473
12474
|
} };
|
|
12474
12475
|
if (options.cardActions) return {
|
|
12475
12476
|
...handlers,
|
|
12476
|
-
"card.action.trigger": (payload) => Effect.runPromise(options.cardActions.handleCardAction(payload).pipe(Effect.map(createFeishuCardActionCallbackResponse), Effect.
|
|
12477
|
+
"card.action.trigger": (payload) => Effect.runPromise(options.cardActions.handleCardAction(payload).pipe(Effect.map(createFeishuCardActionCallbackResponse), Effect.catch(() => Effect.succeed(createFeishuCardActionErrorResponse()))))
|
|
12477
12478
|
};
|
|
12478
12479
|
return handlers;
|
|
12479
12480
|
}
|
|
@@ -12530,7 +12531,7 @@ function toStandardRunResult(result) {
|
|
|
12530
12531
|
function createFeishuAgentExecutionBridge(options) {
|
|
12531
12532
|
const activeNamespacedSessions = /* @__PURE__ */ new Set();
|
|
12532
12533
|
const cancellationRequests = /* @__PURE__ */ new Map();
|
|
12533
|
-
const cancellationSerial =
|
|
12534
|
+
const cancellationSerial = Semaphore.makeUnsafe(1);
|
|
12534
12535
|
const activeHandles = /* @__PURE__ */ new Set();
|
|
12535
12536
|
const activeRuns = /* @__PURE__ */ new Map();
|
|
12536
12537
|
let stopping = false;
|
|
@@ -12543,7 +12544,7 @@ function createFeishuAgentExecutionBridge(options) {
|
|
|
12543
12544
|
if (current) return current;
|
|
12544
12545
|
const completion = yield* Deferred.make();
|
|
12545
12546
|
cancellationRequests.set(runId, completion);
|
|
12546
|
-
yield* Effect.
|
|
12547
|
+
yield* Effect.forkDetach(Effect.suspend(() => options.cancel({
|
|
12547
12548
|
reason: "endpoint_stopping",
|
|
12548
12549
|
runId,
|
|
12549
12550
|
sessionKey
|
|
@@ -12637,17 +12638,17 @@ function createFeishuAgentExecutionBridge(options) {
|
|
|
12637
12638
|
if (activeRunId) activeRuns.delete(activeRunId);
|
|
12638
12639
|
activeHandles.delete(completion);
|
|
12639
12640
|
activeNamespacedSessions.delete(sessionKey);
|
|
12640
|
-
}).pipe(Effect.
|
|
12641
|
-
yield* Effect.
|
|
12641
|
+
}).pipe(Effect.andThen(Deferred.succeed(completion, void 0)))));
|
|
12642
|
+
yield* Effect.forkDetach(restore(run).pipe(Effect.exit, Effect.flatMap((exit) => Deferred.done(outcome, exit))));
|
|
12642
12643
|
return yield* restore(Deferred.await(outcome));
|
|
12643
12644
|
}))
|
|
12644
12645
|
}
|
|
12645
12646
|
};
|
|
12646
12647
|
}
|
|
12647
12648
|
function waitUntilDeadline(effect, deadline, message) {
|
|
12648
|
-
return effect.pipe(Effect.interruptible, Effect.
|
|
12649
|
+
return effect.pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
12649
12650
|
duration: Math.max(0, deadline - Date.now()),
|
|
12650
|
-
|
|
12651
|
+
orElse: () => Effect.fail(new Error(message))
|
|
12651
12652
|
}));
|
|
12652
12653
|
}
|
|
12653
12654
|
function namespaceSessionKey(namespace, sessionKey) {
|
|
@@ -12793,7 +12794,7 @@ function createFeishuDeploymentEndpoint(options) {
|
|
|
12793
12794
|
let stopping = false;
|
|
12794
12795
|
const activeDrains = /* @__PURE__ */ new Set();
|
|
12795
12796
|
const awaitDrain = (drain) => Fiber.join(drain).pipe(Effect.onExit((exit) => Effect.sync(() => {
|
|
12796
|
-
if (exit._tag === "Success" || !Cause.
|
|
12797
|
+
if (exit._tag === "Success" || !Cause.hasInterruptsOnly(exit.cause)) activeDrains.delete(drain);
|
|
12797
12798
|
})));
|
|
12798
12799
|
const stopCardRollover = options.cardRollover ? Effect.tryPromise({
|
|
12799
12800
|
try: () => Promise.resolve(options.cardRollover.stop()),
|
|
@@ -12811,7 +12812,7 @@ function createFeishuDeploymentEndpoint(options) {
|
|
|
12811
12812
|
});
|
|
12812
12813
|
const workerLoop = createEffectLoopDriver(createDurableQueueWorkerLoop({
|
|
12813
12814
|
drainAvailable: () => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
12814
|
-
const drain = yield* Effect.
|
|
12815
|
+
const drain = yield* Effect.forkDetach(worker.drainAvailable().pipe(Effect.interruptible));
|
|
12815
12816
|
activeDrains.add(drain);
|
|
12816
12817
|
return yield* restore(awaitDrain(drain));
|
|
12817
12818
|
})),
|
|
@@ -12825,7 +12826,7 @@ function createFeishuDeploymentEndpoint(options) {
|
|
|
12825
12826
|
handlers: createFeishuEventHandlers({
|
|
12826
12827
|
cardActions: daemon,
|
|
12827
12828
|
queue: {
|
|
12828
|
-
accept: (payload) => shouldAcceptFeishuEndpointMessage(payload, options.groupPolicy, options.botOpenId) ? inbound.accept(payload).pipe(Effect.tap((result) => result.accepted || result.reason !== "capacity" || !options.onCapacityExceeded ? Effect.void : options.onCapacityExceeded(payload).pipe(Effect.
|
|
12829
|
+
accept: (payload) => shouldAcceptFeishuEndpointMessage(payload, options.groupPolicy, options.botOpenId) ? inbound.accept(payload).pipe(Effect.tap((result) => result.accepted || result.reason !== "capacity" || !options.onCapacityExceeded ? Effect.void : options.onCapacityExceeded(payload).pipe(Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.interrupt : reportWorkerError(Cause.squash(cause)))))) : Effect.succeed({
|
|
12829
12830
|
accepted: false,
|
|
12830
12831
|
messageId: payload.event.message.message_id,
|
|
12831
12832
|
pending: queue.pending(),
|
|
@@ -12853,7 +12854,7 @@ function createFeishuDeploymentEndpoint(options) {
|
|
|
12853
12854
|
catch: (error) => error
|
|
12854
12855
|
});
|
|
12855
12856
|
workerLoop.start();
|
|
12856
|
-
}).pipe(Effect.
|
|
12857
|
+
}).pipe(Effect.catchCause((startCause) => Effect.uninterruptible(Effect.gen(function* () {
|
|
12857
12858
|
const cleanupErrors = yield* settleCleanup([
|
|
12858
12859
|
Effect.tryPromise({
|
|
12859
12860
|
try: () => websocket.stop(),
|
|
@@ -12873,11 +12874,11 @@ function createFeishuDeploymentEndpoint(options) {
|
|
|
12873
12874
|
const timeoutMs = options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS;
|
|
12874
12875
|
const deadline = Date.now() + timeoutMs;
|
|
12875
12876
|
let executionStopped = false;
|
|
12876
|
-
const websocketStop = yield* Effect.
|
|
12877
|
+
const websocketStop = yield* Effect.forkChild(Effect.tryPromise({
|
|
12877
12878
|
try: () => websocket.stop(),
|
|
12878
12879
|
catch: (error) => error
|
|
12879
12880
|
}));
|
|
12880
|
-
const workerStop = yield* Effect.
|
|
12881
|
+
const workerStop = yield* Effect.forkChild(Effect.tryPromise({
|
|
12881
12882
|
try: () => workerLoop.stop(),
|
|
12882
12883
|
catch: (error) => error
|
|
12883
12884
|
}));
|
|
@@ -12887,9 +12888,9 @@ function createFeishuDeploymentEndpoint(options) {
|
|
|
12887
12888
|
executionStopped = true;
|
|
12888
12889
|
}))),
|
|
12889
12890
|
Fiber.join(workerStop),
|
|
12890
|
-
Effect.suspend(() => executionStopped ? Effect.all([...activeDrains].map(awaitDrain), { concurrency: "unbounded" }).pipe(Effect.interruptible, Effect.
|
|
12891
|
+
Effect.suspend(() => executionStopped ? Effect.all([...activeDrains].map(awaitDrain), { concurrency: "unbounded" }).pipe(Effect.interruptible, Effect.timeoutOrElse({
|
|
12891
12892
|
duration: Math.max(0, deadline - Date.now()),
|
|
12892
|
-
|
|
12893
|
+
orElse: () => Effect.fail(/* @__PURE__ */ new Error(`Feishu Inbox drain did not stop within ${timeoutMs}ms`))
|
|
12893
12894
|
})) : Effect.void),
|
|
12894
12895
|
stopCardRollover
|
|
12895
12896
|
]);
|
|
@@ -12909,13 +12910,989 @@ 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* () {
|
|
12915
|
-
const cotPrepared = options.cotPublisher ? yield* options.cotPublisher.prepare(run).pipe(Effect.as(true), Effect.
|
|
13892
|
+
const cotPrepared = options.cotPublisher ? yield* options.cotPublisher.prepare(run).pipe(Effect.as(true), Effect.catch((error) => options.reportCotError("create", error).pipe(Effect.as(false)))) : false;
|
|
12916
13893
|
yield* options.prepareCardTarget(run).pipe(Effect.onExit((exit) => {
|
|
12917
13894
|
if (Exit.isSuccess(exit) || !options.cotPublisher || !cotPrepared) return Effect.void;
|
|
12918
|
-
return options.cotPublisher.fail(run.runId).pipe(Effect.
|
|
13895
|
+
return options.cotPublisher.fail(run.runId).pipe(Effect.catchCause((cause) => options.reportCotError("abort", Cause.squash(cause))), Effect.catchCause(() => Effect.void));
|
|
12919
13896
|
}));
|
|
12920
13897
|
});
|
|
12921
13898
|
}
|
|
@@ -13233,7 +14210,7 @@ function replaceChain(chains, replacement) {
|
|
|
13233
14210
|
//#region src/adapters/outbound/persistence/card-presentation/in-memory-card-presentation-repository.ts
|
|
13234
14211
|
function createInMemoryCardPresentationRepository(options = {}) {
|
|
13235
14212
|
let chains = cloneAndValidate(options.initial ?? []);
|
|
13236
|
-
const semaphore =
|
|
14213
|
+
const semaphore = Semaphore.makeUnsafe(1);
|
|
13237
14214
|
return {
|
|
13238
14215
|
snapshot: () => structuredClone(chains),
|
|
13239
14216
|
transact: (decide) => semaphore.withPermits(1)(Effect.uninterruptible(Effect.gen(function* () {
|
|
@@ -13375,7 +14352,7 @@ var FeishuCardTargetRegistryStoreError = class {
|
|
|
13375
14352
|
};
|
|
13376
14353
|
function createJsonFileFeishuCardTargetRegistry(options) {
|
|
13377
14354
|
let loaded;
|
|
13378
|
-
const semaphore =
|
|
14355
|
+
const semaphore = Semaphore.makeUnsafe(1);
|
|
13379
14356
|
const ready = () => {
|
|
13380
14357
|
if (loaded) return Effect.succeed(loaded);
|
|
13381
14358
|
return semaphore.withPermits(1)(loaded ? Effect.succeed(loaded) : openJsonCardPresentationRepository(options).pipe(Effect.map(createCardPresentationService), Effect.mapError(toRegistryStoreError), Effect.tap((service) => Effect.sync(() => {
|
|
@@ -14254,7 +15231,7 @@ function createEffectSessionScheduler(options) {
|
|
|
14254
15231
|
const readySet = /* @__PURE__ */ new Set();
|
|
14255
15232
|
const activeSessions = /* @__PURE__ */ new Set();
|
|
14256
15233
|
const inFlight = /* @__PURE__ */ new Map();
|
|
14257
|
-
const serial =
|
|
15234
|
+
const serial = Semaphore.makeUnsafe(1);
|
|
14258
15235
|
let queuedRuns = 0;
|
|
14259
15236
|
let disposed = false;
|
|
14260
15237
|
const markReady = (sessionKey) => {
|
|
@@ -14275,7 +15252,7 @@ function createEffectSessionScheduler(options) {
|
|
|
14275
15252
|
readySet.delete(sessionKey);
|
|
14276
15253
|
const readyIndex = readySessions.indexOf(sessionKey);
|
|
14277
15254
|
if (readyIndex >= 0) readySessions.splice(readyIndex, 1);
|
|
14278
|
-
})).pipe(Effect.
|
|
15255
|
+
})).pipe(Effect.andThen(Deferred.succeed(scheduled.interruption, void 0)), Effect.asVoid);
|
|
14279
15256
|
const pump = () => Effect.gen(function* () {
|
|
14280
15257
|
const selected = yield* serial.withPermits(1)(Effect.gen(function* () {
|
|
14281
15258
|
const runs = [];
|
|
@@ -14300,11 +15277,11 @@ function createEffectSessionScheduler(options) {
|
|
|
14300
15277
|
}
|
|
14301
15278
|
return runs;
|
|
14302
15279
|
}));
|
|
14303
|
-
yield* Effect.forEach(selected, ({ cacheEntry, completion, scheduled }) => Effect.
|
|
15280
|
+
yield* Effect.forEach(selected, ({ cacheEntry, completion, scheduled }) => Effect.forkDetach(Effect.raceFirst(runtimes.start(cacheEntry, () => options.createRuntime(scheduled.input.sessionKey)).pipe(Effect.andThen(Deferred.await(cacheEntry.deferred)), Effect.flatMap((runtime) => serial.withPermits(1)(Effect.sync(() => !disposed && inFlight.get(scheduled.input.sessionKey)?.scheduled === scheduled)).pipe(Effect.flatMap((mayRun) => mayRun ? runtime.run(scheduled.input) : Effect.fail(new SessionRuntimeSchedulerDisposed("session scheduler was disposed")))))), Deferred.await(scheduled.interruption).pipe(Effect.andThen(Effect.interrupt))).pipe(Effect.interruptible, Effect.exit, Effect.tap((exit) => Deferred.done(scheduled.deferred, exit)), Effect.ensuring(serial.withPermits(1)(Effect.sync(() => {
|
|
14304
15281
|
activeSessions.delete(scheduled.input.sessionKey);
|
|
14305
15282
|
if (inFlight.get(scheduled.input.sessionKey)?.scheduled === scheduled) inFlight.delete(scheduled.input.sessionKey);
|
|
14306
15283
|
if ((queues.get(scheduled.input.sessionKey)?.length ?? 0) > 0) markReady(scheduled.input.sessionKey);
|
|
14307
|
-
})).pipe(Effect.
|
|
15284
|
+
})).pipe(Effect.andThen(Deferred.succeed(completion, void 0)), Effect.asVoid)), Effect.flatMap(() => pump())), { uninterruptible: "inherit" }), { discard: true });
|
|
14308
15285
|
});
|
|
14309
15286
|
const existingRuntime = (sessionKey) => Effect.gen(function* () {
|
|
14310
15287
|
const selected = yield* runtimes.getExisting(sessionKey);
|
|
@@ -14757,19 +15734,19 @@ function createAutomationScheduler(options) {
|
|
|
14757
15734
|
const mandate = mandates.activate(options.binding);
|
|
14758
15735
|
const clock = options.clock ?? systemClock;
|
|
14759
15736
|
const schedule = createDailyAutomationSchedule(options.binding.schedule, options.binding.timeZone, clock.now());
|
|
14760
|
-
const execution =
|
|
14761
|
-
const lifecycle =
|
|
15737
|
+
const execution = Semaphore.makeUnsafe(1);
|
|
15738
|
+
const lifecycle = Semaphore.makeUnsafe(1);
|
|
14762
15739
|
let loopFiber;
|
|
14763
15740
|
let running = false;
|
|
14764
15741
|
const runDue = (at) => execution.withPermits(1)(executeDue(options, schedule, at, (occurrence) => mandates.claim(mandate.id, occurrence, options.binding.servicePrincipalId)));
|
|
14765
15742
|
const start = () => lifecycle.withPermits(1)(Effect.suspend(() => {
|
|
14766
15743
|
if (running) return Effect.void;
|
|
14767
15744
|
running = true;
|
|
14768
|
-
const loop = runScheduleLoop(options, schedule, clock, runDue).pipe(Effect.
|
|
15745
|
+
const loop = runScheduleLoop(options, schedule, clock, runDue).pipe(Effect.catch((error) => notifyError(options.onError, error)), Effect.ensuring(Effect.sync(() => {
|
|
14769
15746
|
running = false;
|
|
14770
15747
|
loopFiber = void 0;
|
|
14771
15748
|
})));
|
|
14772
|
-
return Effect.
|
|
15749
|
+
return Effect.forkDetach(loop, { uninterruptible: "inherit" }).pipe(Effect.tap((fiber) => Effect.sync(() => {
|
|
14773
15750
|
if (running) loopFiber = fiber;
|
|
14774
15751
|
})), Effect.asVoid);
|
|
14775
15752
|
}));
|
|
@@ -14830,7 +15807,7 @@ function executeDue(options, schedule, at, claim) {
|
|
|
14830
15807
|
body: result.body,
|
|
14831
15808
|
...result.presentation === void 0 ? {} : { presentation: result.presentation },
|
|
14832
15809
|
runId: result.runId
|
|
14833
|
-
}))))), Effect.
|
|
15810
|
+
}))))), Effect.catch((error) => options.repository.save(running.fail(automationFailureMessage(error))).pipe(Effect.andThen(Effect.fail(error)))));
|
|
14834
15811
|
}
|
|
14835
15812
|
if (record.status === "suppressed") return record;
|
|
14836
15813
|
const snapshot = record.toSnapshot();
|
|
@@ -14864,7 +15841,7 @@ function automationFailureMessage(error) {
|
|
|
14864
15841
|
return message.trim() === "" ? "Unknown Automation run failure" : message;
|
|
14865
15842
|
}
|
|
14866
15843
|
function notifyError(onError, error) {
|
|
14867
|
-
return (onError?.(error) ?? Effect.void).pipe(Effect.
|
|
15844
|
+
return (onError?.(error) ?? Effect.void).pipe(Effect.catch(() => Effect.void));
|
|
14868
15845
|
}
|
|
14869
15846
|
const systemClock = {
|
|
14870
15847
|
now: () => /* @__PURE__ */ new Date(),
|
|
@@ -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({
|
|
@@ -15752,7 +16623,7 @@ function listChildDirectories(parent) {
|
|
|
15752
16623
|
return Effect.tryPromise({
|
|
15753
16624
|
try: () => readdir(parent, { withFileTypes: true }),
|
|
15754
16625
|
catch: (error) => error
|
|
15755
|
-
}).pipe(Effect.map((entries) => entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right))), Effect.
|
|
16626
|
+
}).pipe(Effect.map((entries) => entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right))), Effect.catch((error) => error instanceof Error && "code" in error && error.code === "ENOENT" ? Effect.succeed([]) : Effect.fail(error)));
|
|
15756
16627
|
}
|
|
15757
16628
|
//#endregion
|
|
15758
16629
|
//#region src/bootstrap/pi/pi-session-options.ts
|
|
@@ -15852,7 +16723,7 @@ function createBackgroundSessionDeliveryQueue(options) {
|
|
|
15852
16723
|
return {
|
|
15853
16724
|
drain: () => Effect.gen(function* () {
|
|
15854
16725
|
const records = yield* fromPromise$1(() => options.deliveries.pending());
|
|
15855
|
-
yield* Effect.forEach(records, (record) => fromPromise$1(() => options.deliver(record)).pipe(Effect.flatMap(({ providerMessageId }) => fromPromise$1(() => options.deliveries.markDelivered(record.deliveryId, providerMessageId))), Effect.tap((delivered) => delivered ? Effect.sync(options.onDelivered) : Effect.void), Effect.
|
|
16726
|
+
yield* Effect.forEach(records, (record) => fromPromise$1(() => options.deliver(record)).pipe(Effect.flatMap(({ providerMessageId }) => fromPromise$1(() => options.deliveries.markDelivered(record.deliveryId, providerMessageId))), Effect.tap((delivered) => delivered ? Effect.sync(options.onDelivered) : Effect.void), Effect.catch((error) => options.onError === void 0 ? Effect.void : Effect.sync(() => options.onError?.(error))), Effect.asVoid), { discard: true });
|
|
15856
16727
|
}),
|
|
15857
16728
|
enqueue: (sessionId, kind, text, session) => {
|
|
15858
16729
|
if (text.trim() === "") return Effect.void;
|
|
@@ -15934,7 +16805,7 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
15934
16805
|
const owner = `supervisor:${options.createId()}`;
|
|
15935
16806
|
const inFlight = /* @__PURE__ */ new Map();
|
|
15936
16807
|
const stepControllers = /* @__PURE__ */ new Map();
|
|
15937
|
-
const supervision =
|
|
16808
|
+
const supervision = Semaphore.makeUnsafe(1);
|
|
15938
16809
|
const counters = {
|
|
15939
16810
|
completed: 0,
|
|
15940
16811
|
deliveriesDelivered: 0,
|
|
@@ -15956,7 +16827,7 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
15956
16827
|
},
|
|
15957
16828
|
...options.onError === void 0 ? {} : { onError: options.onError }
|
|
15958
16829
|
});
|
|
15959
|
-
const reportError = (error) => options.onError === void 0 ? Effect.void : Effect.sync(() => options.onError?.(error)).pipe(Effect.
|
|
16830
|
+
const reportError = (error) => options.onError === void 0 ? Effect.void : Effect.sync(() => options.onError?.(error)).pipe(Effect.catchCause(() => Effect.void));
|
|
15960
16831
|
const repositoryGet = (sessionId) => fromPromise(() => options.repository.get(sessionId));
|
|
15961
16832
|
const repositoryList = () => fromPromise(() => options.repository.list());
|
|
15962
16833
|
const persistTransition = (input) => Effect.uninterruptible(fromPromise(() => commitBackgroundSessionTransition({
|
|
@@ -16084,7 +16955,7 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
16084
16955
|
wakeText: buildInterruptedBackgroundSessionWakeText(state)
|
|
16085
16956
|
})
|
|
16086
16957
|
});
|
|
16087
|
-
}).pipe(Effect.
|
|
16958
|
+
}).pipe(Effect.catch((error) => reportError(error)), Effect.asVoid);
|
|
16088
16959
|
const renewLeaseOnce = (sessionId, lease, controller) => Effect.gen(function* () {
|
|
16089
16960
|
const current = yield* repositoryGet(sessionId);
|
|
16090
16961
|
if (!current || current.lease === void 0 || current.lease.owner !== owner || current.lease.epoch !== lease.epoch) {
|
|
@@ -16111,8 +16982,8 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
16111
16982
|
return true;
|
|
16112
16983
|
});
|
|
16113
16984
|
const renewLease = (sessionId, lease, controller) => {
|
|
16114
|
-
const cycle = Effect.suspend(() => options.sleep(options.config.leaseRenewalIntervalMs).pipe(Effect.flatMap(() => renewLeaseOnce(sessionId, lease, controller)), Effect.flatMap((renewed) => renewed ? cycle : Effect.void), Effect.
|
|
16115
|
-
if (Cause.
|
|
16985
|
+
const cycle = Effect.suspend(() => options.sleep(options.config.leaseRenewalIntervalMs).pipe(Effect.flatMap(() => renewLeaseOnce(sessionId, lease, controller)), Effect.flatMap((renewed) => renewed ? cycle : Effect.void), Effect.catchCause((cause) => {
|
|
16986
|
+
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
|
|
16116
16987
|
controller.abort(/* @__PURE__ */ new Error(`background session lease renewal failed: ${backgroundSessionErrorMessage(Cause.squash(cause))}`));
|
|
16117
16988
|
return Effect.void;
|
|
16118
16989
|
})));
|
|
@@ -16134,7 +17005,7 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
16134
17005
|
wakeText: `步骤结果提交失败(${backgroundSessionErrorMessage(error)}),将重试`
|
|
16135
17006
|
})
|
|
16136
17007
|
});
|
|
16137
|
-
}).pipe(Effect.
|
|
17008
|
+
}).pipe(Effect.catch((commitError) => reportError(commitError)), Effect.asVoid);
|
|
16138
17009
|
const commitStep = (claimed, outcome, lease) => Effect.gen(function* () {
|
|
16139
17010
|
const now = options.clock.now();
|
|
16140
17011
|
const current = yield* repositoryGet(claimed.sessionId);
|
|
@@ -16239,7 +17110,7 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
16239
17110
|
const controller = new AbortController();
|
|
16240
17111
|
stepControllers.set(session.sessionId, controller);
|
|
16241
17112
|
yield* Effect.gen(function* () {
|
|
16242
|
-
const outcome = yield* Effect.acquireUseRelease(Effect.
|
|
17113
|
+
const outcome = yield* Effect.acquireUseRelease(Effect.forkChild(renewLease(session.sessionId, lease, controller), { uninterruptible: false }), () => options.runStep({
|
|
16243
17114
|
session: claimed,
|
|
16244
17115
|
signal: controller.signal,
|
|
16245
17116
|
wakeText
|
|
@@ -16253,16 +17124,16 @@ function createBackgroundSessionSupervisor(options) {
|
|
|
16253
17124
|
result
|
|
16254
17125
|
})
|
|
16255
17126
|
})), (fiber) => Fiber.interrupt(fiber).pipe(Effect.asVoid));
|
|
16256
|
-
yield* Effect.uninterruptible(commitStep(claimed, outcome, lease).pipe(Effect.
|
|
17127
|
+
yield* Effect.uninterruptible(commitStep(claimed, outcome, lease).pipe(Effect.catch((error) => reportError(error).pipe(Effect.flatMap(() => requeueAfterFailedCommit(session.sessionId, lease, error))))));
|
|
16257
17128
|
}).pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort(/* @__PURE__ */ new Error("background session supervisor is stopping"))).pipe(Effect.flatMap(() => requeueAfterInterruptedFiber(session.sessionId, lease)))), Effect.ensuring(Effect.sync(() => {
|
|
16258
17129
|
stepControllers.delete(session.sessionId);
|
|
16259
17130
|
})));
|
|
16260
17131
|
});
|
|
16261
17132
|
const startStep = (session) => Effect.gen(function* () {
|
|
16262
17133
|
const gate = yield* Deferred.make();
|
|
16263
|
-
const fiber = yield* Effect.
|
|
17134
|
+
const fiber = yield* Effect.forkDetach(Deferred.await(gate).pipe(Effect.flatMap(() => runStep(session)), Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.void : reportError(Cause.squash(cause))), Effect.ensuring(Effect.sync(() => {
|
|
16264
17135
|
inFlight.delete(session.sessionId);
|
|
16265
|
-
}))));
|
|
17136
|
+
}))), { uninterruptible: "inherit" });
|
|
16266
17137
|
inFlight.set(session.sessionId, fiber);
|
|
16267
17138
|
yield* Deferred.succeed(gate, void 0);
|
|
16268
17139
|
}).pipe(Effect.uninterruptible);
|
|
@@ -16338,7 +17209,7 @@ function createHumanInteractionService(options) {
|
|
|
16338
17209
|
}) : (ms) => Effect.sleep(ms);
|
|
16339
17210
|
const persistAndPresent = (interaction, existing) => Effect.gen(function* () {
|
|
16340
17211
|
if (Date.parse(options.clock.now()) >= Date.parse(interaction.expiresAt)) return yield* Effect.fail(/* @__PURE__ */ new Error(`human interaction expired before presentation: ${interaction.id}`));
|
|
16341
|
-
const persisted = existing ? existing : yield* options.repository.create(interaction).pipe(Effect.as(interaction), Effect.
|
|
17212
|
+
const persisted = existing ? existing : yield* options.repository.create(interaction).pipe(Effect.as(interaction), Effect.catch((createError) => options.repository.get(interaction.id).pipe(Effect.flatMap((concurrent) => concurrent ? Effect.succeed(concurrent) : Effect.fail(createError)))));
|
|
16342
17213
|
if (!persisted.isSameRequest(interaction)) return yield* Effect.fail(/* @__PURE__ */ new Error(`human interaction id is already bound to another request: ${interaction.id}`));
|
|
16343
17214
|
yield* options.presenter.present(persisted);
|
|
16344
17215
|
return persisted;
|
|
@@ -16367,7 +17238,7 @@ function createHumanInteractionService(options) {
|
|
|
16367
17238
|
return yield* options.repository.update(input.approvalId, (interaction) => interaction.consume({
|
|
16368
17239
|
binding: input.binding,
|
|
16369
17240
|
now: options.clock.now()
|
|
16370
|
-
}).state).pipe(Effect.as(true), Effect.
|
|
17241
|
+
}).state).pipe(Effect.as(true), Effect.catch((error) => isTransitionDenied(error) ? Effect.succeed(false) : Effect.fail(error)));
|
|
16371
17242
|
}),
|
|
16372
17243
|
get: (interactionId) => options.repository.get(interactionId),
|
|
16373
17244
|
resolve: (input) => options.repository.update(input.interactionId, (interaction) => interaction.resolve({
|
|
@@ -16401,7 +17272,7 @@ function isTransitionDenied(error) {
|
|
|
16401
17272
|
return error instanceof Error && error.cause instanceof Error && error.cause.name === "HumanInteractionTransitionDenied";
|
|
16402
17273
|
}
|
|
16403
17274
|
function waitForAbort(signal) {
|
|
16404
|
-
return Effect.
|
|
17275
|
+
return Effect.callback((resume) => {
|
|
16405
17276
|
const onAbort = () => resume(Effect.fail(signal.reason ?? /* @__PURE__ */ new Error("human interaction wait was aborted")));
|
|
16406
17277
|
if (signal.aborted) {
|
|
16407
17278
|
onAbort();
|
|
@@ -16962,10 +17833,10 @@ async function createRivusDaemonProcess(context) {
|
|
|
16962
17833
|
running: () => bootstrap.process.running(),
|
|
16963
17834
|
start: () => bootstrap.process.start(),
|
|
16964
17835
|
status: () => bootstrap.status(),
|
|
16965
|
-
stop: () => bootstrap.process.stop().pipe(Effect.
|
|
17836
|
+
stop: () => bootstrap.process.stop().pipe(Effect.andThen(Effect.tryPromise({
|
|
16966
17837
|
try: () => sessionRegistry.disposeAll(),
|
|
16967
17838
|
catch: (error) => error
|
|
16968
|
-
})), Effect.
|
|
17839
|
+
})), Effect.andThen(Effect.tryPromise({
|
|
16969
17840
|
try: () => assembly.dispose(),
|
|
16970
17841
|
catch: (error) => error
|
|
16971
17842
|
})))
|
|
@@ -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,
|
|
@@ -17354,15 +18235,16 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17354
18235
|
reply: (messageId, text) => replies.reply(messageId, text),
|
|
17355
18236
|
sessionStore,
|
|
17356
18237
|
...input.steer ? { steer: input.steer } : {},
|
|
17357
|
-
...cotPublisher ? { publishRunUpdate: (update) => cotPublisher.publish(update).pipe(Effect.
|
|
18238
|
+
...cotPublisher ? { publishRunUpdate: (update) => cotPublisher.publish(update).pipe(Effect.catch((error) => reportCotError(input.endpointId, "update", error))) } : {},
|
|
17358
18239
|
sessionNamespace: input.definition.sessionNamespace,
|
|
17359
18240
|
sleep,
|
|
17360
18241
|
websocketClient: createLazyFeishuWebSocketClient(credentials, input.definition.baseUrl),
|
|
17361
18242
|
workerConcurrency: 4
|
|
17362
18243
|
});
|
|
17363
18244
|
interactionRegistry.register(input.endpointId, interactions);
|
|
17364
|
-
return endpoint;
|
|
17365
|
-
|
|
18245
|
+
if (embedded === void 0) return endpoint;
|
|
18246
|
+
return wrapEmbeddedHerdrEndpointLifecycle(endpoint, embedded.runtime);
|
|
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);
|
|
17368
18250
|
yield* Effect.tryPromise({
|
|
@@ -17559,7 +18441,7 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17559
18441
|
concurrency: scheduler.concurrency,
|
|
17560
18442
|
run: (request) => scheduler.run(request),
|
|
17561
18443
|
steer: (request) => scheduler.steer(request),
|
|
17562
|
-
dispose: () => scheduler.dispose().pipe(Effect.
|
|
18444
|
+
dispose: () => scheduler.dispose().pipe(Effect.andThen(Effect.tryPromise({
|
|
17563
18445
|
try: () => sessionRegistry.disposeAll(),
|
|
17564
18446
|
catch: (error) => error
|
|
17565
18447
|
})), Effect.tap(() => Effect.sync(() => {
|
|
@@ -17571,7 +18453,7 @@ async function createRivusDeploymentAdapters(context) {
|
|
|
17571
18453
|
}
|
|
17572
18454
|
})))
|
|
17573
18455
|
};
|
|
17574
|
-
}).pipe(Effect.
|
|
18456
|
+
}).pipe(Effect.catchDefect((error) => Effect.fail(error)))
|
|
17575
18457
|
};
|
|
17576
18458
|
} catch (error) {
|
|
17577
18459
|
await Promise.allSettled([management?.enabled ? management.close() : Promise.resolve(), telemetry?.shutdown()]);
|