import {
  cancelStatusFor,
  interactionFromWireRequest,
  interactionToPersistedPart,
  isSafeInteractionFieldKey,
  parseInteractionAnswers,
  parseInteractionCancel,
  parseInteractionRequest,
  questionInteractionContentSignature
} from "./chunk-3ZK5IJSW.js";
import {
  canTransitionPlanStatus,
  parsePlanSubmittedEvent,
  persistedPartToPlan,
  planFollowUpTurnId,
  planToPersistedPart
} from "./chunk-YJMCRXQQ.js";

// src/durable-chat/errors.ts
var DurableChatError = class extends Error {
  code;
  status;
  details;
  constructor(code, message, status, details) {
    super(message);
    this.name = "DurableChatError";
    this.code = code;
    this.status = status;
    this.details = details;
  }
};
var DurableChatConflictError = class extends DurableChatError {
  constructor(message = "durable chat state conflict", details) {
    super("DURABLE_CHAT_CONFLICT", message, 409, details);
    this.name = "DurableChatConflictError";
  }
};
var DurableChatUnavailableError = class extends DurableChatError {
  constructor(message = "durable chat authority unavailable", details) {
    super("DURABLE_CHAT_UNAVAILABLE", message, 503, details);
    this.name = "DurableChatUnavailableError";
  }
};
var DurableChatGoneError = class extends DurableChatError {
  constructor(message = "durable chat plan is gone", details) {
    super("DURABLE_CHAT_GONE", message, 410, details);
    this.name = "DurableChatGoneError";
  }
};

// src/durable-chat/types.ts
function createDurableChatScope(value) {
  if (typeof value !== "string" || value.trim() === "") throw new TypeError("durable chat scope must be a non-empty string");
  return value;
}
function durableChatScopeKey(scope) {
  if (typeof scope !== "string" || scope.length === 0) throw new TypeError("durable chat scope is required");
  return scope;
}
function normalizePlanDecision(value) {
  if (value === "approved" || value === "approve") return "approved";
  if (value === "rejected" || value === "reject") return "rejected";
  return null;
}
function planCommandKey(planId, revision, decision) {
  return `plan:${encodeURIComponent(planId)}:${revision}:${decision}`;
}
function planAuthorityIdempotencyKey(scope, planId, revision, decision) {
  return `durable-plan:${encodeURIComponent(durableChatScopeKey(scope))}:${encodeURIComponent(planId)}:${revision}:${decision}`;
}
function planEffectKey(scope, planId, revision, decision) {
  return `after-decision:${encodeURIComponent(durableChatScopeKey(scope))}:${encodeURIComponent(planId)}:${revision}:${decision}`;
}
function stablePlanReceipt(scope, planId, revision, decision, result) {
  const authorityIdempotencyKey = planAuthorityIdempotencyKey(scope, planId, revision, decision);
  return {
    receiptId: `receipt:${authorityIdempotencyKey}`,
    planId,
    revision,
    decision,
    turnId: result.followUp.turnId,
    state: result.followUp.state,
    authorityIdempotencyKey
  };
}

// src/durable-chat/memory.ts
var InMemoryDurableChatStateStore = class {
  plans = /* @__PURE__ */ new Map();
  currentPlans = /* @__PURE__ */ new Map();
  commands = /* @__PURE__ */ new Map();
  effects = /* @__PURE__ */ new Map();
  interactions = /* @__PURE__ */ new Map();
  interactionSemantic = /* @__PURE__ */ new Map();
  /** Duplicate event ids point at the canonical semantic interaction so all
   * duplicate asks settle through one terminal row/attempt. */
  interactionAliases = /* @__PURE__ */ new Map();
  answerIntents = /* @__PURE__ */ new Map();
  scopePrefix(scope) {
    return `${encodeURIComponent(durableChatScopeKey(scope))}\0`;
  }
  scopedKey(scope, ...parts) {
    return `${this.scopePrefix(scope)}${parts.map((part) => encodeURIComponent(String(part))).join("\0")}`;
  }
  planKey(scope, planId, revision) {
    return this.scopedKey(scope, planId, revision);
  }
  currentKey(scope, planId) {
    return this.scopedKey(scope, planId);
  }
  commandKey(scope, key) {
    return this.scopedKey(scope, key);
  }
  interactionKey(scope, id) {
    return this.scopedKey(scope, id);
  }
  intentKey(scope, key) {
    return this.scopedKey(scope, key);
  }
  effectKey(scope, key) {
    return this.scopedKey(scope, key);
  }
  async getPlanProjection(scope, planId, revision) {
    const current = revision === void 0 ? this.currentPlans.get(this.currentKey(scope, planId)) : void 0;
    const key = revision === void 0 ? current : this.planKey(scope, planId, revision);
    return key ? this.plans.get(key) ?? null : null;
  }
  async listPlanProjections(scope, planId) {
    const prefix = this.scopePrefix(scope);
    return [...this.plans.entries()].filter(([key, projection]) => key.startsWith(prefix) && (!planId || projection.planId === planId)).map(([, projection]) => projection).sort((a, b) => a.revision - b.revision);
  }
  async putPlanProjection(scope, projection) {
    if (!projection.planId || !Number.isInteger(projection.revision) || projection.revision < 1) {
      throw new TypeError("plan projection requires planId and positive integer revision");
    }
    const key = this.planKey(scope, projection.planId, projection.revision);
    const prior = this.plans.get(key);
    if (prior && JSON.stringify(prior) !== JSON.stringify(projection)) {
      if (prior.body !== projection.body || prior.title !== projection.title || prior.submittedAt !== projection.submittedAt) {
        throw new DurableChatConflictError("plan content changed without a new revision");
      }
      if (prior.status === projection.status && !["preparing", "pending"].includes(prior.status)) {
        throw new DurableChatConflictError("terminal plan projection cannot be rewritten");
      }
      if (!canTransitionPlanStatus(prior.status, projection.status)) {
        throw new DurableChatConflictError("conflicting plan projection for the same revision");
      }
    }
    this.plans.set(key, projection);
    const currentKey = this.currentKey(scope, projection.planId);
    const currentKeyValue = this.currentPlans.get(currentKey);
    const current = currentKeyValue ? this.plans.get(currentKeyValue) : void 0;
    if (!current || projection.revision > current.revision) this.currentPlans.set(currentKey, key);
  }
  async getPlanCommand(scope, commandKey) {
    return this.commands.get(this.commandKey(scope, commandKey)) ?? null;
  }
  async claimPlanCommand(scope, command) {
    const key = this.commandKey(scope, command.commandKey);
    const existing = this.commands.get(key);
    if (!existing) {
      const prefix = this.scopePrefix(scope);
      const competing = [...this.commands.entries()].find(
        ([storedKey, candidate]) => storedKey.startsWith(prefix) && candidate.planId === command.planId && candidate.revision === command.revision && candidate.decision !== command.decision
      );
      if (competing) {
        return { status: "conflict", record: competing[1], reason: "competing decision for plan revision" };
      }
      this.commands.set(key, command);
      return { status: "claimed", record: command };
    }
    if (existing.planId === command.planId && existing.revision === command.revision && existing.decision === command.decision) return { status: "existing", record: existing };
    return { status: "conflict", record: existing, reason: "command key is already used by another decision" };
  }
  async recordPlanAuthorityResult(scope, commandKey, result, receipt) {
    const key = this.commandKey(scope, commandKey);
    const command = this.commands.get(key);
    if (!command) throw new DurableChatConflictError("cannot record authority result before claiming command");
    command.authorityResult = result;
    command.receipt = receipt;
    command.state = "authority_committed";
  }
  async finalizePlanCommand(scope, commandKey) {
    const command = this.commands.get(this.commandKey(scope, commandKey));
    if (!command) throw new DurableChatConflictError("cannot finalize unknown plan command");
    command.state = "finalized";
  }
  async getPlanEffect(scope, effectKey) {
    return this.effects.get(this.effectKey(scope, effectKey)) ?? null;
  }
  async claimPlanEffect(scope, effect) {
    const key = this.effectKey(scope, effect.effectKey);
    const existing = this.effects.get(key);
    if (existing) return { status: "existing", record: existing };
    this.effects.set(key, effect);
    return { status: "claimed", record: effect };
  }
  async completePlanEffect(scope, effectKey) {
    const effect = this.effects.get(this.effectKey(scope, effectKey));
    if (!effect) throw new DurableChatConflictError("cannot complete unknown plan effect");
    effect.state = "completed";
    effect.completedAt = (/* @__PURE__ */ new Date()).toISOString();
  }
  async failPlanEffect(scope, effectKey, error) {
    const effect = this.effects.get(this.effectKey(scope, effectKey));
    if (!effect) throw new DurableChatConflictError("cannot fail unknown plan effect");
    effect.state = "error";
    effect.error = error;
  }
  async getInteractionProjection(scope, interactionId) {
    const directKey = this.interactionKey(scope, interactionId);
    const alias = this.interactionAliases.get(directKey);
    return this.interactions.get(alias ?? directKey) ?? null;
  }
  async listInteractionProjections(scope) {
    const prefix = this.scopePrefix(scope);
    return [...this.interactions.entries()].filter(([key]) => key.startsWith(prefix)).map(([, value]) => value);
  }
  async upsertInteractionProjection(scope, projection) {
    if (!projection.id) throw new TypeError("interaction projection requires id");
    const key = this.interactionKey(scope, projection.id);
    const prior = this.interactions.get(key);
    const resolvedPrior = prior ?? (() => {
      const alias = this.interactionAliases.get(key);
      return alias ? this.interactions.get(alias) : void 0;
    })();
    if (!prior && resolvedPrior) {
      this.interactionAliases.set(key, this.interactionKey(scope, resolvedPrior.id));
    }
    if (resolvedPrior) {
      if (projection.eventId && resolvedPrior.eventId === projection.eventId) return resolvedPrior;
      if (resolvedPrior.status !== "pending") {
        if (resolvedPrior.tombstone && resolvedPrior.kind === "unknown" && projection.status === resolvedPrior.status) {
          const enriched = {
            ...projection,
            id: resolvedPrior.id,
            status: resolvedPrior.status,
            tombstone: true,
            ...resolvedPrior.cancelReason ? { cancelReason: resolvedPrior.cancelReason } : {}
          };
          this.interactions.set(this.interactionKey(scope, resolvedPrior.id), enriched);
          return enriched;
        }
        if (resolvedPrior.status === projection.status && JSON.stringify(resolvedPrior.answers) === JSON.stringify(projection.answers)) return resolvedPrior;
        throw new DurableChatConflictError("interaction already has a conflicting terminal outcome");
      }
      if (projection.status !== "pending") {
        const canonical = resolvedPrior.id === projection.id ? projection : { ...projection, id: resolvedPrior.id };
        this.interactions.set(this.interactionKey(scope, canonical.id), canonical);
        if (resolvedPrior.semanticKey) {
          this.interactionSemantic.delete(this.scopedKey(scope, resolvedPrior.semanticKey));
        }
        return canonical;
      }
      return resolvedPrior;
    }
    if (projection.semanticKey) {
      const semantic = this.scopedKey(scope, projection.semanticKey);
      const priorId = this.interactionSemantic.get(semantic);
      if (priorId) {
        const canonicalKey = this.interactionKey(scope, priorId);
        this.interactionAliases.set(this.interactionKey(scope, projection.id), canonicalKey);
        return this.interactions.get(canonicalKey) ?? projection;
      }
      this.interactionSemantic.set(semantic, projection.id);
    }
    this.interactions.set(key, projection);
    return projection;
  }
  async getAnswerIntent(scope, intentKey2) {
    return this.answerIntents.get(this.intentKey(scope, intentKey2)) ?? null;
  }
  async claimAnswerIntent(scope, intent) {
    const key = this.intentKey(scope, intent.intentKey);
    const existing = this.answerIntents.get(key);
    if (!existing) {
      this.answerIntents.set(key, intent);
      return { status: "claimed", record: intent };
    }
    if (existing.interactionId === intent.interactionId && existing.attemptKey === intent.attemptKey && existing.outcome === intent.outcome && JSON.stringify(existing.data) === JSON.stringify(intent.data)) {
      return { status: "existing", record: existing };
    }
    return { status: "conflict", record: existing, reason: "answer intent key is already used by another answer" };
  }
  async acknowledgeAnswerIntent(scope, intentKey2, acknowledgement) {
    const intent = this.answerIntents.get(this.intentKey(scope, intentKey2));
    if (!intent) throw new DurableChatConflictError("cannot acknowledge unknown answer intent");
    if (intent.state === "finalized") return;
    intent.acknowledgement = acknowledgement;
    intent.state = "acknowledged";
  }
  async finalizeAnswerIntent(scope, intentKey2, guarantee = "reconciled") {
    const intent = this.answerIntents.get(this.intentKey(scope, intentKey2));
    if (!intent) throw new DurableChatConflictError("cannot finalize unknown answer intent");
    if (intent.state === "finalized") return;
    if (!intent.acknowledgement?.acknowledged) {
      throw new DurableChatConflictError("cannot finalize an answer before authority acknowledgement");
    }
    const projection = await this.getInteractionProjection(scope, intent.interactionId);
    if (!projection) throw new DurableChatConflictError("cannot finalize an answer before its ask projection");
    const status = intent.outcome === "accepted" ? "answered" : "declined";
    await this.upsertInteractionProjection(scope, {
      ...projection,
      status,
      ...intent.data ? { answers: intent.data } : {},
      updatedAt: intent.acknowledgement.at ?? (/* @__PURE__ */ new Date()).toISOString()
    });
    intent.state = "finalized";
    intent.guarantee = guarantee;
    intent.finalizedAt = (/* @__PURE__ */ new Date()).toISOString();
  }
  async abortAnswerIntent(scope, intentKey2, error) {
    const intent = this.answerIntents.get(this.intentKey(scope, intentKey2));
    if (!intent) throw new DurableChatConflictError("cannot abort unknown answer intent");
    if (intent.state === "finalized") return;
    intent.state = "aborted";
    intent.error = error;
  }
};
var InMemoryDurableChatStore = InMemoryDurableChatStateStore;
function createInMemoryDurableChatStateStore() {
  return new InMemoryDurableChatStateStore();
}

// src/durable-chat/interactions.ts
function nowIso(now) {
  return (now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
}
function intentKey(scope, interactionId, attemptKey) {
  return `interaction-answer:${encodeURIComponent(scope)}:${encodeURIComponent(interactionId)}:${encodeURIComponent(attemptKey)}`;
}
function ensureAttemptKey(attemptKey) {
  if (!attemptKey || typeof attemptKey !== "string" || attemptKey.length > 512) throw new TypeError("attemptKey must be a non-empty string");
}
function safeAnswers(data) {
  if (data === void 0) return void 0;
  for (const key of Object.keys(data)) {
    if (!isSafeInteractionFieldKey(key)) throw new TypeError(`unsafe interaction answer key: ${key}`);
  }
  const parsed = parseInteractionAnswers(data);
  if (!parsed.succeeded) throw new TypeError(parsed.error);
  return parsed.value;
}
async function upsertDurableInteractionAsk(store, scope, request, options = {}) {
  const semanticKey = options.semanticKey ?? questionInteractionContentSignature(interactionFromWireRequest(request)) ?? void 0;
  const incoming = {
    ...interactionFromWireRequest(request),
    ...options.eventId ? { eventId: options.eventId } : {},
    ...semanticKey ? { semanticKey } : {},
    updatedAt: nowIso(options.now)
  };
  const prior = await store.getInteractionProjection(scope, request.id);
  if (prior?.tombstone) {
    return store.upsertInteractionProjection(scope, {
      ...incoming,
      status: prior.status,
      tombstone: true,
      ...prior.cancelReason ? { cancelReason: prior.cancelReason } : {}
    });
  }
  if (prior && prior.status !== "pending") return prior;
  return store.upsertInteractionProjection(scope, incoming);
}
async function recordDurableInteractionCancel(store, scope, interactionId, reason, options = {}) {
  if (!interactionId) throw new TypeError("interaction id is required");
  const status = cancelStatusFor(reason);
  const prior = await store.getInteractionProjection(scope, interactionId);
  if (prior) {
    if (prior.status === status) return prior;
    if (prior.status !== "pending") throw new DurableChatConflictError("interaction already has a conflicting terminal outcome");
    return store.upsertInteractionProjection(scope, {
      ...prior,
      status,
      tombstone: true,
      ...reason ? { cancelReason: reason } : {},
      ...options.eventId ? { eventId: options.eventId } : {},
      updatedAt: nowIso(options.now)
    });
  }
  return store.upsertInteractionProjection(scope, {
    id: interactionId,
    kind: "unknown",
    title: "",
    fields: [],
    status,
    tombstone: true,
    ...reason ? { cancelReason: reason } : {},
    ...options.eventId ? { eventId: options.eventId } : {},
    updatedAt: nowIso(options.now)
  });
}
async function recordDurableInteractionAnswer(store, scope, interactionId, outcome, answers, options = {}) {
  const prior = await store.getInteractionProjection(scope, interactionId);
  if (!prior) throw new DurableChatConflictError("cannot answer an interaction before its ask projection");
  const status = outcome === "accepted" ? "answered" : "declined";
  const parsed = safeAnswers(answers);
  if (prior.status === status && JSON.stringify(prior.answers) === JSON.stringify(parsed)) return prior;
  if (prior.status !== "pending") throw new DurableChatConflictError("interaction already has a conflicting terminal outcome");
  return store.upsertInteractionProjection(scope, {
    ...prior,
    status,
    ...parsed ? { answers: parsed } : {},
    ...options.eventId ? { eventId: options.eventId } : {},
    updatedAt: nowIso(options.now)
  });
}
function createDurableInteractionSettlement(options) {
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
  return {
    async prepare(scope, interactionId, outcome, data) {
      ensureAttemptKey(options.attemptKey);
      const parsed = safeAnswers(data);
      const record = {
        scope,
        interactionId,
        attemptKey: options.attemptKey,
        intentKey: intentKey(scope, interactionId, options.attemptKey),
        outcome,
        ...parsed ? { data: parsed } : {},
        state: "prepared",
        guarantee: options.guarantee ?? "reconciled",
        createdAt: now()
      };
      const result = await options.store.claimAnswerIntent(scope, record);
      if (result.status === "conflict") throw new DurableChatConflictError(result.reason);
      return result.record;
    },
    async acknowledge(scope, key, acknowledgement = {}) {
      const existing = await options.store.getAnswerIntent(scope, key);
      if (!existing) throw new DurableChatConflictError("unknown answer intent");
      const value = {
        acknowledged: true,
        ...acknowledgement.authorityId ? { authorityId: acknowledgement.authorityId } : {},
        ...acknowledgement.status ? { status: acknowledgement.status } : {},
        at: acknowledgement.at ?? now()
      };
      await options.store.acknowledgeAnswerIntent(scope, key, value);
      return await options.store.getAnswerIntent(scope, key);
    },
    async finalize(scope, key) {
      const existing = await options.store.getAnswerIntent(scope, key);
      if (!existing) throw new DurableChatConflictError("unknown answer intent");
      await options.store.finalizeAnswerIntent(scope, key, options.guarantee ?? "reconciled");
      return await options.store.getAnswerIntent(scope, key);
    },
    async abort(scope, key, error) {
      const existing = await options.store.getAnswerIntent(scope, key);
      if (!existing) throw new DurableChatConflictError("unknown answer intent");
      await options.store.abortAnswerIntent(scope, key, error);
      return await options.store.getAnswerIntent(scope, key);
    },
    async reconcile(scope, key) {
      const existing = await options.store.getAnswerIntent(scope, key);
      if (!existing || !options.reconcileAuthority) return existing;
      if (existing.state === "finalized") return existing;
      const acknowledgement = await options.reconcileAuthority({ scope, intent: existing });
      if (!acknowledgement) return existing;
      await options.store.acknowledgeAnswerIntent(scope, key, {
        acknowledged: true,
        ...acknowledgement.authorityId ? { authorityId: acknowledgement.authorityId } : {},
        ...acknowledgement.status ? { status: acknowledgement.status } : {},
        ...acknowledgement.at ? { at: acknowledgement.at } : {}
      });
      await options.store.finalizeAnswerIntent(scope, key, options.guarantee ?? "reconciled");
      return await options.store.getAnswerIntent(scope, key);
    }
  };
}
function durableInteractionIntentKey(scope, interactionId, attemptKey) {
  ensureAttemptKey(attemptKey);
  return intentKey(scope, interactionId, attemptKey);
}
var applyDurableInteractionAsk = upsertDurableInteractionAsk;
var applyDurableInteractionCancel = recordDurableInteractionCancel;
var applyDurableInteractionAnswer = recordDurableInteractionAnswer;

// src/durable-chat/adapters.ts
function createDurableInteractionProjectionAdapter(options) {
  const observed = /* @__PURE__ */ new Set();
  return {
    async upsertAsk(request) {
      observed.add(request.id);
      await upsertDurableInteractionAsk(options.store, options.scope, request, { now: options.now });
    },
    async cancel(cancel) {
      observed.add(cancel.id);
      await recordDurableInteractionCancel(
        options.store,
        options.scope,
        cancel.id,
        cancel.reason,
        { now: options.now }
      );
    },
    async materialize() {
      const projections = await Promise.all(
        [...observed].map((id) => options.store.getInteractionProjection(options.scope, id))
      );
      const canonical = new Map(projections.filter((item) => item !== null).map((item) => [item.id, item]));
      return [...canonical.values()].filter((projection) => projection.kind !== "unknown").map((projection) => interactionToPersistedPart(
        {
          id: projection.id,
          kind: projection.kind,
          title: projection.title,
          ...projection.body ? { body: projection.body } : {},
          answerSpec: { fields: projection.fields }
        },
        projection.status,
        projection.cancelReason,
        projection.answers
      ));
    }
  };
}
function eventRecord(value) {
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
  const record = value;
  if (record.kind === "event" && record.event && typeof record.event === "object") {
    return record.event;
  }
  return record;
}
function createDurableChatEventProjection(options) {
  const interactions = createDurableInteractionProjectionAdapter(options);
  const plans = /* @__PURE__ */ new Set();
  return {
    async observe(value) {
      const event = eventRecord(value);
      if (!event || typeof event.type !== "string") return;
      const data = eventRecord(event.data);
      if (event.type === "interaction") {
        const parsed = parseInteractionRequest(data ?? void 0);
        if (parsed.succeeded) await interactions.upsertAsk(parsed.value);
        return;
      }
      if (event.type === "interaction.cancel") {
        const parsed = parseInteractionCancel(data ?? void 0);
        if (parsed.succeeded) {
          try {
            await interactions.cancel(parsed.value);
          } catch (error) {
            if (!(error instanceof DurableChatConflictError)) throw error;
          }
        }
        return;
      }
      if (!event.type.startsWith("plan.")) return;
      const submitted = parsePlanSubmittedEvent(event);
      const planRecord = eventRecord(data?.plan) ?? eventRecord(eventRecord(event.properties)?.plan);
      const plan = submitted.succeeded ? submitted.value : planRecord ? persistedPartToPlan({ type: "plan", ...planRecord }) : null;
      if (!plan) return;
      plans.add(`${plan.planId}\0${plan.revision}`);
      try {
        await options.store.putPlanProjection(options.scope, plan);
      } catch (error) {
        if (!(error instanceof DurableChatConflictError)) throw error;
      }
    },
    async materialize() {
      const interactionParts = await interactions.materialize();
      const planParts = await Promise.all([...plans].map(async (key) => {
        const [planId, rawRevision] = key.split("\0");
        const plan = await options.store.getPlanProjection(options.scope, planId, Number(rawRevision));
        return plan ? planToPersistedPart(plan) : null;
      }));
      return [...interactionParts, ...planParts.filter((part) => part !== null)];
    }
  };
}
function createDurableInteractionRoutePersistence(options) {
  const guarantee = options.guarantee;
  return {
    guarantee,
    async prepare(args) {
      const scope = await options.scope(args);
      const settlement = createDurableInteractionSettlement({
        store: options.store,
        attemptKey: args.attemptKey,
        guarantee,
        now: options.now,
        ...options.reconcileAuthority ? { reconcileAuthority: ({ intent: intent2 }) => options.reconcileAuthority({ scope, intent: intent2, route: args }) } : {}
      });
      const intent = await settlement.prepare(
        scope,
        args.answer.id,
        args.answer.outcome,
        args.answer.data
      );
      return { scope, settlement, intent };
    },
    async reconcile({ prepared }) {
      let intent = await prepared.settlement.reconcile(prepared.scope, prepared.intent.intentKey);
      if (intent?.state === "acknowledged") {
        intent = await prepared.settlement.finalize(prepared.scope, prepared.intent.intentKey);
      }
      return { settled: intent?.state === "finalized" };
    },
    async acknowledge({ prepared }) {
      await prepared.settlement.acknowledge(prepared.scope, prepared.intent.intentKey, {
        status: prepared.intent.outcome
      });
    },
    async finalize({ prepared }) {
      await prepared.settlement.finalize(prepared.scope, prepared.intent.intentKey);
    },
    async fail({ prepared, error }) {
      await prepared.settlement.abort(
        prepared.scope,
        prepared.intent.intentKey,
        error instanceof Error ? error.message : String(error)
      );
    }
  };
}

// src/durable-chat/plan-routes.ts
function errorResponse(error, fallbackCode, fallbackStatus, fallbackMessage) {
  if (error instanceof Response) return error;
  if (error && typeof error === "object" && "status" in error && typeof error.status === "number") {
    const typed = error;
    return Response.json(
      { code: typeof typed.code === "string" ? typed.code : fallbackCode, error: typeof typed.message === "string" ? typed.message : fallbackMessage },
      { status: typed.status }
    );
  }
  return Response.json({ code: fallbackCode, error: fallbackMessage }, { status: fallbackStatus });
}
function scopeFromAuthorization(value) {
  if (value instanceof Response) return value;
  if (typeof value === "string" && value) return value;
  if (value && typeof value === "object" && "scope" in value && value.scope) return value.scope;
  return Response.json({ code: "DURABLE_CHAT_UNAUTHORIZED", error: "not authorized" }, { status: 401 });
}
function planIdFromUrl(request) {
  const url = new URL(request.url);
  const query = url.searchParams.get("planId") ?? url.searchParams.get("id");
  if (query) return query;
  const segments = url.pathname.split("/").filter(Boolean);
  return segments.length > 0 ? decodeURIComponent(segments[segments.length - 1]) : null;
}
function asCurrent(value) {
  if (value === null) return { plan: null };
  if ("plan" in value) return value;
  return { plan: value };
}
function terminalDecision(plan) {
  if (!plan) return null;
  return plan.status === "approved" || plan.status === "rejected" ? plan.status : null;
}
function parseDecisionBody(body) {
  if (!body || typeof body !== "object" || Array.isArray(body)) return { error: "Invalid JSON body" };
  const record = body;
  const planId = typeof record.planId === "string" && record.planId ? record.planId : typeof record.id === "string" && record.id ? record.id : null;
  const revision = record.revision;
  const decision = normalizePlanDecision(record.decision ?? record.outcome ?? record.status);
  if (!planId) return { error: "Missing planId" };
  if (typeof revision !== "number" || !Number.isInteger(revision) || revision < 1) return { error: "revision must be a positive integer" };
  if (!decision) return { error: "decision must be approved or rejected" };
  if (record.feedback !== void 0 && typeof record.feedback !== "string") return { error: "feedback must be a string" };
  return { planId, revision, decision, ...typeof record.feedback === "string" ? { feedback: record.feedback } : {} };
}
function withReceipt(result, scope, parsed) {
  const receipt = result.receipt ?? stablePlanReceipt(scope, parsed.planId, parsed.revision, parsed.decision, result);
  return { ...result, receipt };
}
function recoverReceipt(scope, plan, decision) {
  return stablePlanReceipt(scope, plan.planId, plan.revision, decision, {
    followUp: { turnId: planFollowUpTurnId(plan.planId, decision), state: "unknown" }
  });
}
function createDurablePlanRoutes(options) {
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
  const logger = options.logger ?? console;
  async function authorize(request, operation, planId) {
    try {
      return scopeFromAuthorization(await options.authorize({ request, operation, ...planId ? { planId } : {} }));
    } catch (error) {
      return errorResponse(error, "DURABLE_CHAT_UNAUTHORIZED", 401, "not authorized");
    }
  }
  async function runEffect(scope, plan, receipt) {
    const effectKey = planEffectKey(scope, plan.planId, plan.revision, receipt.decision);
    try {
      const existing = await options.store.getPlanEffect(scope, effectKey);
      if (existing?.state === "completed") return true;
      const claim = await options.store.claimPlanEffect(scope, {
        effectKey,
        scope,
        planId: plan.planId,
        revision: plan.revision,
        decision: receipt.decision,
        state: "claimed",
        claimedAt: now()
      });
      if (claim.status === "existing" && claim.record.state === "completed") return true;
      await options.afterDecision({ scope, plan, receipt, effectKey });
      await options.store.completePlanEffect(scope, effectKey);
      return true;
    } catch (error) {
      await options.store.failPlanEffect(scope, effectKey, error instanceof Error ? error.message : String(error)).catch(() => void 0);
      logger.warn("[durable-chat] afterDecision failed:", error);
      return false;
    }
  }
  async function current(request) {
    const planId = planIdFromUrl(request);
    if (!planId) return Response.json({ code: "DURABLE_CHAT_BAD_REQUEST", error: "Missing planId" }, { status: 400 });
    const scope = await authorize(request, "current", planId);
    if (scope instanceof Response) return scope;
    const local = await options.store.getPlanProjection(scope, planId);
    let authoritative;
    try {
      authoritative = asCurrent(await options.authority.current({ scope, planId }));
    } catch (error) {
      const decision2 = terminalDecision(local);
      if (decision2) {
        const command = await options.store.getPlanCommand(scope, planCommandKey(planId, local.revision, decision2));
        if (command?.receipt) {
          const effectComplete2 = await runEffect(scope, command.authorityResult?.plan ?? local, command.receipt);
          return Response.json({
            plan: command.authorityResult?.plan ?? local,
            receipt: command.receipt,
            replayed: true,
            ...!effectComplete2 ? { effectPending: true } : {}
          });
        }
      }
      return errorResponse(error, "DURABLE_CHAT_UNAVAILABLE", 503, "plan authority unavailable");
    }
    if (!authoritative.plan) {
      return Response.json({ code: "DURABLE_CHAT_GONE", error: "plan is no longer available" }, { status: 410 });
    }
    const authorityPlan = authoritative.plan;
    const localWins = Boolean(local && (local.revision > authorityPlan.revision || local.revision === authorityPlan.revision && !canTransitionPlanStatus(local.status, authorityPlan.status)));
    const plan = localWins ? local : authorityPlan;
    let projectionPending = false;
    if (!localWins) {
      try {
        await options.store.putPlanProjection(scope, plan);
      } catch (error) {
        projectionPending = true;
        logger.warn("[durable-chat] failed to heal plan projection:", error);
      }
    }
    const decision = terminalDecision(plan);
    let receipt = localWins ? void 0 : authoritative.receipt;
    if (!receipt && decision) {
      const command = await options.store.getPlanCommand(scope, planCommandKey(plan.planId, plan.revision, decision));
      receipt = command?.receipt;
      if (!receipt) receipt = recoverReceipt(scope, plan, decision);
    }
    const effectComplete = decision && receipt ? await runEffect(scope, plan, receipt) : true;
    return Response.json({
      plan,
      ...receipt ? { receipt } : {},
      ...projectionPending ? { projectionPending: true } : {},
      ...!effectComplete ? { effectPending: true } : {}
    });
  }
  async function decide(request) {
    const body = await request.json().catch(() => null);
    const parsed = parseDecisionBody(body);
    if ("error" in parsed) return Response.json({ code: "DURABLE_CHAT_BAD_REQUEST", error: parsed.error }, { status: 400 });
    const scope = await authorize(request, "decide", parsed.planId);
    if (scope instanceof Response) return scope;
    const local = await options.store.getPlanProjection(scope, parsed.planId, parsed.revision);
    const current2 = await options.store.getPlanProjection(scope, parsed.planId);
    if (!local) {
      return Response.json({ code: "DURABLE_CHAT_UNAVAILABLE", error: "submitted plan projection is required before deciding" }, { status: 503 });
    }
    if (current2 && parsed.revision < current2.revision) {
      return Response.json({ code: "DURABLE_CHAT_CONFLICT", error: "stale plan revision" }, { status: 409 });
    }
    if (local.status !== "pending") {
      const existingTerminal = terminalDecision(local);
      if (existingTerminal === parsed.decision) {
        const existing = await options.store.getPlanCommand(scope, planCommandKey(parsed.planId, parsed.revision, parsed.decision));
        const receipt = existing?.receipt ?? recoverReceipt(scope, local, parsed.decision);
        const effectComplete2 = await runEffect(scope, existing?.authorityResult?.plan ?? local, receipt);
        return Response.json({
          plan: existing?.authorityResult?.plan ?? local,
          receipt,
          replayed: true,
          ...!effectComplete2 ? { effectPending: true } : {}
        });
      }
      return Response.json({
        code: "DURABLE_CHAT_CONFLICT",
        error: "plan is not pending",
        plan: local
      }, { status: 409 });
    }
    const opposite = parsed.decision === "approved" ? "rejected" : "approved";
    const oppositeCommand = await options.store.getPlanCommand(scope, planCommandKey(parsed.planId, parsed.revision, opposite));
    if (oppositeCommand) return Response.json({ code: "DURABLE_CHAT_CONFLICT", error: "competing decision for plan revision" }, { status: 409 });
    const commandKey = planCommandKey(parsed.planId, parsed.revision, parsed.decision);
    const authorityIdempotencyKey = planAuthorityIdempotencyKey(scope, parsed.planId, parsed.revision, parsed.decision);
    const command = {
      scope,
      planId: parsed.planId,
      revision: parsed.revision,
      decision: parsed.decision,
      commandKey,
      authorityIdempotencyKey,
      state: "claimed",
      claimedAt: now()
    };
    const claimed = await options.store.claimPlanCommand(scope, command);
    if (claimed.status === "conflict") return Response.json({ code: "DURABLE_CHAT_CONFLICT", error: claimed.reason }, { status: 409 });
    if (claimed.status === "existing" && claimed.record.receipt) {
      const effectComplete2 = await runEffect(scope, claimed.record.authorityResult?.plan ?? local, claimed.record.receipt);
      return Response.json({
        plan: claimed.record.authorityResult?.plan ?? local,
        receipt: claimed.record.receipt,
        replayed: true,
        ...!effectComplete2 ? { effectPending: true } : {}
      });
    }
    let authorityResult;
    try {
      authorityResult = withReceipt(await options.authority.decide({
        scope,
        planId: parsed.planId,
        revision: parsed.revision,
        decision: parsed.decision,
        ...parsed.feedback ? { feedback: parsed.feedback } : {},
        idempotencyKey: authorityIdempotencyKey
      }), scope, parsed);
    } catch (error) {
      try {
        const reconciled = asCurrent(await options.authority.current({
          scope,
          planId: parsed.planId,
          revision: parsed.revision
        }));
        const terminal = terminalDecision(reconciled.plan);
        if (reconciled.plan && terminal === parsed.decision) {
          const receipt = reconciled.receipt ?? recoverReceipt(scope, reconciled.plan, terminal);
          authorityResult = {
            plan: reconciled.plan,
            followUp: { turnId: receipt.turnId, state: receipt.state },
            idempotent: true,
            receipt
          };
        } else if (reconciled.plan && reconciled.plan.status !== "pending") {
          let projectionPending2 = false;
          try {
            await options.store.putPlanProjection(scope, reconciled.plan);
          } catch (projectionError) {
            projectionPending2 = true;
            logger.warn("[durable-chat] failed to reconcile conflicting plan projection:", projectionError);
          }
          return Response.json({
            code: "DURABLE_CHAT_CONFLICT",
            error: "plan is not pending",
            plan: reconciled.plan,
            ...reconciled.receipt ? { receipt: reconciled.receipt } : {},
            ...projectionPending2 ? { projectionPending: true } : {}
          }, { status: 409 });
        } else {
          return errorResponse(error, "DURABLE_CHAT_UNAVAILABLE", 503, "plan authority unavailable");
        }
      } catch {
        return errorResponse(error, "DURABLE_CHAT_UNAVAILABLE", 503, "plan authority unavailable");
      }
    }
    let projectionPending = false;
    try {
      await options.store.recordPlanAuthorityResult(scope, commandKey, authorityResult, authorityResult.receipt);
    } catch (error) {
      projectionPending = true;
      logger.warn("[durable-chat] failed to record authority result:", error);
    }
    try {
      await options.store.putPlanProjection(scope, authorityResult.plan);
      await options.store.finalizePlanCommand(scope, commandKey);
    } catch (error) {
      projectionPending = true;
      logger.warn("[durable-chat] failed to finalize plan projection:", error);
    }
    const effectComplete = await runEffect(scope, authorityResult.plan, authorityResult.receipt);
    return Response.json({
      plan: authorityResult.plan,
      receipt: authorityResult.receipt,
      ...projectionPending ? { projectionPending: true } : {},
      ...!effectComplete ? { effectPending: true } : {}
    });
  }
  return { current, decide };
}

export {
  DurableChatError,
  DurableChatConflictError,
  DurableChatUnavailableError,
  DurableChatGoneError,
  createDurableChatScope,
  durableChatScopeKey,
  normalizePlanDecision,
  planCommandKey,
  planAuthorityIdempotencyKey,
  planEffectKey,
  stablePlanReceipt,
  InMemoryDurableChatStateStore,
  InMemoryDurableChatStore,
  createInMemoryDurableChatStateStore,
  upsertDurableInteractionAsk,
  recordDurableInteractionCancel,
  recordDurableInteractionAnswer,
  createDurableInteractionSettlement,
  durableInteractionIntentKey,
  applyDurableInteractionAsk,
  applyDurableInteractionCancel,
  applyDurableInteractionAnswer,
  createDurableInteractionProjectionAdapter,
  createDurableChatEventProjection,
  createDurableInteractionRoutePersistence,
  createDurablePlanRoutes
};
//# sourceMappingURL=chunk-6GQ5DKFK.js.map