@m8t-stack/cli 0.2.80 → 0.2.82

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/cli.js CHANGED
@@ -229,10 +229,77 @@ var init_agent_roster = __esm({
229
229
  });
230
230
 
231
231
  // ../../packages/api-contract/dist/esm/ui-directive.js
232
- var DECISION_REFUSAL_REASONS, decisionRefusalReasons;
232
+ function parseArgs(input) {
233
+ if (!isRecord(input) || !nonEmpty(input.title) || !Array.isArray(input.options))
234
+ return null;
235
+ const { options } = input;
236
+ if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS)
237
+ return null;
238
+ const parsed = [];
239
+ for (const option of options) {
240
+ if (!isRecord(option) || !nonEmpty(option.label) || !nonEmpty(option.detail))
241
+ return null;
242
+ parsed.push({ label: option.label, detail: option.detail });
243
+ }
244
+ return { title: input.title, options: parsed };
245
+ }
246
+ function parseState(input, optionCount) {
247
+ if (!isRecord(input))
248
+ return null;
249
+ switch (input.status) {
250
+ case "pending":
251
+ return { status: "pending" };
252
+ case "dismissed":
253
+ return { status: "dismissed" };
254
+ case "submitting":
255
+ return inRange(input.optionIndex, optionCount) ? { status: "submitting", optionIndex: input.optionIndex } : null;
256
+ case "selected":
257
+ if (!inRange(input.optionIndex, optionCount))
258
+ return null;
259
+ return input.acknowledgement === "pending" || input.acknowledgement === "complete" || input.acknowledgement === "error" ? { status: "selected", optionIndex: input.optionIndex, acknowledgement: input.acknowledgement } : null;
260
+ case "error":
261
+ return nonEmpty(input.message) ? { status: "error", message: input.message } : null;
262
+ default:
263
+ return null;
264
+ }
265
+ }
266
+ function parseUiDirectiveFrame(input) {
267
+ if (!isRecord(input))
268
+ return null;
269
+ if (input.source !== "voice" && input.source !== "text")
270
+ return null;
271
+ const directive = input.directive;
272
+ if (!isRecord(directive))
273
+ return null;
274
+ if (directive.version !== 1 || directive.name !== "present_decision")
275
+ return null;
276
+ if (!nonEmpty(directive.callId))
277
+ return null;
278
+ const args = parseArgs(directive.args);
279
+ if (!args)
280
+ return null;
281
+ const state = parseState(directive.state, args.options.length);
282
+ if (!state)
283
+ return null;
284
+ return {
285
+ directive: { version: 1, name: "present_decision", callId: directive.callId, args, state },
286
+ source: input.source
287
+ };
288
+ }
289
+ function isDecisionRefusalReason(value) {
290
+ return typeof value === "string" && decisionRefusalReasons.includes(value);
291
+ }
292
+ var UI_DIRECTIVE_PART_TYPE, MIN_OPTIONS, MAX_OPTIONS, isRecord, nonEmpty, inRange, UI_DIRECTIVE_RESULT_PART_TYPE, DECISION_REFUSAL_REASONS, decisionRefusalReasons;
233
293
  var init_ui_directive = __esm({
234
294
  "../../packages/api-contract/dist/esm/ui-directive.js"() {
235
295
  "use strict";
296
+ UI_DIRECTIVE_PART_TYPE = "data-ui-directive";
297
+ MIN_OPTIONS = 2;
298
+ MAX_OPTIONS = 4;
299
+ isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
300
+ nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
301
+ inRange = (value, count) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value < count;
302
+ UI_DIRECTIVE_RESULT_PART_TYPE = "data-ui-result";
236
303
  DECISION_REFUSAL_REASONS = {
237
304
  /** 409 — no live call with that `callId`. Answered twice, or talked past and dismissed. */
238
305
  notPending: "decision_not_pending",
@@ -1400,7 +1467,7 @@ var init_enable_hosted_brain = __esm({
1400
1467
  import { Builtins, Cli } from "clipanion";
1401
1468
 
1402
1469
  // src/lib/package-version.ts
1403
- var CLI_VERSION = "0.2.80";
1470
+ var CLI_VERSION = "0.2.82";
1404
1471
 
1405
1472
  // src/lib/render-error.ts
1406
1473
  init_errors();
@@ -12728,6 +12795,25 @@ function classifyFoundryError(err) {
12728
12795
  return { category: "unknown", retryable: false, status, message };
12729
12796
  }
12730
12797
 
12798
+ // ../../packages/foundry-invoke/dist/esm/artifacts.js
12799
+ var FENCE_RE = /```json m8t:artifacts\s*\n([\s\S]*?)\n```/;
12800
+ function parseArtifacts(raw) {
12801
+ const text = raw;
12802
+ const m = FENCE_RE.exec(text);
12803
+ if (!m)
12804
+ return { text: text.trimEnd(), artifacts: [] };
12805
+ let artifacts = [];
12806
+ try {
12807
+ const parsed = JSON.parse(m[1]);
12808
+ if (Array.isArray(parsed))
12809
+ artifacts = parsed;
12810
+ } catch {
12811
+ artifacts = [];
12812
+ }
12813
+ const clean2 = text.slice(0, m.index).trimEnd();
12814
+ return { text: clean2, artifacts };
12815
+ }
12816
+
12731
12817
  // src/lib/render-error.ts
12732
12818
  function renderHint(hint) {
12733
12819
  return hint.split("\n").join("\n ");
@@ -26631,13 +26717,66 @@ import { TableClient as TableClient7 } from "@azure/data-tables";
26631
26717
  import { AIProjectClient as AIProjectClient3 } from "@azure/ai-projects";
26632
26718
  import { LogsQueryClient as LogsQueryClient3, LogsQueryResultStatus as LogsQueryResultStatus3 } from "@azure/monitor-query";
26633
26719
 
26720
+ // ../../packages/agent-ledger/dist/esm/pii.js
26721
+ import { createHmac } from "crypto";
26722
+ var EMAIL_PLACEHOLDER = "[email]";
26723
+ var PHONE_PLACEHOLDER = "[phone]";
26724
+ var SECRET_PLACEHOLDER = "[secret]";
26725
+ var PEM_BLOCK_RE = /-----BEGIN [A-Z0-9 ]{0,32}PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]{0,32}PRIVATE KEY-----/g;
26726
+ var JWT_RE = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}(?:\.[A-Za-z0-9_-]{10,})?/g;
26727
+ var HTTP_CREDENTIAL_RE = /\b(Bearer|Basic)[ \t]+(?=[A-Za-z0-9._~+/=-]{0,80}[0-9+/=])[A-Za-z0-9._~+/=-]{16,}/gi;
26728
+ var KNOWN_PREFIX_RE = /(?<![A-Za-z0-9_-])(?:sk-[A-Za-z0-9_-]{16,}|rk-[A-Za-z0-9_-]{16,}|xox[a-z]-[A-Za-z0-9-]{10,}|gh[pousr]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|npm_[A-Za-z0-9]{28,}|AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|ya29\.[A-Za-z0-9_-]{20,}|dckr_pat_[A-Za-z0-9_-]{20,}|shp(?:at|ca|pa|ss)_[a-fA-F0-9]{32,})(?![A-Za-z0-9_-])/g;
26729
+ var KEYED_VALUE = `("[^"\\n]{8,}"|'[^'\\n]{8,}'|(?=[A-Za-z0-9+/=_.~%-]{0,64}[0-9+/=])[A-Za-z0-9+/=_.~%-]{8,})`;
26730
+ var KEYED_STRONG_SECRET_RE = new RegExp(`(?<![A-Za-z0-9])(shared[_-]?access[_-]?key|account[_-]?key|access[_-]?key|storage[_-]?key|api[_-]?key|apikey|x[_-]?api[_-]?key|client[_-]?secret|sas[_-]?token|access[_-]?token|refresh[_-]?token|auth[_-]?token|id[_-]?token|bot[_-]?token|private[_-]?key|password|passwd|pwd)(["']?\\s*[:=]\\s*)("[^"\\n]{8,}"|'[^'\\n]{8,}'|[A-Za-z0-9+/=_.~%-]{8,})`, "gi");
26731
+ var KEYED_WEAK_SECRET_RE = new RegExp(`(?<![A-Za-z0-9])(authorization|secret|token|sig)(["']?\\s*[:=]\\s*)` + KEYED_VALUE, "gi");
26732
+ var TELEGRAM_BOT_TOKEN_RE = /(?<!\d)\d{8,10}:AA[A-Za-z0-9_-]{32,34}(?![A-Za-z0-9_-])/g;
26733
+ var SPOKEN_SECRET_RE = /\b((?:password|passcode|passphrase|api[ _-]?key|access code|pin(?: code)?)\s+is[ \t:]+)(?=\S{0,32}[0-9])\S{4,64}/gi;
26734
+ var SPOKEN_PASSWORD_RE = /\b((?:password|passcode|passphrase)\s+is[ \t:]+)(?!\[)(?!(?:wrong|incorrect|invalid|expired|required|missing|reset|unchanged|correct|secure|weak|strong|blank|empty|not|now|still|too|the|already|also|only|just|being|getting)\b)("[^"\n]{4,64}"|'[^'\n]{4,64}'|\S{4,64})/gi;
26735
+ var B64ISH_RE = /(?<![A-Za-z0-9+=_-])(?=[A-Za-z0-9+=_-]{0,64}[A-Z])(?=[A-Za-z0-9+=_-]{0,64}[a-z])(?=[A-Za-z0-9+=_-]{0,64}[0-9])[A-Za-z0-9+=_-]{40,}(?![A-Za-z0-9+=_-])/g;
26736
+ var EMAIL_RE = new RegExp("[\\p{L}\\p{N}._%+-]+@[\\p{L}\\p{N}](?:[\\p{L}\\p{N}.-]*[\\p{L}\\p{N}])?\\.\\p{L}{2,}(?![\\p{L}\\p{N}-])", "gu");
26737
+ var PHONE_INTL_RE = /(?<![\d/.-])\+\d{1,3}(?:[ .-]?\(\d{1,4}\))?(?:[ .-]?\d){6,12}(?!\d)/g;
26738
+ var PHONE_SEP_RE = /(?<![\d.-])(?:\d[ .-])?(?:\(\d{3}\)[ .-]?\d{3}[ .-]?\d{4}|\d{3}[ .-]\d{3}[ .-]\d{4}|0\d{1,2}[ .-]\d{7})(?![\d-])/g;
26739
+ var PHONE_CONTEXT_RE = /\b((?:call|text|dial|whatsapp)(?:\s+(?:me|us|him|her|them|back))?(?:\s+(?:at|on))?\s*[:-]?\s*|(?:phone|mobile|cell|tel)(?:\s*(?:number|no\.?))?\s*(?:is|:)?\s*)(?<![\d.(-])\d{7,15}(?![\d.])/gi;
26740
+ var PEM_OPEN_RE = /-----BEGIN [A-Z0-9 ]{0,32}PRIVATE KEY-----/;
26741
+ var UNCLOSED_QUOTED_SECRET_RE = /(?<![A-Za-z0-9])(?:shared[_-]?access[_-]?key|account[_-]?key|access[_-]?key|storage[_-]?key|api[_-]?key|apikey|x[_-]?api[_-]?key|client[_-]?secret|sas[_-]?token|access[_-]?token|refresh[_-]?token|auth[_-]?token|id[_-]?token|bot[_-]?token|private[_-]?key|password|passwd|pwd|authorization|secret|token|sig)["']?\s*[:=]\s*["'][^"'\n]*$/i;
26742
+ function failClosedOnCutConstructs(text) {
26743
+ const pem = PEM_OPEN_RE.exec(text);
26744
+ if (pem && !/-----END [A-Z0-9 ]{0,32}PRIVATE KEY-----/.test(text.slice(pem.index))) {
26745
+ return text.slice(0, pem.index) + SECRET_PLACEHOLDER;
26746
+ }
26747
+ const quoted = UNCLOSED_QUOTED_SECRET_RE.exec(text);
26748
+ if (quoted)
26749
+ return text.slice(0, quoted.index) + SECRET_PLACEHOLDER;
26750
+ return text;
26751
+ }
26752
+ function maskSecrets(text) {
26753
+ const masked = text.replace(PEM_BLOCK_RE, SECRET_PLACEHOLDER).replace(JWT_RE, SECRET_PLACEHOLDER).replace(HTTP_CREDENTIAL_RE, (_, scheme) => `${scheme} ${SECRET_PLACEHOLDER}`).replace(KNOWN_PREFIX_RE, SECRET_PLACEHOLDER).replace(TELEGRAM_BOT_TOKEN_RE, SECRET_PLACEHOLDER).replace(KEYED_STRONG_SECRET_RE, (_, key2, sep4) => `${key2}${sep4}${SECRET_PLACEHOLDER}`).replace(KEYED_WEAK_SECRET_RE, (_, key2, sep4) => `${key2}${sep4}${SECRET_PLACEHOLDER}`).replace(SPOKEN_SECRET_RE, (_, lead) => `${lead}${SECRET_PLACEHOLDER}`).replace(SPOKEN_PASSWORD_RE, (_, lead) => `${lead}${SECRET_PLACEHOLDER}`).replace(B64ISH_RE, SECRET_PLACEHOLDER);
26754
+ return failClosedOnCutConstructs(masked);
26755
+ }
26756
+ function maskContact(text) {
26757
+ return text.replace(EMAIL_RE, EMAIL_PLACEHOLDER).replace(PHONE_INTL_RE, PHONE_PLACEHOLDER).replace(PHONE_SEP_RE, PHONE_PLACEHOLDER).replace(PHONE_CONTEXT_RE, (_, lead) => `${lead}${PHONE_PLACEHOLDER}`);
26758
+ }
26759
+ var BARE_PHONE_RUN_RE = /(?<![\w.,-])\d{7,15}(?!\.?\d)(?![\w,-])/g;
26760
+ function maskContactStrict(text) {
26761
+ return maskContact(text).replace(BARE_PHONE_RUN_RE, PHONE_PLACEHOLDER);
26762
+ }
26763
+ var USERREF_DIGEST_VERSION = "1";
26764
+ function userRefHashKey() {
26765
+ return process.env.M8T_LEDGER_USERREF_KEY ?? (process.env.STORAGE_ACCOUNT_NAME ? `m8t-ledger-userref:${process.env.STORAGE_ACCOUNT_NAME}` : "m8t-agent-ledger-userref-dev");
26766
+ }
26767
+ function maskUserRef(ref) {
26768
+ if (ref == null)
26769
+ return void 0;
26770
+ return ref.replace(EMAIL_RE, (m) => `email#${USERREF_DIGEST_VERSION}:${createHmac("sha256", userRefHashKey()).update(m.toLowerCase()).digest("hex").slice(0, 32)}`);
26771
+ }
26772
+
26634
26773
  // ../../packages/agent-ledger/dist/esm/read/ledger-identity.js
26635
26774
  var CHANNEL_SOURCES = /* @__PURE__ */ new Set(["telegram", "slack"]);
26636
26775
  function resolveIdentity(source, userRef, members = []) {
26637
26776
  if (!userRef)
26638
26777
  return { source, ref: "unknown" };
26639
26778
  if (CHANNEL_SOURCES.has(source)) {
26640
- const m = members.find((x) => x.source === source && x.handle.toLowerCase() === userRef.toLowerCase());
26779
+ const m = members.find((x) => x.source === source && (x.handle.toLowerCase() === userRef.toLowerCase() || maskUserRef(x.handle.toLowerCase()) === userRef.toLowerCase()));
26641
26780
  if (m)
26642
26781
  return { source, ref: userRef, display: m.displayName };
26643
26782
  }
@@ -26655,7 +26794,7 @@ var str3 = (v) => {
26655
26794
  return "";
26656
26795
  };
26657
26796
  var req = (v, fallback = "") => str3(v) ?? fallback;
26658
- function parseArtifacts(v) {
26797
+ function parseArtifacts2(v) {
26659
26798
  if (typeof v !== "string" || v === "")
26660
26799
  return void 0;
26661
26800
  try {
@@ -26689,7 +26828,7 @@ function mapEntity(e) {
26689
26828
  delegationId: str3(e.delegationId),
26690
26829
  parentEventId: str3(e.parentEventId),
26691
26830
  depth: e.depth == null ? void 0 : Number(e.depth),
26692
- artifacts: parseArtifacts(e.artifacts)
26831
+ artifacts: parseArtifacts2(e.artifacts)
26693
26832
  };
26694
26833
  }
26695
26834
  var rank = (o) => o === "ok" ? 0 : 1;
@@ -26947,6 +27086,10 @@ function makeRowKey(timestampIso, eventId) {
26947
27086
  return `${String(reverse).padStart(19, "0")}_${eventId}`;
26948
27087
  }
26949
27088
 
27089
+ // ../../packages/agent-ledger/dist/esm/entity.js
27090
+ var SNIPPET_MAX_LEN = 256;
27091
+ var PII_SCAN_WINDOW = SNIPPET_MAX_LEN * 16;
27092
+
26950
27093
  // ../../packages/brain/engine/dist/esm/cursor.js
26951
27094
  var CURSOR_PARTITION_KEY = "dreamcursor";
26952
27095
  function isAbsentCursorError(e) {
@@ -27175,7 +27318,7 @@ function mapItems(raw) {
27175
27318
  continue;
27176
27319
  if (item.role !== "user" && item.role !== "assistant")
27177
27320
  continue;
27178
- const text = Array.isArray(item.content) ? stripVoiceEnvelope(item.content.map((c) => typeof c.text === "string" && c.text.length > 0 ? c.text : typeof c.transcript === "string" && c.transcript.length > 0 ? c.transcript : "").filter(Boolean).join("\n\n")) : "";
27321
+ const text = Array.isArray(item.content) ? maskSecrets(stripVoiceEnvelope(item.content.map((c) => typeof c.text === "string" && c.text.length > 0 ? c.text : typeof c.transcript === "string" && c.transcript.length > 0 ? c.transcript : "").filter(Boolean).join("\n\n"))) : "";
27179
27322
  const at = typeof item.created_at === "number" ? new Date(item.created_at * 1e3).toISOString() : void 0;
27180
27323
  acc.push(at ? { role: item.role, text, at } : { role: item.role, text });
27181
27324
  }
@@ -27460,7 +27603,9 @@ async function runPipeline(args) {
27460
27603
  const grpRows = rowsByGroupKey.get(groupKeyOf(g)) ?? [];
27461
27604
  (consumedRowsByPk[pk] ??= []).push(...grpRows);
27462
27605
  const provRows = grpRows.map((r) => ({ rowKey: rowStorageKey(r), eventTimestamp: r.eventTimestamp }));
27463
- const conv = normalize(ng, items, enrichment);
27606
+ const trustedOwner = g.source !== "a2a" && g.user.ref !== "unknown" && !g.user.ref.startsWith("svc:");
27607
+ const dreamItems = trustedOwner ? items : items.map((i) => ({ ...i, text: maskContactStrict(i.text) }));
27608
+ const conv = normalize(ng, dreamItems, enrichment);
27464
27609
  conv.provenance = { pk, rows: provRows };
27465
27610
  conversations.push(conv);
27466
27611
  if (outcome === "consumed")
@@ -29217,14 +29362,278 @@ function defaultDeps(overrides) {
29217
29362
  return deps;
29218
29363
  }
29219
29364
 
29220
- // src/commands/foundry/create.ts
29365
+ // src/commands/conversations/sweep.ts
29366
+ import { createHash as createHash4 } from "crypto";
29221
29367
  import { Command as Command53, Option as Option50 } from "clipanion";
29368
+ import { AzureCliCredential as AzureCliCredential2 } from "@azure/identity";
29369
+ import { TableClient as TableClient8 } from "@azure/data-tables";
29370
+ import { AIProjectClient as AIProjectClient4 } from "@azure/ai-projects";
29371
+ init_errors();
29372
+ var LEDGER_TABLE_NAME2 = "AgentLedger";
29373
+ var KEY_LIFETIME_DAYS = 30;
29374
+ var DAY_MS = 24 * 60 * 60 * 1e3;
29375
+ function ownerDigest(nativeUserKey) {
29376
+ return createHash4("sha256").update(nativeUserKey).digest("base64url");
29377
+ }
29378
+ function svcPrincipal(svcRef) {
29379
+ return svcRef.split(":")[1] ?? "";
29380
+ }
29381
+ function svcConversationFacts(rows) {
29382
+ const facts = /* @__PURE__ */ new Map();
29383
+ for (const r of rows) {
29384
+ if (!r.foundryConversationId) continue;
29385
+ if (!r.userRef?.startsWith("svc:")) continue;
29386
+ const prev = facts.get(r.foundryConversationId);
29387
+ if (!prev || r.eventTimestamp > prev.lastSeen) {
29388
+ facts.set(r.foundryConversationId, {
29389
+ conversationId: r.foundryConversationId,
29390
+ svcRef: r.userRef,
29391
+ principal: svcPrincipal(r.userRef),
29392
+ agent: r.agentName,
29393
+ lastSeen: r.eventTimestamp
29394
+ });
29395
+ }
29396
+ }
29397
+ return facts;
29398
+ }
29399
+ function selectStaleCandidates(facts, cutoffIso) {
29400
+ return [...facts.values()].filter((f) => f.lastSeen < cutoffIso).sort((a, b) => a.lastSeen < b.lastSeen ? -1 : 1);
29401
+ }
29402
+ function confirmCandidate(fact, meta, newestItemAt, cutoffIso) {
29403
+ if (meta.app !== "m8t-webapp") return { ok: false, reason: `unexpected app tag '${meta.app ?? ""}'` };
29404
+ const expectedOwner = `web:${ownerDigest(fact.svcRef)}`;
29405
+ if (meta.ownerKey !== expectedOwner) return { ok: false, reason: "owner mismatch" };
29406
+ if (newestItemAt === null) return { ok: false, reason: "no items to prove inactivity" };
29407
+ if (newestItemAt >= cutoffIso) return { ok: false, reason: "recent activity" };
29408
+ return { ok: true };
29409
+ }
29410
+ function statusOf2(e) {
29411
+ const err = e;
29412
+ return err?.statusCode ?? err?.status;
29413
+ }
29414
+ function defaultDeps2() {
29415
+ const credential2 = new AzureCliCredential2();
29416
+ const openaiFor = (ctx) => new AIProjectClient4(ctx.projectEndpoint, credential2).getOpenAIClient();
29417
+ return {
29418
+ async resolveContext(opts) {
29419
+ const account = await getAzAccount();
29420
+ const subscriptionId = opts.subscription ?? account.subscriptionId;
29421
+ const project = await resolveFoundryProject({
29422
+ credential: credential2,
29423
+ subscriptionId,
29424
+ interactive: false,
29425
+ endpoint: opts.endpoint
29426
+ });
29427
+ const m = /\/resourceGroups\/([^/]+)/i.exec(project.accountScope);
29428
+ if (!m) throw new LocalCliError({ code: "sweep_context", message: `Could not parse resource group from '${project.accountScope}'.` });
29429
+ const { ledgerTableEndpoint } = await discoverLedgerResources({
29430
+ credential: credential2,
29431
+ subscriptionId,
29432
+ resourceGroup: m[1]
29433
+ });
29434
+ return { projectEndpoint: project.endpoint, ledgerTableEndpoint };
29435
+ },
29436
+ async fetchRows(ctx, fromIso, toIso) {
29437
+ const client = new TableClient8(ctx.ledgerTableEndpoint, LEDGER_TABLE_NAME2, credential2);
29438
+ return fetchLedgerRows({ workers: [], source: "all", from: fromIso, to: toIso }, client);
29439
+ },
29440
+ async retrieveMetadata(ctx, conversationId) {
29441
+ try {
29442
+ const conv = await openaiFor(ctx).conversations.retrieve(conversationId);
29443
+ const md = conv.metadata;
29444
+ const s = (v) => typeof v === "string" ? v : void 0;
29445
+ if (!md || typeof md !== "object") return {};
29446
+ return { app: s(md.app), ownerKey: s(md.ownerKey), platform: s(md.platform) };
29447
+ } catch (e) {
29448
+ if (statusOf2(e) === 404) return "gone";
29449
+ throw e;
29450
+ }
29451
+ },
29452
+ async newestItemAt(ctx, conversationId) {
29453
+ try {
29454
+ for await (const item of openaiFor(ctx).conversations.items.list(conversationId)) {
29455
+ const created = item.created_at;
29456
+ if (typeof created !== "number") return { at: null };
29457
+ return { at: new Date(created * 1e3).toISOString() };
29458
+ }
29459
+ return { at: null };
29460
+ } catch (e) {
29461
+ if (statusOf2(e) === 404) return "gone";
29462
+ throw e;
29463
+ }
29464
+ },
29465
+ async deleteConversation(ctx, conversationId) {
29466
+ try {
29467
+ await openaiFor(ctx).conversations.delete(conversationId);
29468
+ return "deleted";
29469
+ } catch (e) {
29470
+ if (statusOf2(e) === 404) return "already-gone";
29471
+ throw e;
29472
+ }
29473
+ }
29474
+ };
29475
+ }
29476
+ var ConversationsSweepCommand = class extends M8tCommand {
29477
+ static paths = [["conversations", "sweep"]];
29478
+ static usage = Command53.Usage({
29479
+ category: "Conversations",
29480
+ description: "Delete expired public-visitor conversations (dry run by default)",
29481
+ details: `
29482
+ Finds conversations owned by delegated end users (public visitors) whose
29483
+ last activity predates the retention window (${String(KEY_LIFETIME_DAYS)}-day key life + grace),
29484
+ confirms each against its own metadata and newest item, and \u2014 only with
29485
+ --delete \u2014 permanently removes them. Team, channel, and delegated-work
29486
+ conversations are structurally out of scope. The masked ledger telemetry
29487
+ of swept conversations is kept.
29488
+ `,
29489
+ examples: [
29490
+ ["Report what would be deleted (nothing is)", "m8t conversations sweep"],
29491
+ ["Delete, capped at 50", "m8t conversations sweep --delete --max 50"]
29492
+ ]
29493
+ });
29494
+ doDelete = Option50.Boolean("--delete", false, {
29495
+ description: "Perform deletions (without this flag the command only reports)"
29496
+ });
29497
+ principal = Option50.String("--principal", {
29498
+ description: "Service-principal oid whose end-user keys are known-ephemeral (required with --delete; a dry run without it reports candidates grouped by principal)"
29499
+ });
29500
+ max = Option50.String("--max", "200", { description: "Maximum deletions per run" });
29501
+ graceDays = Option50.String("--grace-days", "14", {
29502
+ description: "Days past the 30-day key life before a conversation is eligible"
29503
+ });
29504
+ lookbackDays = Option50.String("--lookback-days", "180", {
29505
+ description: "How far back to scan ledger activity for candidates"
29506
+ });
29507
+ subscription = Option50.String("--subscription", { description: "Azure subscription id override" });
29508
+ endpoint = Option50.String("--endpoint", { description: "Foundry project endpoint override" });
29509
+ deps = defaultDeps2();
29510
+ async executeCommand() {
29511
+ const out = this.context.stdout;
29512
+ const max = Number.parseInt(this.max, 10);
29513
+ const grace = Number.parseInt(this.graceDays, 10);
29514
+ const lookback = Number.parseInt(this.lookbackDays, 10);
29515
+ if (!Number.isFinite(max) || max < 1) throw new LocalCliError({ code: "sweep_args", message: "--max must be a positive integer." });
29516
+ if (!Number.isFinite(grace) || grace < 0) throw new LocalCliError({ code: "sweep_args", message: "--grace-days must be a non-negative integer." });
29517
+ const retentionDays = KEY_LIFETIME_DAYS + grace;
29518
+ if (!Number.isFinite(lookback) || lookback <= retentionDays) {
29519
+ throw new LocalCliError({ code: "sweep_args", message: `--lookback-days must exceed the retention window (${String(retentionDays)} days).` });
29520
+ }
29521
+ const now = Date.now();
29522
+ const cutoffIso = new Date(now - retentionDays * DAY_MS).toISOString();
29523
+ const fromIso = new Date(now - lookback * DAY_MS).toISOString();
29524
+ const toIso = new Date(now).toISOString();
29525
+ const ctx = await this.deps.resolveContext({ subscription: this.subscription, endpoint: this.endpoint });
29526
+ out.write(`Scanning ledger activity ${fromIso} \u2192 ${toIso}; retention cutoff ${cutoffIso}.
29527
+ `);
29528
+ const rows = await this.deps.fetchRows(ctx, fromIso, toIso);
29529
+ const facts = svcConversationFacts(rows);
29530
+ const stale = selectStaleCandidates(facts, cutoffIso);
29531
+ if (this.doDelete && !this.principal) {
29532
+ throw new LocalCliError({
29533
+ code: "sweep_principal_required",
29534
+ message: "--delete requires --principal <service-principal oid>.",
29535
+ hint: "Run without --delete first: the dry run lists candidates grouped by principal so you can identify the visitor-minting one."
29536
+ });
29537
+ }
29538
+ const inScope = this.principal ? stale.filter((f) => f.principal === this.principal) : stale;
29539
+ const outOfScope = stale.length - inScope.length;
29540
+ out.write(
29541
+ `${String(facts.size)} delegated conversation(s) seen in window; ${String(stale.length)} past cutoff` + (this.principal ? ` (${String(inScope.length)} for principal ${this.principal}, ${String(outOfScope)} other-principal ignored)` : "") + ".\n"
29542
+ );
29543
+ if (!this.principal && stale.length > 0) {
29544
+ const byPrincipal = /* @__PURE__ */ new Map();
29545
+ for (const f of stale) byPrincipal.set(f.principal, (byPrincipal.get(f.principal) ?? 0) + 1);
29546
+ out.write("Stale candidates by principal (pass --principal to scope deletion):\n");
29547
+ for (const [pr, n] of [...byPrincipal.entries()].sort((a, b) => b[1] - a[1])) {
29548
+ out.write(` ${pr || "(malformed)"} ${String(n)}
29549
+ `);
29550
+ }
29551
+ }
29552
+ out.write("\n");
29553
+ let attempts = 0;
29554
+ let deleted = 0;
29555
+ let alreadyGone = 0;
29556
+ let skipped = 0;
29557
+ let failures = 0;
29558
+ let remaining = 0;
29559
+ for (const fact of inScope) {
29560
+ if (attempts >= max) {
29561
+ remaining += 1;
29562
+ continue;
29563
+ }
29564
+ const ageDays = Math.floor((now - Date.parse(fact.lastSeen)) / DAY_MS);
29565
+ const label = `${fact.conversationId} agent=${fact.agent} last-active=${fact.lastSeen} (${String(ageDays)}d)`;
29566
+ let meta;
29567
+ let newest;
29568
+ try {
29569
+ meta = await this.deps.retrieveMetadata(ctx, fact.conversationId);
29570
+ newest = meta === "gone" ? "gone" : await this.deps.newestItemAt(ctx, fact.conversationId);
29571
+ } catch (e) {
29572
+ failures += 1;
29573
+ out.write(`${colors.error("failed")} ${label} \u2014 ${e.message}
29574
+ `);
29575
+ continue;
29576
+ }
29577
+ if (meta === "gone" || newest === "gone") {
29578
+ alreadyGone += 1;
29579
+ out.write(`${colors.dim("already gone")} ${label}
29580
+ `);
29581
+ continue;
29582
+ }
29583
+ const verdict = confirmCandidate(fact, meta, newest.at, cutoffIso);
29584
+ if (!verdict.ok) {
29585
+ skipped += 1;
29586
+ out.write(`${colors.warn("skip")} ${label} \u2014 ${verdict.reason}
29587
+ `);
29588
+ continue;
29589
+ }
29590
+ attempts += 1;
29591
+ if (!this.doDelete) {
29592
+ out.write(`${colors.field("would delete")} ${label}
29593
+ `);
29594
+ continue;
29595
+ }
29596
+ try {
29597
+ const result2 = await this.deps.deleteConversation(ctx, fact.conversationId);
29598
+ if (result2 === "deleted") {
29599
+ deleted += 1;
29600
+ out.write(`${colors.error("deleted")} ${label}
29601
+ `);
29602
+ } else {
29603
+ alreadyGone += 1;
29604
+ out.write(`${colors.dim("already gone")} ${label}
29605
+ `);
29606
+ }
29607
+ } catch (e) {
29608
+ failures += 1;
29609
+ out.write(`${colors.error("failed")} ${label} \u2014 ${e.message}
29610
+ `);
29611
+ }
29612
+ }
29613
+ out.write("\n");
29614
+ const tail = `already gone ${String(alreadyGone)}, skipped ${String(skipped)} (fail-closed), failed ${String(failures)}` + (remaining > 0 ? `, ${String(remaining)} past the cap \u2014 re-run to continue` : "") + ".\n";
29615
+ if (this.doDelete) {
29616
+ out.write(`Deleted ${String(deleted)}, ${tail}`);
29617
+ } else {
29618
+ out.write(`DRY RUN \u2014 nothing deleted. ${String(attempts)} conversation(s) would be deleted, ${tail}`);
29619
+ }
29620
+ if (failures > 0) {
29621
+ out.write(`${colors.error("Run incomplete:")} ${String(failures)} verification/deletion failure(s) \u2014 fix access and re-run.
29622
+ `);
29623
+ return 1;
29624
+ }
29625
+ return 0;
29626
+ }
29627
+ };
29628
+
29629
+ // src/commands/foundry/create.ts
29630
+ import { Command as Command54, Option as Option51 } from "clipanion";
29222
29631
 
29223
29632
  // src/lib/foundry-create.ts
29224
29633
  init_errors();
29225
- import { createHash as createHash4 } from "crypto";
29634
+ import { createHash as createHash5 } from "crypto";
29226
29635
  function deriveAccountName(subscriptionId) {
29227
- const h = createHash4("sha256").update(subscriptionId).digest("hex").slice(0, 12);
29636
+ const h = createHash5("sha256").update(subscriptionId).digest("hex").slice(0, 12);
29228
29637
  return `m8t${h}`;
29229
29638
  }
29230
29639
  function assertHostedRegion(location) {
@@ -29456,7 +29865,7 @@ async function createFoundryProject(args) {
29456
29865
  init_errors();
29457
29866
  var FoundryCreateCommand = class extends M8tCommand {
29458
29867
  static paths = [["foundry", "create"]];
29459
- static usage = Command53.Usage({
29868
+ static usage = Command54.Usage({
29460
29869
  description: "Create an AI Foundry (AIServices) account + project + model deployment from scratch.",
29461
29870
  details: "Non-interactive and idempotent. Creates the AIServices account (custom subdomain + project management), a project, and a model deployment (default gpt-4.1-mini @ capacity 50). Region must be hosted-agent-eligible. Emits the project endpoint as structured output. Re-run is a clean no-op (account/project skipped if present; deployment capacity converges UP, never down).",
29462
29871
  examples: [
@@ -29465,16 +29874,16 @@ var FoundryCreateCommand = class extends M8tCommand {
29465
29874
  ["Higher capacity for a reasoning model", "$0 foundry create --resource-group rg-m8t-stack --location eastus2 --model gpt-5-mini --capacity 250"]
29466
29875
  ]
29467
29876
  });
29468
- resourceGroup = Option50.String("--resource-group");
29469
- location = Option50.String("--location");
29470
- account = Option50.String("--account");
29471
- project = Option50.String("--project", "m8t");
29472
- model = Option50.String("--model", "gpt-4.1-mini");
29473
- modelVersion = Option50.String("--model-version", "2025-04-14");
29474
- capacity = Option50.String("--capacity", "50");
29475
- subscription = Option50.String("--subscription");
29476
- skipQuotaCheck = Option50.Boolean("--skip-quota-check", false);
29477
- output = Option50.String("--output");
29877
+ resourceGroup = Option51.String("--resource-group");
29878
+ location = Option51.String("--location");
29879
+ account = Option51.String("--account");
29880
+ project = Option51.String("--project", "m8t");
29881
+ model = Option51.String("--model", "gpt-4.1-mini");
29882
+ modelVersion = Option51.String("--model-version", "2025-04-14");
29883
+ capacity = Option51.String("--capacity", "50");
29884
+ subscription = Option51.String("--subscription");
29885
+ skipQuotaCheck = Option51.Boolean("--skip-quota-check", false);
29886
+ output = Option51.String("--output");
29478
29887
  async executeCommand() {
29479
29888
  const mode = resolveOutputMode(
29480
29889
  this.output,
@@ -29546,22 +29955,22 @@ var FoundryCreateCommand = class extends M8tCommand {
29546
29955
  };
29547
29956
 
29548
29957
  // src/commands/foundry/await-ready.ts
29549
- import { Command as Command54, Option as Option51 } from "clipanion";
29550
- import { AzureCliCredential as AzureCliCredential2 } from "@azure/identity";
29958
+ import { Command as Command55, Option as Option52 } from "clipanion";
29959
+ import { AzureCliCredential as AzureCliCredential3 } from "@azure/identity";
29551
29960
  init_errors();
29552
29961
  var FoundryAwaitReadyCommand = class extends M8tCommand {
29553
29962
  static paths = [["foundry", "await-ready"]];
29554
- static usage = Command54.Usage({
29963
+ static usage = Command55.Usage({
29555
29964
  description: "Wait until a freshly-created Foundry project's data plane reliably serves it.",
29556
29965
  details: "Probes the project (GET /agents) until it returns 200 on a few consecutive tries, or fails clearly after a bounded budget. A newly-created account can serve intermittent 404 'Project not found' for minutes; run this after 'foundry create' and before deploying agents so the worker phase doesn't catch the unstable window.",
29557
29966
  examples: [["Wait for a project to be ready", "$0 foundry await-ready --endpoint https://acc.services.ai.azure.com/api/projects/m8t"]]
29558
29967
  });
29559
- endpoint = Option51.String("--endpoint");
29560
- consecutive = Option51.String("--consecutive", "3");
29561
- attempts = Option51.String("--attempts", "60");
29562
- interval = Option51.String("--interval", "5");
29563
- subscription = Option51.String("--subscription");
29564
- output = Option51.String("--output");
29968
+ endpoint = Option52.String("--endpoint");
29969
+ consecutive = Option52.String("--consecutive", "3");
29970
+ attempts = Option52.String("--attempts", "60");
29971
+ interval = Option52.String("--interval", "5");
29972
+ subscription = Option52.String("--subscription");
29973
+ output = Option52.String("--output");
29565
29974
  async executeCommand() {
29566
29975
  const mode = resolveOutputMode(this.output, this.context.stdout);
29567
29976
  const endpoint = typeof this.endpoint === "string" ? this.endpoint : void 0;
@@ -29572,7 +29981,7 @@ var FoundryAwaitReadyCommand = class extends M8tCommand {
29572
29981
  const attempts = typeof this.attempts === "string" ? Number(this.attempts) : 60;
29573
29982
  const intervalMs = (typeof this.interval === "string" ? Number(this.interval) : 5) * 1e3;
29574
29983
  await getAzAccount();
29575
- const credential2 = new AzureCliCredential2();
29984
+ const credential2 = new AzureCliCredential3();
29576
29985
  const probe = makeAgentsProbe(credential2, endpoint);
29577
29986
  const onProgress = mode === "pretty" ? (m) => this.context.stderr.write(` ${colors.dim(m)}
29578
29987
  `) : void 0;
@@ -29595,7 +30004,7 @@ var FoundryAwaitReadyCommand = class extends M8tCommand {
29595
30004
  };
29596
30005
 
29597
30006
  // src/commands/bootstrap/preflight.ts
29598
- import { Command as Command55, Option as Option52 } from "clipanion";
30007
+ import { Command as Command56, Option as Option53 } from "clipanion";
29599
30008
 
29600
30009
  // ../../packages/telemetry-contract/artifact/tier-map.ts
29601
30010
  var EVENT_TIERS = {
@@ -29647,7 +30056,7 @@ function preflightRenderable(results) {
29647
30056
  }
29648
30057
  var BootstrapPreflightCommand = class extends M8tCommand {
29649
30058
  static paths = [["bootstrap", "preflight"]];
29650
- static usage = Command55.Usage({
30059
+ static usage = Command56.Usage({
29651
30060
  description: "Loudly verify you can install m8t (Owner/UAA + directory admin) and hard-stop if not.",
29652
30061
  details: "Step 1 of `m8t bootstrap`. Prints an unmissable admin-credentials notice, then checks: Owner or User Access Administrator at subscription scope (hard requirement), directory-admin capability to register the app (or pass --client-id), every Azure resource provider the install uses (registering any that are missing), soft-deleted Cognitive Services accounts (they keep their model quota until purged, so quota can read free and the model deployment still fail), and \u2014 with --location \u2014 model quota in the target region. Exits non-zero with the exact failing check + remedy. `m8t prereqs` runs the same substrate checks on their own.",
29653
30062
  examples: [
@@ -29656,9 +30065,9 @@ var BootstrapPreflightCommand = class extends M8tCommand {
29656
30065
  ["BYO app registration (directory guests)", "$0 bootstrap preflight --client-id <appId>"]
29657
30066
  ]
29658
30067
  });
29659
- clientId = Option52.String("--client-id");
29660
- subscription = Option52.String("--subscription");
29661
- location = Option52.String("--location", {
30068
+ clientId = Option53.String("--client-id");
30069
+ subscription = Option53.String("--subscription");
30070
+ location = Option53.String("--location", {
29662
30071
  description: "Target region. Enables the model-quota check \u2014 without it, quota is not verified."
29663
30072
  });
29664
30073
  async executeCommand() {
@@ -29758,7 +30167,7 @@ ${colors.error(" " + why)}
29758
30167
  import * as fs35 from "fs";
29759
30168
  import * as os15 from "os";
29760
30169
  import * as path38 from "path";
29761
- import { Command as Command56, Option as Option53 } from "clipanion";
30170
+ import { Command as Command57, Option as Option54 } from "clipanion";
29762
30171
  init_errors();
29763
30172
 
29764
30173
  // src/lib/bootstrap-mi.ts
@@ -29913,7 +30322,7 @@ async function kickInstaller(s) {
29913
30322
  }
29914
30323
 
29915
30324
  // src/lib/bootstrap-status.ts
29916
- import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
30325
+ import { createHash as createHash6, randomUUID as randomUUID2 } from "crypto";
29917
30326
  import * as fs33 from "fs/promises";
29918
30327
  import * as os13 from "os";
29919
30328
  import * as path36 from "path";
@@ -29921,7 +30330,7 @@ init_errors();
29921
30330
  var STATUS_CONTAINER = "status";
29922
30331
  var STATUS_BLOB = "status.json";
29923
30332
  function deriveStatusSaName(resourceGroup, subscriptionId) {
29924
- const hex = createHash5("sha256").update(resourceGroup + subscriptionId).digest("hex").slice(0, 12);
30333
+ const hex = createHash6("sha256").update(resourceGroup + subscriptionId).digest("hex").slice(0, 12);
29925
30334
  return `m8tinst${hex}`;
29926
30335
  }
29927
30336
  function statusBlobUrl(saName) {
@@ -30128,7 +30537,7 @@ var ACI_NAME = "m8t-installer";
30128
30537
  var MI_NAME = "m8t-installer-mi";
30129
30538
  var BootstrapLaunchCommand = class extends M8tCommand {
30130
30539
  static paths = [["bootstrap", "launch"]];
30131
- static usage = Command56.Usage({
30540
+ static usage = Command57.Usage({
30132
30541
  description: "Create + authorize the installer managed identity, then kick the cloud installer.",
30133
30542
  details: "Step 2 of `m8t bootstrap` (run after `preflight`). Ensures the resource group, creates the m8t app registration (or uses --client-id), creates a user-assigned managed identity granted Owner at subscription scope, and launches the published m8t-installer image as an ACI run-to-completion job under that identity. Writes ~/.m8t/bootstrap.json for `status` and `reap`, and threads your Azure object id so the installer can grant you the platform's data-plane roles itself.",
30134
30543
  examples: [
@@ -30139,26 +30548,26 @@ var BootstrapLaunchCommand = class extends M8tCommand {
30139
30548
  ["Share a support contact", "$0 bootstrap launch --location eastus2 --contact-email you@example.com --company 'Acme'"]
30140
30549
  ]
30141
30550
  });
30142
- location = Option53.String("--location");
30143
- resourceGroup = Option53.String("--resource-group");
30144
- clientId = Option53.String("--client-id");
30145
- subscription = Option53.String("--subscription");
30146
- installerTag = Option53.String("--installer-tag");
30551
+ location = Option54.String("--location");
30552
+ resourceGroup = Option54.String("--resource-group");
30553
+ clientId = Option54.String("--client-id");
30554
+ subscription = Option54.String("--subscription");
30555
+ installerTag = Option54.String("--installer-tag");
30147
30556
  // Full image ref override (registry + repo + tag) — an escape hatch when the
30148
30557
  // default org/tag is wrong for the current CLI (e.g. a stale published build).
30149
30558
  // Wins over --installer-tag / the pinned default.
30150
- installerImage = Option53.String("--installer-image");
30151
- gatewayImageRef = Option53.String("--gateway-image-ref");
30152
- githubAppCreds = Option53.String("--github-app-creds");
30153
- contactEmail = Option53.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
30154
- company = Option53.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
30559
+ installerImage = Option54.String("--installer-image");
30560
+ gatewayImageRef = Option54.String("--gateway-image-ref");
30561
+ githubAppCreds = Option54.String("--github-app-creds");
30562
+ contactEmail = Option54.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
30563
+ company = Option54.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
30155
30564
  // Value-carrying on purpose: a bare --force would be cargo-culted into
30156
30565
  // runbooks and harness prompts and erode the protection, whereas a faithful
30157
30566
  // paste can never accidentally carry the victim group's name. It AUTHORIZES
30158
30567
  // the target; --resource-group is what CHOOSES it.
30159
- reinstallInto = Option53.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
30160
- org = Option53.String("--org", { description: "Assert the GitHub App on disk is installed on this org; refuses on a mismatch." });
30161
- noBrains = Option53.Boolean("--no-brains", false, { description: "Install without brain-backed workers. For test and CI rigs; the supported install creates brains." });
30568
+ reinstallInto = Option54.String("--reinstall-into", { description: "Consent to installing into --resource-group even though it already holds resources. Must match the target group's name." });
30569
+ org = Option54.String("--org", { description: "Assert the GitHub App on disk is installed on this org; refuses on a mismatch." });
30570
+ noBrains = Option54.Boolean("--no-brains", false, { description: "Install without brain-backed workers. For test and CI rigs; the supported install creates brains." });
30162
30571
  async executeCommand() {
30163
30572
  const location = typeof this.location === "string" ? this.location : void 0;
30164
30573
  if (!location) {
@@ -30319,7 +30728,7 @@ var BootstrapLaunchCommand = class extends M8tCommand {
30319
30728
  };
30320
30729
 
30321
30730
  // src/commands/bootstrap/status.ts
30322
- import { Command as Command58, Option as Option55 } from "clipanion";
30731
+ import { Command as Command59, Option as Option56 } from "clipanion";
30323
30732
  init_errors();
30324
30733
 
30325
30734
  // src/lib/bootstrap-aci-state.ts
@@ -30405,7 +30814,7 @@ var ONBOARDING_BLOCK_KEYS = [
30405
30814
  "advisor_name",
30406
30815
  "advisor_email"
30407
30816
  ];
30408
- function isRecord(value) {
30817
+ function isRecord2(value) {
30409
30818
  return typeof value === "object" && value !== null && !Array.isArray(value);
30410
30819
  }
30411
30820
  function hasValidUniqueJsonKeys(source) {
@@ -30513,7 +30922,7 @@ var V3_COMPANY_KEYS = ["name", "one_liner"];
30513
30922
  var V3_ADDRESS_KEYS = ["address", "city", "postal_code", "country"];
30514
30923
  var V3_REQUEST_KEYS = ["type", "model", "region", "consent", "company_address"];
30515
30924
  function exactStringRecord(value, keys) {
30516
- if (!isRecord(value)) return "non-string-value";
30925
+ if (!isRecord2(value)) return "non-string-value";
30517
30926
  const present = new Set(Object.keys(value));
30518
30927
  for (const key2 of keys) if (!present.has(key2)) return "missing-key";
30519
30928
  if (present.size !== keys.length) return "unexpected-key";
@@ -30525,7 +30934,7 @@ function canonicalPendingRequests(value) {
30525
30934
  if (value.length > 1) return { ok: false, reason: "too-many-pending-requests" };
30526
30935
  const requests = [];
30527
30936
  for (const entry of value) {
30528
- if (!isRecord(entry)) return { ok: false, reason: "non-string-value" };
30937
+ if (!isRecord2(entry)) return { ok: false, reason: "non-string-value" };
30529
30938
  const present = new Set(Object.keys(entry));
30530
30939
  for (const key2 of V3_REQUEST_KEYS) {
30531
30940
  if (!present.has(key2)) return { ok: false, reason: "missing-key" };
@@ -30575,7 +30984,7 @@ function canonicalV3Block(value) {
30575
30984
  };
30576
30985
  }
30577
30986
  function canonicalBlock(value) {
30578
- if (!isRecord(value)) return { ok: false, reason: "non-string-value" };
30987
+ if (!isRecord2(value)) return { ok: false, reason: "non-string-value" };
30579
30988
  if (value.schema_version === "3") return canonicalV3Block(value);
30580
30989
  if (value.schema_version !== "2") return { ok: false, reason: "unknown-schema-version" };
30581
30990
  const keys = new Set(Object.keys(value));
@@ -30620,7 +31029,7 @@ function parseOnboardingArtifactResult(machineText) {
30620
31029
  if (json.includes("m8t_onboarding")) return { ok: false, reason: "malformed-json" };
30621
31030
  continue;
30622
31031
  }
30623
- if (!isRecord(parsed) || !Object.hasOwn(parsed, "m8t_onboarding")) continue;
31032
+ if (!isRecord2(parsed) || !Object.hasOwn(parsed, "m8t_onboarding")) continue;
30624
31033
  if (Object.keys(parsed).length !== 1) return { ok: false, reason: "unexpected-key" };
30625
31034
  const outcome = canonicalBlock(parsed.m8t_onboarding);
30626
31035
  if (!outcome.ok) return outcome;
@@ -30666,7 +31075,7 @@ async function readCursorPages(args) {
30666
31075
  } catch {
30667
31076
  return null;
30668
31077
  }
30669
- if (!isRecord(body) || !Array.isArray(body.data)) return null;
31078
+ if (!isRecord2(body) || !Array.isArray(body.data)) return null;
30670
31079
  const page = body;
30671
31080
  all.push(...page.data);
30672
31081
  if (page.has_more !== true) {
@@ -30687,7 +31096,7 @@ function decodeFoundryUser(token) {
30687
31096
  if (parts.length !== 3 || parts.some((part) => part.length === 0) || !/^[A-Za-z0-9_-]+$/.test(parts[1] ?? "")) return null;
30688
31097
  try {
30689
31098
  const payload = JSON.parse(Buffer.from(parts[1] ?? "", "base64url").toString("utf8"));
30690
- if (!isRecord(payload)) return null;
31099
+ if (!isRecord2(payload)) return null;
30691
31100
  const claim = payload.upn ?? payload.preferred_username ?? payload.email;
30692
31101
  return typeof claim === "string" && claim.trim().length > 0 ? claim : null;
30693
31102
  } catch {
@@ -30718,7 +31127,7 @@ async function findOnboardingProfile(args) {
30718
31127
  });
30719
31128
  if (!listed) return { ...EMPTY_PROFILE_RESULT };
30720
31129
  const conversations = orderNewest(listed.flatMap((value, ordinal) => {
30721
- if (!isRecord(value) || typeof value.id !== "string" || value.id.length === 0 || !isRecord(value.metadata)) return [];
31130
+ if (!isRecord2(value) || typeof value.id !== "string" || value.id.length === 0 || !isRecord2(value.metadata)) return [];
30722
31131
  const metadata = value.metadata;
30723
31132
  if (metadata.app !== "m8t-webapp" || metadata.agent !== INTAKE_AGENT_NAME) return [];
30724
31133
  return [{
@@ -30736,7 +31145,7 @@ async function findOnboardingProfile(args) {
30736
31145
  });
30737
31146
  if (!items) return { hadIntake: true, block: null, machineText: null, speechText: null, rejection: null };
30738
31147
  const assistantItems = orderNewest(items.flatMap((value, ordinal) => {
30739
- if (!isRecord(value) || value.type !== "message" || value.role !== "assistant" || !Array.isArray(value.content)) return [];
31148
+ if (!isRecord2(value) || value.type !== "message" || value.role !== "assistant" || !Array.isArray(value.content)) return [];
30740
31149
  const id = typeof value.id === "string" ? value.id : "";
30741
31150
  return [{ value, id, createdAt: timestamp(value.created_at), ordinal }];
30742
31151
  }));
@@ -30745,7 +31154,7 @@ async function findOnboardingProfile(args) {
30745
31154
  for (const item of assistantItems) {
30746
31155
  const content = item.value.content;
30747
31156
  const machineText = content.flatMap((part) => {
30748
- if (!isRecord(part)) return [];
31157
+ if (!isRecord2(part)) return [];
30749
31158
  if (typeof part.text === "string" && part.text.length > 0) return [part.text];
30750
31159
  if (typeof part.transcript === "string") return [part.transcript];
30751
31160
  return [];
@@ -31323,7 +31732,7 @@ import { randomUUID as randomUUID3 } from "crypto";
31323
31732
  import { execFile, spawn as spawn7 } from "child_process";
31324
31733
 
31325
31734
  // src/lib/companion-artifact.ts
31326
- import { createHash as createHash6 } from "crypto";
31735
+ import { createHash as createHash7 } from "crypto";
31327
31736
  import { constants } from "fs";
31328
31737
  import * as fs36 from "fs/promises";
31329
31738
  import * as path40 from "path";
@@ -31361,7 +31770,7 @@ function canonicalEntry(entry) {
31361
31770
  `;
31362
31771
  }
31363
31772
  function artifactTreeSha256(entries) {
31364
- const hash = createHash6("sha256");
31773
+ const hash = createHash7("sha256");
31365
31774
  const ordered = [...entries].sort(
31366
31775
  (left, right) => left.path.localeCompare(right.path)
31367
31776
  );
@@ -31431,7 +31840,7 @@ function parseArtifactManifest(value) {
31431
31840
  };
31432
31841
  }
31433
31842
  async function sha256File2(filePath) {
31434
- return createHash6("sha256").update(await fs36.readFile(filePath)).digest("hex");
31843
+ return createHash7("sha256").update(await fs36.readFile(filePath)).digest("hex");
31435
31844
  }
31436
31845
  async function walk2(root, relative4 = "") {
31437
31846
  const directory = path40.join(root, ...relative4.split("/").filter(Boolean));
@@ -32247,7 +32656,7 @@ async function uninstallCompanion(options) {
32247
32656
  // src/commands/companion/install.ts
32248
32657
  import * as os18 from "os";
32249
32658
  import * as path43 from "path";
32250
- import { Command as Command57, Option as Option54 } from "clipanion";
32659
+ import { Command as Command58, Option as Option55 } from "clipanion";
32251
32660
 
32252
32661
  // src/lib/companion-channel.ts
32253
32662
  async function readCompanionRelease(source = { url: CHANNEL_LATEST_URL }, deps = {}) {
@@ -32262,7 +32671,7 @@ async function readCompanionRelease(source = { url: CHANNEL_LATEST_URL }, deps =
32262
32671
  }
32263
32672
 
32264
32673
  // src/lib/companion-download.ts
32265
- import { createHash as createHash7 } from "crypto";
32674
+ import { createHash as createHash8 } from "crypto";
32266
32675
  import { execFile as execFile2 } from "child_process";
32267
32676
  import * as fs38 from "fs/promises";
32268
32677
  import * as os17 from "os";
@@ -32311,7 +32720,7 @@ async function downloadCompanionArtifact(component, deps = {}) {
32311
32720
  message: `${pinned.asset} is larger than this command will accept.`
32312
32721
  });
32313
32722
  }
32314
- const digest = createHash7("sha256").update(bytes).digest("hex");
32723
+ const digest = createHash8("sha256").update(bytes).digest("hex");
32315
32724
  if (digest !== pinned.sha256) {
32316
32725
  throw new LocalCliError({
32317
32726
  code: "COMPANION_ASSET_DIGEST_MISMATCH",
@@ -32405,7 +32814,7 @@ Version: ${state.version}
32405
32814
  }
32406
32815
  var CompanionInstallCommand = class extends M8tCommand {
32407
32816
  static paths = [["companion", "install"]];
32408
- static usage = Command57.Usage({
32817
+ static usage = Command58.Usage({
32409
32818
  description: "Install the desktop companions for this user from the release channel.",
32410
32819
  examples: [
32411
32820
  ["Install the released build", "$0 companion install"],
@@ -32415,10 +32824,10 @@ var CompanionInstallCommand = class extends M8tCommand {
32415
32824
  ]
32416
32825
  ]
32417
32826
  });
32418
- from = Option54.String("--from", {
32827
+ from = Option55.String("--from", {
32419
32828
  description: "A locally staged build directory instead of the released one."
32420
32829
  });
32421
- resourceGroup = Option54.String("--resource-group", {
32830
+ resourceGroup = Option55.String("--resource-group", {
32422
32831
  description: "Which deployment to bind to, when the subscription holds more than one."
32423
32832
  });
32424
32833
  async executeCommand() {
@@ -32446,7 +32855,7 @@ async function looksLikeCheckout(dir) {
32446
32855
  return false;
32447
32856
  }
32448
32857
  }
32449
- var defaultDeps2 = {
32858
+ var defaultDeps3 = {
32450
32859
  discoverGateway,
32451
32860
  writeConfig,
32452
32861
  ensureGatewayRedirectUri,
@@ -32459,7 +32868,7 @@ var defaultDeps2 = {
32459
32868
  }),
32460
32869
  homedir: () => os19.homedir()
32461
32870
  };
32462
- async function finalizeInstall(args, deps = defaultDeps2) {
32871
+ async function finalizeInstall(args, deps = defaultDeps3) {
32463
32872
  const markerDir = path44.join(deps.homedir(), ".m8t");
32464
32873
  const markerPath = path44.join(markerDir, "repo-root");
32465
32874
  const cwd = process.cwd();
@@ -32634,7 +33043,7 @@ var BootstrapStatusCommand = class extends M8tCommand {
32634
33043
  // runbooks and shakedown recipes that a founder may already be part-way
32635
33044
  // through — it prints a deprecation notice and does the right thing.
32636
33045
  static paths = [["bootstrap", "status"], ["bootstrap", "finish"]];
32637
- static usage = Command58.Usage({
33046
+ static usage = Command59.Usage({
32638
33047
  description: "Show the cloud installer's live status (phase, progress, result) \u2014 and finish the local setup when it lands.",
32639
33048
  details: "Reads the durable status blob written by the installer. --watch polls until the install reaches done or failed.\n\nOn reaching done under --watch this also completes the local half of the install, which nothing else in the bootstrap path can do: it registers the webapp's sign-in redirect URI (the installer runs as a managed identity with no directory role, and at launch time the gateway FQDN did not exist yet), writes ~/.m8t/repo-root and the gateway discovery cache, and seeds your advisors' brains from the onboarding intake. Re-runnable \u2014 `m8t bootstrap finish` is a deprecated alias that does exactly this on an already-done install.",
32640
33049
  examples: [
@@ -32643,12 +33052,12 @@ var BootstrapStatusCommand = class extends M8tCommand {
32643
33052
  ["Redo the local setup on a finished install", "$0 bootstrap status --finalize --repo-root /path/to/m8t"]
32644
33053
  ]
32645
33054
  });
32646
- watch = Option55.Boolean("--watch", false);
32647
- output = Option55.String("--output");
32648
- repoRoot = Option55.String("--repo-root", { description: "The m8t clone to point local tools at (default: the current directory)." });
32649
- finalize = Option55.Boolean("--finalize", false, { description: "Complete the local setup against an already-done install, without watching." });
32650
- subscription = Option55.String("--subscription");
32651
- resourceGroup = Option55.String("--resource-group");
33055
+ watch = Option56.Boolean("--watch", false);
33056
+ output = Option56.String("--output");
33057
+ repoRoot = Option56.String("--repo-root", { description: "The m8t clone to point local tools at (default: the current directory)." });
33058
+ finalize = Option56.Boolean("--finalize", false, { description: "Complete the local setup against an already-done install, without watching." });
33059
+ subscription = Option56.String("--subscription");
33060
+ resourceGroup = Option56.String("--resource-group");
32652
33061
  async executeCommand() {
32653
33062
  const state = await readBootstrapState();
32654
33063
  if (!state) {
@@ -32781,7 +33190,7 @@ function formatStatus(d) {
32781
33190
  }
32782
33191
 
32783
33192
  // src/commands/bootstrap/reap.ts
32784
- import { Command as Command59, Option as Option56 } from "clipanion";
33193
+ import { Command as Command60, Option as Option57 } from "clipanion";
32785
33194
  init_errors();
32786
33195
 
32787
33196
  // src/lib/bootstrap-reap.ts
@@ -32875,7 +33284,7 @@ async function reapInstaller(opts) {
32875
33284
  // src/commands/bootstrap/reap.ts
32876
33285
  var BootstrapReapCommand = class extends M8tCommand {
32877
33286
  static paths = [["bootstrap", "reap"]];
32878
- static usage = Command59.Usage({
33287
+ static usage = Command60.Usage({
32879
33288
  description: "Tear down the installer scaffolding (ACI \u2192 MI \u2192 its role assignments) after a successful install.",
32880
33289
  details: "Runs locally on the 'done' signal (the installer can't delete its own identity). The platform RG and the gateway's own assignments persist. A failed install is left intact for diagnosis unless --force.\n\n--sweep-orphans is a DIFFERENT and much broader mode: instead of reaping this install, it scans the WHOLE SUBSCRIPTION for m8t role assignments left behind by installs whose resources are gone \u2014 orphaned installer Owner-at-subscription-scope grants and orphaned gateway subscription-scope roles. It is a dry run that only lists what it found unless you also pass --yes, which deletes them.",
32881
33290
  examples: [
@@ -32885,9 +33294,9 @@ var BootstrapReapCommand = class extends M8tCommand {
32885
33294
  ["\u2026and delete them", "$0 bootstrap reap --sweep-orphans --yes"]
32886
33295
  ]
32887
33296
  });
32888
- force = Option56.Boolean("--force", false, { description: "Reap even if the install failed or never reported a status." });
32889
- sweepOrphans = Option56.Boolean("--sweep-orphans", false, { description: "Subscription-wide: find m8t role assignments orphaned by earlier installs. Lists only, unless --yes." });
32890
- yes = Option56.Boolean("--yes", false, { description: "With --sweep-orphans, actually delete what the sweep found." });
33297
+ force = Option57.Boolean("--force", false, { description: "Reap even if the install failed or never reported a status." });
33298
+ sweepOrphans = Option57.Boolean("--sweep-orphans", false, { description: "Subscription-wide: find m8t role assignments orphaned by earlier installs. Lists only, unless --yes." });
33299
+ yes = Option57.Boolean("--yes", false, { description: "With --sweep-orphans, actually delete what the sweep found." });
32891
33300
  async executeCommand() {
32892
33301
  if (this.sweepOrphans === true) {
32893
33302
  const { subscriptionId: sub } = await getAzAccount();
@@ -32984,7 +33393,7 @@ Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors
32984
33393
  import * as fs41 from "fs";
32985
33394
  import * as os21 from "os";
32986
33395
  import * as path46 from "path";
32987
- import { Command as Command60, Option as Option57 } from "clipanion";
33396
+ import { Command as Command61, Option as Option58 } from "clipanion";
32988
33397
  import { DefaultAzureCredential as DefaultAzureCredential24 } from "@azure/identity";
32989
33398
  init_errors();
32990
33399
 
@@ -33812,7 +34221,7 @@ function renderDeployFailure(error) {
33812
34221
  }
33813
34222
  var BootstrapUiCommand = class extends M8tCommand {
33814
34223
  static paths = [["bootstrap", "ui"]];
33815
- static usage = Command60.Usage({
34224
+ static usage = Command61.Usage({
33816
34225
  description: "Deploy Ezra + start the local onboarding chat UI in the background (returns immediately).",
33817
34226
  details: [
33818
34227
  "Run after `m8t bootstrap launch`, in parallel with `status --watch`. Waits for the cloud",
@@ -33833,16 +34242,16 @@ var BootstrapUiCommand = class extends M8tCommand {
33833
34242
  ["Experimental: start the voice relay (no effect on the text-only intake)", "$0 bootstrap ui --repo-root /path/to/m8t --voice"]
33834
34243
  ]
33835
34244
  });
33836
- repoRoot = Option57.String("--repo-root");
33837
- port = Option57.String("--port", "3000");
33838
- endpoint = Option57.String("--endpoint", {
34245
+ repoRoot = Option58.String("--repo-root");
34246
+ port = Option58.String("--port", "3000");
34247
+ endpoint = Option58.String("--endpoint", {
33839
34248
  description: "Foundry project endpoint to target \u2014 overrides discovery when this install's resource group holds more than one Foundry project."
33840
34249
  });
33841
- prepOnly = Option57.Boolean("--prep-only", false);
33842
- skipInstall = Option57.Boolean("--skip-install", false);
33843
- stop = Option57.Boolean("--stop", false);
33844
- foreground = Option57.Boolean("--foreground", false);
33845
- voice = Option57.Boolean("--voice", false, {
34250
+ prepOnly = Option58.Boolean("--prep-only", false);
34251
+ skipInstall = Option58.Boolean("--skip-install", false);
34252
+ stop = Option58.Boolean("--stop", false);
34253
+ foreground = Option58.Boolean("--foreground", false);
34254
+ voice = Option58.Boolean("--voice", false, {
33846
34255
  description: "Experimental: when serving, starts the voice relay and writes the intake voice env var. The onboarding intake is text-only and is unaffected by this flag \u2014 no voice worker is registered for it."
33847
34256
  });
33848
34257
  async executeCommand() {
@@ -34014,10 +34423,10 @@ var BootstrapUiCommand = class extends M8tCommand {
34014
34423
  };
34015
34424
 
34016
34425
  // src/commands/bootstrap/seed-profile.ts
34017
- import { Command as Command61, Option as Option58 } from "clipanion";
34426
+ import { Command as Command62, Option as Option59 } from "clipanion";
34018
34427
  var BootstrapSeedProfileCommand = class extends M8tCommand {
34019
34428
  static paths = [["bootstrap", "seed-profile"]];
34020
- static usage = Command61.Usage({
34429
+ static usage = Command62.Usage({
34021
34430
  description: "Seed your advisors' brains with the founder + company profile from the onboarding intake.",
34022
34431
  details: "Reads the latest onboarding conversation, renders memory/founder.md + memory/company-profile.md (+ their MEMORY.md index lines), and commits them to both <org>/stacey-brain and <org>/ezra-brain via the GitHub App. Idempotent. --watch polls until the founder finishes the intake.",
34023
34432
  examples: [
@@ -34025,11 +34434,11 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
34025
34434
  ["Wait for the founder to finish", "$0 bootstrap seed-profile --watch"]
34026
34435
  ]
34027
34436
  });
34028
- endpoint = Option58.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
34029
- brain = Option58.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/ezra-brain." });
34030
- watch = Option58.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
34031
- timeout = Option58.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
34032
- githubAppCreds = Option58.String("--github-app-creds");
34437
+ endpoint = Option59.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
34438
+ brain = Option59.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/ezra-brain." });
34439
+ watch = Option59.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
34440
+ timeout = Option59.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
34441
+ githubAppCreds = Option59.String("--github-app-creds");
34033
34442
  async executeCommand() {
34034
34443
  const ctx = await resolveSeedContext({
34035
34444
  endpointOverride: typeof this.endpoint === "string" ? this.endpoint : void 0,
@@ -34092,7 +34501,7 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
34092
34501
  import * as fs42 from "fs";
34093
34502
  import * as os22 from "os";
34094
34503
  import * as path47 from "path";
34095
- import { Command as Command62, Option as Option59 } from "clipanion";
34504
+ import { Command as Command63, Option as Option60 } from "clipanion";
34096
34505
  init_errors();
34097
34506
 
34098
34507
  // src/lib/telemetry-enroll.ts
@@ -34174,7 +34583,7 @@ async function resolveKeyVaultName(containerAppResourceId) {
34174
34583
  }
34175
34584
  var TelemetryEnrollCommand = class extends M8tCommand {
34176
34585
  static paths = [["telemetry", "enroll"]];
34177
- static usage = Command62.Usage({
34586
+ static usage = Command63.Usage({
34178
34587
  description: "Enroll this installation for operational telemetry (pre-existing installs).",
34179
34588
  details: "Generates an instance id + ingest key from the m8t telemetry ingest and stores the key in your platform Key Vault, the same place the installer writes it on a fresh install. Your installation is identified only by that random instance id \u2014 no contact, company, or subscription details are sent unless you pass them explicitly. Idempotent: refuses if a key already exists.",
34180
34589
  examples: [
@@ -34182,11 +34591,11 @@ var TelemetryEnrollCommand = class extends M8tCommand {
34182
34591
  ["Enroll and share a support contact", "$0 telemetry enroll --contact-email you@example.com --company 'Acme'"]
34183
34592
  ]
34184
34593
  });
34185
- company = Option59.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
34186
- contactEmail = Option59.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
34187
- subscription = Option59.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
34188
- resourceGroup = Option59.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
34189
- force = Option59.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
34594
+ company = Option60.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
34595
+ contactEmail = Option60.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
34596
+ subscription = Option60.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
34597
+ resourceGroup = Option60.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
34598
+ force = Option60.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
34190
34599
  async executeCommand() {
34191
34600
  const account = await getAzAccount();
34192
34601
  const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
@@ -34236,7 +34645,7 @@ var TelemetryEnrollCommand = class extends M8tCommand {
34236
34645
  };
34237
34646
 
34238
34647
  // src/commands/companion/bridge.ts
34239
- import { Command as Command63, Option as Option60 } from "clipanion";
34648
+ import { Command as Command64, Option as Option61 } from "clipanion";
34240
34649
 
34241
34650
  // ../../packages/companion-bridge-contract/src/index.ts
34242
34651
  var COMPANION_MESSAGE_MAX_CODE_POINTS = 32768;
@@ -34248,6 +34657,14 @@ var COMPANION_HISTORY_LIMIT_MAX = 40;
34248
34657
  var COMPANION_TURN_TEXT_MAX_CODE_POINTS = 32768;
34249
34658
  var COMPANION_REPLY_DELTA_MAX_CODE_POINTS = 4096;
34250
34659
  var COMPANION_TURN_ID_MAX_LENGTH = 256;
34660
+ var COMPANION_DECISION_CALL_ID_MAX_LENGTH = 256;
34661
+ var COMPANION_DECISION_TITLE_MAX_LENGTH = 200;
34662
+ var COMPANION_DECISION_LABEL_MAX_LENGTH = 100;
34663
+ var COMPANION_DECISION_DETAIL_MAX_CODE_POINTS = 400;
34664
+ var COMPANION_DECISION_OPTIONS_MIN = 2;
34665
+ var COMPANION_DECISION_OPTIONS_MAX = 4;
34666
+ var COMPANION_ARTIFACT_NAME_MAX_LENGTH = 200;
34667
+ var COMPANION_ARTIFACTS_MAX = 16;
34251
34668
  var PERSONAS = /* @__PURE__ */ new Set([
34252
34669
  "startup-advisor",
34253
34670
  "ezra"
@@ -34257,7 +34674,8 @@ var FAILURE_REASONS = /* @__PURE__ */ new Set([
34257
34674
  "not-authorized",
34258
34675
  "mate-unavailable",
34259
34676
  "busy",
34260
- "bad-request"
34677
+ "bad-request",
34678
+ "decision-not-pending"
34261
34679
  ]);
34262
34680
  var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/u;
34263
34681
  var URL_SAFE_OPAQUE_ID = /^[A-Za-z0-9_-]+$/u;
@@ -34266,7 +34684,7 @@ var VERSION3 = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/u;
34266
34684
  function isVersionOrNull(value) {
34267
34685
  return value === null || typeof value === "string" && VERSION3.test(value);
34268
34686
  }
34269
- function isRecord2(value) {
34687
+ function isRecord3(value) {
34270
34688
  return typeof value === "object" && value !== null && !Array.isArray(value);
34271
34689
  }
34272
34690
  function hasExactKeys(value, keys) {
@@ -34290,8 +34708,72 @@ var INSTANT_SHAPE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
34290
34708
  function isInstantOrNull(value) {
34291
34709
  return value === null || typeof value === "string" && INSTANT_SHAPE.test(value) && Number.isFinite(Date.parse(value));
34292
34710
  }
34293
- function isCompanionTurn(value) {
34294
- return isRecord2(value) && hasExactKeys(value, ["id", "role", "text", "at"]) && isBoundedPlainString(value.id, COMPANION_TURN_ID_MAX_LENGTH) && (value.role === "user" || value.role === "mate") && isBoundedMessageText(value.text, COMPANION_TURN_TEXT_MAX_CODE_POINTS) && isInstantOrNull(value.at);
34711
+ function parseDecision(value) {
34712
+ if (!isRecord3(value) || !isBoundedPlainString(value.callId, COMPANION_DECISION_CALL_ID_MAX_LENGTH) || !isBoundedPlainString(value.title, COMPANION_DECISION_TITLE_MAX_LENGTH) || !Array.isArray(value.options) || value.options.length < COMPANION_DECISION_OPTIONS_MIN || value.options.length > COMPANION_DECISION_OPTIONS_MAX) {
34713
+ return eventError();
34714
+ }
34715
+ const options = value.options.map((option) => {
34716
+ if (!isRecord3(option) || !hasExactKeys(option, ["label", "detail"]) || !isBoundedPlainString(option.label, COMPANION_DECISION_LABEL_MAX_LENGTH) || !isBoundedMessageText(option.detail, COMPANION_DECISION_DETAIL_MAX_CODE_POINTS)) {
34717
+ return eventError();
34718
+ }
34719
+ return { label: option.label, detail: option.detail };
34720
+ });
34721
+ const base = { callId: value.callId, title: value.title, options };
34722
+ if (value.status === "pending" || value.status === "dismissed") {
34723
+ if (!hasExactKeys(value, ["callId", "title", "options", "status"])) {
34724
+ return eventError();
34725
+ }
34726
+ return { ...base, status: value.status };
34727
+ }
34728
+ if (value.status !== "selected" || !hasExactKeys(value, ["callId", "title", "options", "status", "optionIndex"]) || !Number.isSafeInteger(value.optionIndex) || value.optionIndex < 0 || value.optionIndex >= options.length) {
34729
+ return eventError();
34730
+ }
34731
+ return { ...base, status: "selected", optionIndex: value.optionIndex };
34732
+ }
34733
+ function parseArtifact(value) {
34734
+ if (!isRecord3(value) || !isBoundedPlainString(value.name, COMPANION_ARTIFACT_NAME_MAX_LENGTH)) {
34735
+ return eventError();
34736
+ }
34737
+ if (value.sizeBytes === void 0) {
34738
+ if (!hasExactKeys(value, ["name"])) return eventError();
34739
+ return { name: value.name };
34740
+ }
34741
+ if (!hasExactKeys(value, ["name", "sizeBytes"]) || !Number.isSafeInteger(value.sizeBytes) || value.sizeBytes < 0) {
34742
+ return eventError();
34743
+ }
34744
+ return { name: value.name, sizeBytes: value.sizeBytes };
34745
+ }
34746
+ function parseTurnArtifacts(value) {
34747
+ if (!Array.isArray(value) || value.length === 0 || value.length > COMPANION_ARTIFACTS_MAX) {
34748
+ return eventError();
34749
+ }
34750
+ return value.map(parseArtifact);
34751
+ }
34752
+ function parseTurn(value) {
34753
+ if (!isRecord3(value) || !isBoundedPlainString(value.id, COMPANION_TURN_ID_MAX_LENGTH) || value.role !== "user" && value.role !== "mate" || !isInstantOrNull(value.at)) {
34754
+ return eventError();
34755
+ }
34756
+ const keys = [
34757
+ "id",
34758
+ "role",
34759
+ "text",
34760
+ "at",
34761
+ ...value.decision === void 0 ? [] : ["decision"],
34762
+ ...value.artifacts === void 0 ? [] : ["artifacts"]
34763
+ ];
34764
+ if (!hasExactKeys(value, keys)) return eventError();
34765
+ const allowEmptyText = value.decision !== void 0 || value.artifacts !== void 0;
34766
+ if (!(allowEmptyText && value.text === "" || isBoundedMessageText(value.text, COMPANION_TURN_TEXT_MAX_CODE_POINTS))) {
34767
+ return eventError();
34768
+ }
34769
+ return {
34770
+ id: value.id,
34771
+ role: value.role,
34772
+ text: value.text,
34773
+ at: value.at,
34774
+ ...value.decision === void 0 ? {} : { decision: parseDecision(value.decision) },
34775
+ ...value.artifacts === void 0 ? {} : { artifacts: parseTurnArtifacts(value.artifacts) }
34776
+ };
34295
34777
  }
34296
34778
  function isCanonicalInstant(value) {
34297
34779
  if (typeof value !== "string") return false;
@@ -34347,7 +34829,7 @@ function isValidMateRoute(value) {
34347
34829
  }
34348
34830
  }
34349
34831
  function parseRequest(value) {
34350
- if (!isRecord2(value)) return requestError();
34832
+ if (!isRecord3(value)) return requestError();
34351
34833
  if (value.type === "roster") {
34352
34834
  if (!hasExactKeys(value, ["type"])) return requestError();
34353
34835
  return { type: "roster" };
@@ -34356,18 +34838,36 @@ function parseRequest(value) {
34356
34838
  if (!hasExactKeys(value, ["type"])) return requestError();
34357
34839
  return { type: "update" };
34358
34840
  }
34359
- if (value.type === "history") {
34841
+ if (value.type === "history" || value.type === "recall") {
34360
34842
  if (!hasExactKeys(value, ["type", "requestId", "personaKey", "limit"]) || !isRequestId(value.requestId) || !isPersonaKey(value.personaKey) || !Number.isSafeInteger(value.limit) || value.limit < 1 || value.limit > COMPANION_HISTORY_LIMIT_MAX) {
34361
34843
  return requestError();
34362
34844
  }
34363
34845
  return {
34364
- type: "history",
34846
+ type: value.type,
34365
34847
  requestId: value.requestId,
34366
34848
  personaKey: value.personaKey,
34367
34849
  limit: value.limit
34368
34850
  };
34369
34851
  }
34370
- if (value.type !== "send" && value.type !== "converse" || !hasExactKeys(value, [
34852
+ if (value.type === "decide") {
34853
+ if (!hasExactKeys(value, [
34854
+ "type",
34855
+ "requestId",
34856
+ "personaKey",
34857
+ "callId",
34858
+ "optionIndex"
34859
+ ]) || !isRequestId(value.requestId) || !isPersonaKey(value.personaKey) || !isBoundedPlainString(value.callId, COMPANION_DECISION_CALL_ID_MAX_LENGTH) || !Number.isSafeInteger(value.optionIndex) || value.optionIndex < 0 || value.optionIndex >= COMPANION_DECISION_OPTIONS_MAX) {
34860
+ return requestError();
34861
+ }
34862
+ return {
34863
+ type: "decide",
34864
+ requestId: value.requestId,
34865
+ personaKey: value.personaKey,
34866
+ callId: value.callId,
34867
+ optionIndex: value.optionIndex
34868
+ };
34869
+ }
34870
+ if (value.type !== "send" && value.type !== "converse" && value.type !== "chat" || !hasExactKeys(value, [
34371
34871
  "type",
34372
34872
  "requestId",
34373
34873
  "personaKey",
@@ -34395,7 +34895,7 @@ function parseRequestLine(line2) {
34395
34895
  }
34396
34896
  }
34397
34897
  function parseMate(value) {
34398
- if (!isRecord2(value) || !hasExactKeys(value, [
34898
+ if (!isRecord3(value) || !hasExactKeys(value, [
34399
34899
  "personaKey",
34400
34900
  "agentName",
34401
34901
  "displayName",
@@ -34420,7 +34920,7 @@ function parseMate(value) {
34420
34920
  };
34421
34921
  }
34422
34922
  function parseEvent(value) {
34423
- if (!isRecord2(value)) return eventError();
34923
+ if (!isRecord3(value)) return eventError();
34424
34924
  if (value.type === "update") {
34425
34925
  if (!hasExactKeys(value, ["type", "installed", "available", "severity"]) || !isVersionOrNull(value.installed) || !isVersionOrNull(value.available) || !(value.severity === null || SEVERITIES2.has(value.severity))) {
34426
34926
  return eventError();
@@ -34463,18 +34963,33 @@ function parseEvent(value) {
34463
34963
  return { type: "completed", requestId: value.requestId };
34464
34964
  }
34465
34965
  if (value.type === "turn") {
34466
- if (!hasExactKeys(value, ["type", "requestId", "turn"]) || !isRequestId(value.requestId) || !isCompanionTurn(value.turn)) {
34966
+ if (!hasExactKeys(value, ["type", "requestId", "turn"]) || !isRequestId(value.requestId)) {
34467
34967
  return eventError();
34468
34968
  }
34469
34969
  return {
34470
34970
  type: "turn",
34471
34971
  requestId: value.requestId,
34472
- turn: {
34473
- id: value.turn.id,
34474
- role: value.turn.role,
34475
- text: value.turn.text,
34476
- at: value.turn.at
34477
- }
34972
+ turn: parseTurn(value.turn)
34973
+ };
34974
+ }
34975
+ if (value.type === "reply-decision") {
34976
+ if (!hasExactKeys(value, ["type", "requestId", "decision"]) || !isRequestId(value.requestId)) {
34977
+ return eventError();
34978
+ }
34979
+ return {
34980
+ type: "reply-decision",
34981
+ requestId: value.requestId,
34982
+ decision: parseDecision(value.decision)
34983
+ };
34984
+ }
34985
+ if (value.type === "reply-artifacts") {
34986
+ if (!hasExactKeys(value, ["type", "requestId", "artifacts"]) || !isRequestId(value.requestId) || !Array.isArray(value.artifacts) || value.artifacts.length === 0 || value.artifacts.length > COMPANION_ARTIFACTS_MAX) {
34987
+ return eventError();
34988
+ }
34989
+ return {
34990
+ type: "reply-artifacts",
34991
+ requestId: value.requestId,
34992
+ artifacts: value.artifacts.map(parseArtifact)
34478
34993
  };
34479
34994
  }
34480
34995
  if (value.type === "history-end") {
@@ -34531,6 +35046,7 @@ function serializeEvent(value) {
34531
35046
  }
34532
35047
 
34533
35048
  // src/lib/companion-chat-client.ts
35049
+ init_esm();
34534
35050
  import { stat as stat4 } from "fs/promises";
34535
35051
  var RESPONSE_MAX_BYTES = 1e6;
34536
35052
  var REQUEST_DEADLINE_MS = 12e4;
@@ -34543,7 +35059,7 @@ var PredispatchFailure = class extends Error {
34543
35059
  }
34544
35060
  reason;
34545
35061
  };
34546
- function isRecord3(value) {
35062
+ function isRecord4(value) {
34547
35063
  return typeof value === "object" && value !== null && !Array.isArray(value);
34548
35064
  }
34549
35065
  function hasControlCharacter(value) {
@@ -34613,16 +35129,16 @@ async function readJsonEnvelope(response) {
34613
35129
  if (error instanceof PredispatchFailure) throw error;
34614
35130
  throw new PredispatchFailure("not-connected");
34615
35131
  }
34616
- if (!isRecord3(envelope) || envelope.ok !== true || !("data" in envelope)) {
35132
+ if (!isRecord4(envelope) || envelope.ok !== true || !("data" in envelope)) {
34617
35133
  throw new PredispatchFailure("not-connected");
34618
35134
  }
34619
35135
  return envelope.data;
34620
35136
  }
34621
35137
  function decodeAgents(data) {
34622
- if (!isRecord3(data) || !Array.isArray(data.agents)) {
35138
+ if (!isRecord4(data) || !Array.isArray(data.agents)) {
34623
35139
  throw new PredispatchFailure("mate-unavailable");
34624
35140
  }
34625
- return data.agents.filter(isRecord3);
35141
+ return data.agents.filter(isRecord4);
34626
35142
  }
34627
35143
  function resolveAgentName(agents, personaKey) {
34628
35144
  const matches = agents.filter(
@@ -34644,18 +35160,18 @@ function requireAgentName(agents, personaKey) {
34644
35160
  return name;
34645
35161
  }
34646
35162
  function decodeConversationId(data) {
34647
- if (!isRecord3(data) || typeof data.id !== "string" || !OPAQUE_ID.test(data.id)) {
35163
+ if (!isRecord4(data) || typeof data.id !== "string" || !OPAQUE_ID.test(data.id)) {
34648
35164
  throw new PredispatchFailure("not-connected");
34649
35165
  }
34650
35166
  return data.id;
34651
35167
  }
34652
35168
  function decodeLastMessageId(data) {
34653
- if (!isRecord3(data) || !Array.isArray(data.messages)) {
35169
+ if (!isRecord4(data) || !Array.isArray(data.messages)) {
34654
35170
  throw new PredispatchFailure("not-connected");
34655
35171
  }
34656
35172
  const messages = data.messages;
34657
35173
  const last = messages.at(-1);
34658
- if (!isRecord3(last) || typeof last.id !== "string" || !OPAQUE_ID.test(last.id)) {
35174
+ if (!isRecord4(last) || typeof last.id !== "string" || !OPAQUE_ID.test(last.id)) {
34659
35175
  return void 0;
34660
35176
  }
34661
35177
  return last.id;
@@ -34706,13 +35222,61 @@ function boundCodePoints(value, maxCodePoints) {
34706
35222
  }
34707
35223
  return value;
34708
35224
  }
35225
+ function boundPlainLine(value, maxLength) {
35226
+ return value.replace(/[\u0000-\u001f\u007f]/gu, " ").trim().slice(0, maxLength);
35227
+ }
35228
+ var ARTIFACTS_PART_TYPE = "data-artifacts";
35229
+ function toCompanionDecision(directive) {
35230
+ const state = directive.state;
35231
+ if (state.status !== "pending" && state.status !== "selected" && state.status !== "dismissed") {
35232
+ return null;
35233
+ }
35234
+ const callId = boundPlainLine(
35235
+ directive.callId,
35236
+ COMPANION_DECISION_CALL_ID_MAX_LENGTH
35237
+ );
35238
+ const title = boundPlainLine(
35239
+ directive.args.title,
35240
+ COMPANION_DECISION_TITLE_MAX_LENGTH
35241
+ );
35242
+ if (callId.length === 0 || title.length === 0) return null;
35243
+ const options = [];
35244
+ for (const option of directive.args.options) {
35245
+ const label = boundPlainLine(option.label, COMPANION_DECISION_LABEL_MAX_LENGTH);
35246
+ const detail = boundCodePoints(
35247
+ sanitizeMessageText(option.detail).trim(),
35248
+ COMPANION_DECISION_DETAIL_MAX_CODE_POINTS
35249
+ );
35250
+ if (label.length === 0 || detail.length === 0) return null;
35251
+ options.push({ label, detail });
35252
+ }
35253
+ const base = { callId, title, options };
35254
+ return state.status === "selected" ? { ...base, status: "selected", optionIndex: state.optionIndex } : { ...base, status: state.status };
35255
+ }
35256
+ function toCompanionArtifacts(data) {
35257
+ const artifacts = [];
35258
+ for (const entry of data) {
35259
+ if (artifacts.length >= COMPANION_ARTIFACTS_MAX) break;
35260
+ if (!isRecord4(entry) || typeof entry.name !== "string") continue;
35261
+ const name = boundPlainLine(entry.name, COMPANION_ARTIFACT_NAME_MAX_LENGTH);
35262
+ if (name.length === 0) continue;
35263
+ const size = entry.size_bytes;
35264
+ artifacts.push(
35265
+ typeof size === "number" && Number.isSafeInteger(size) && size >= 0 ? { name, sizeBytes: size } : { name }
35266
+ );
35267
+ }
35268
+ return artifacts;
35269
+ }
35270
+ function decodeMessageInstant(value) {
35271
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value < EPOCH_SECONDS_LIMIT ? new Date(Math.round(value) * 1e3).toISOString() : null;
35272
+ }
34709
35273
  function decodeTurns(data, limit2) {
34710
- if (!isRecord3(data) || !Array.isArray(data.messages)) {
35274
+ if (!isRecord4(data) || !Array.isArray(data.messages)) {
34711
35275
  throw new PredispatchFailure("not-connected");
34712
35276
  }
34713
35277
  const turns = [];
34714
35278
  for (const entry of data.messages) {
34715
- if (!isRecord3(entry)) continue;
35279
+ if (!isRecord4(entry)) continue;
34716
35280
  if (entry.role !== "user" && entry.role !== "assistant") continue;
34717
35281
  if (typeof entry.id !== "string" || !OPAQUE_ID.test(entry.id)) continue;
34718
35282
  if (typeof entry.content !== "string") continue;
@@ -34721,12 +35285,52 @@ function decodeTurns(data, limit2) {
34721
35285
  COMPANION_TURN_TEXT_MAX_CODE_POINTS
34722
35286
  );
34723
35287
  if (text.length === 0) continue;
34724
- const at = typeof entry.createdAt === "number" && Number.isFinite(entry.createdAt) && entry.createdAt > 0 && entry.createdAt < EPOCH_SECONDS_LIMIT ? new Date(Math.round(entry.createdAt) * 1e3).toISOString() : null;
34725
35288
  turns.push({
34726
35289
  id: entry.id,
34727
35290
  role: entry.role === "user" ? "user" : "mate",
34728
35291
  text,
34729
- at
35292
+ at: decodeMessageInstant(entry.createdAt)
35293
+ });
35294
+ }
35295
+ return turns.slice(-limit2);
35296
+ }
35297
+ function decodeDurableDecision(entry) {
35298
+ const frame = parseUiDirectiveFrame({
35299
+ directive: entry.uiDirective,
35300
+ source: entry.uiDirectiveSource === "voice" ? "voice" : "text"
35301
+ });
35302
+ if (!frame) return null;
35303
+ return toCompanionDecision(frame.directive);
35304
+ }
35305
+ function decodeRichTurns(data, limit2) {
35306
+ if (!isRecord4(data) || !Array.isArray(data.messages)) {
35307
+ throw new PredispatchFailure("not-connected");
35308
+ }
35309
+ const turns = [];
35310
+ for (const entry of data.messages) {
35311
+ if (!isRecord4(entry)) continue;
35312
+ if (entry.role !== "user" && entry.role !== "assistant") continue;
35313
+ if (typeof entry.id !== "string" || !OPAQUE_ID.test(entry.id)) continue;
35314
+ if (typeof entry.content !== "string") continue;
35315
+ const parsed = parseArtifacts(entry.content);
35316
+ const artifacts = toCompanionArtifacts(parsed.artifacts);
35317
+ const text = boundCodePoints(
35318
+ sanitizeMessageText(parsed.text),
35319
+ COMPANION_TURN_TEXT_MAX_CODE_POINTS
35320
+ );
35321
+ const at = decodeMessageInstant(entry.createdAt);
35322
+ const decision = entry.uiDirective === void 0 ? null : decodeDurableDecision(entry);
35323
+ if (decision !== null) {
35324
+ turns.push({ id: entry.id, role: "mate", text, at, decision });
35325
+ continue;
35326
+ }
35327
+ if (text.length === 0 && artifacts.length === 0) continue;
35328
+ turns.push({
35329
+ id: entry.id,
35330
+ role: entry.role === "user" ? "user" : "mate",
35331
+ text,
35332
+ at,
35333
+ ...artifacts.length > 0 ? { artifacts } : {}
34730
35334
  });
34731
35335
  }
34732
35336
  return turns.slice(-limit2);
@@ -34840,7 +35444,7 @@ async function drainAcceptedSse(response) {
34840
35444
  continue;
34841
35445
  }
34842
35446
  const parsed = JSON.parse(data);
34843
- if (!isRecord3(parsed) || typeof parsed.type !== "string" || parsed.type === "error") {
35447
+ if (!isRecord4(parsed) || typeof parsed.type !== "string" || parsed.type === "error") {
34844
35448
  throw new Error("invalid stream event");
34845
35449
  }
34846
35450
  }
@@ -34862,7 +35466,7 @@ function emitBoundedText(text, emit) {
34862
35466
  }
34863
35467
  if (slice.length > 0) emit(slice);
34864
35468
  }
34865
- async function streamAcceptedSse(response, now, onText) {
35469
+ async function streamAcceptedSse(response, now, onText, onData) {
34866
35470
  if (!response.body) throw new Error("response body unavailable");
34867
35471
  const reader = response.body.getReader();
34868
35472
  const decoder = new TextDecoder("utf-8", { fatal: true });
@@ -34888,7 +35492,7 @@ async function streamAcceptedSse(response, now, onText) {
34888
35492
  return;
34889
35493
  }
34890
35494
  const parsed = JSON.parse(data);
34891
- if (!isRecord3(parsed) || typeof parsed.type !== "string" || parsed.type === "error") {
35495
+ if (!isRecord4(parsed) || typeof parsed.type !== "string" || parsed.type === "error") {
34892
35496
  throw new Error("invalid stream event");
34893
35497
  }
34894
35498
  if (parsed.type === "text-delta" && typeof parsed.delta === "string" && parsed.delta.length > 0) {
@@ -34897,6 +35501,10 @@ async function streamAcceptedSse(response, now, onText) {
34897
35501
  flush();
34898
35502
  }
34899
35503
  }
35504
+ if (onData && parsed.type.startsWith("data-")) {
35505
+ flush();
35506
+ onData(parsed);
35507
+ }
34900
35508
  };
34901
35509
  const drainFrames = () => {
34902
35510
  for (; ; ) {
@@ -34932,7 +35540,7 @@ async function streamAcceptedSse(response, now, onText) {
34932
35540
  throw new Error("stream ended without terminal marker");
34933
35541
  }
34934
35542
  }
34935
- async function dispatchConversation(request, sink, deps, consume, readDesktopMarker) {
35543
+ async function dispatchConversation(request, sink, deps, consume, readDesktopMarker, chatBody, classifyRefusal) {
34936
35544
  const { controller, timer } = createDeadline(deps);
34937
35545
  const fetchImpl = deps.fetch ?? fetch;
34938
35546
  let route;
@@ -34978,17 +35586,19 @@ async function dispatchConversation(request, sink, deps, consume, readDesktopMar
34978
35586
  chatResponse = await fetchImpl(`${origin}/api/chat`, {
34979
35587
  method: "POST",
34980
35588
  headers: { ...headers, "content-type": "application/json" },
34981
- body: JSON.stringify({
34982
- messages: [{ role: "user", content: request.text }],
34983
- agentName,
34984
- conversationId
34985
- }),
35589
+ body: JSON.stringify(chatBody(agentName, conversationId)),
34986
35590
  signal: controller.signal
34987
35591
  });
34988
35592
  } catch {
34989
35593
  throw new Error("chat dispatch outcome unavailable");
34990
35594
  }
34991
35595
  if (!chatResponse.ok || !chatResponse.body || !(chatResponse.headers.get("content-type") ?? "").toLowerCase().includes("text/event-stream")) {
35596
+ if (!chatResponse.ok && classifyRefusal) {
35597
+ const reason = await classifyRefusal(chatResponse);
35598
+ if (reason !== null) {
35599
+ return emitFailure(sink, reason, request.requestId);
35600
+ }
35601
+ }
34992
35602
  throw new Error("chat response not accepted");
34993
35603
  }
34994
35604
  const acceptedEvent = parseEvent({
@@ -35026,8 +35636,40 @@ async function dispatchConversation(request, sink, deps, consume, readDesktopMar
35026
35636
  clearTimeout(timer);
35027
35637
  }
35028
35638
  }
35639
+ function textTurnBody(text) {
35640
+ return (agentName, conversationId) => ({
35641
+ messages: [{ role: "user", content: text }],
35642
+ agentName,
35643
+ conversationId
35644
+ });
35645
+ }
35646
+ function replyDataForwarder(requestId, sink) {
35647
+ return (part) => {
35648
+ if (part.type === UI_DIRECTIVE_PART_TYPE) {
35649
+ const frame = parseUiDirectiveFrame(part.data);
35650
+ if (!frame) return;
35651
+ const decision = toCompanionDecision(frame.directive);
35652
+ if (decision === null) return;
35653
+ sink(parseEvent({ type: "reply-decision", requestId, decision }));
35654
+ return;
35655
+ }
35656
+ if (part.type === ARTIFACTS_PART_TYPE && Array.isArray(part.data)) {
35657
+ const artifacts = toCompanionArtifacts(part.data);
35658
+ if (artifacts.length > 0) {
35659
+ sink(parseEvent({ type: "reply-artifacts", requestId, artifacts }));
35660
+ }
35661
+ }
35662
+ };
35663
+ }
35029
35664
  async function sendCompanionMessage(request, sink, deps = {}) {
35030
- return dispatchConversation(request, sink, deps, drainAcceptedSse, true);
35665
+ return dispatchConversation(
35666
+ request,
35667
+ sink,
35668
+ deps,
35669
+ drainAcceptedSse,
35670
+ true,
35671
+ textTurnBody(request.text)
35672
+ );
35031
35673
  }
35032
35674
  async function converseCompanionMessage(request, sink, deps = {}) {
35033
35675
  return dispatchConversation(
@@ -35043,10 +35685,101 @@ async function converseCompanionMessage(request, sink, deps = {}) {
35043
35685
  })
35044
35686
  );
35045
35687
  }),
35046
- false
35688
+ false,
35689
+ textTurnBody(request.text)
35690
+ );
35691
+ }
35692
+ async function chatCompanionMessage(request, sink, deps = {}) {
35693
+ return dispatchConversation(
35694
+ request,
35695
+ sink,
35696
+ deps,
35697
+ (response) => streamAcceptedSse(
35698
+ response,
35699
+ deps.now ?? Date.now,
35700
+ (text) => {
35701
+ sink(
35702
+ parseEvent({
35703
+ type: "reply-delta",
35704
+ requestId: request.requestId,
35705
+ text
35706
+ })
35707
+ );
35708
+ },
35709
+ replyDataForwarder(request.requestId, sink)
35710
+ ),
35711
+ false,
35712
+ textTurnBody(request.text)
35713
+ );
35714
+ }
35715
+ async function classifyDecideRefusal(response) {
35716
+ let reason;
35717
+ try {
35718
+ const envelope = JSON.parse(await readBoundedText(response));
35719
+ if (isRecord4(envelope) && isRecord4(envelope.error) && isRecord4(envelope.error.details)) {
35720
+ reason = envelope.error.details.reason;
35721
+ }
35722
+ } catch {
35723
+ }
35724
+ if (isDecisionRefusalReason(reason)) {
35725
+ return reason === DECISION_REFUSAL_REASONS.notPending ? "decision-not-pending" : "bad-request";
35726
+ }
35727
+ if (response.status === 401 || response.status === 403) {
35728
+ return "not-authorized";
35729
+ }
35730
+ if (response.status === 400) return "bad-request";
35731
+ if (response.status === 409) return "busy";
35732
+ return null;
35733
+ }
35734
+ async function decideCompanionMessage(request, sink, deps = {}) {
35735
+ const message = {
35736
+ role: "user",
35737
+ parts: [
35738
+ {
35739
+ type: UI_DIRECTIVE_RESULT_PART_TYPE,
35740
+ data: {
35741
+ version: 1,
35742
+ name: "present_decision",
35743
+ callId: request.callId,
35744
+ result: { status: "selected", optionIndex: request.optionIndex }
35745
+ }
35746
+ }
35747
+ ]
35748
+ };
35749
+ return dispatchConversation(
35750
+ request,
35751
+ sink,
35752
+ deps,
35753
+ (response) => streamAcceptedSse(
35754
+ response,
35755
+ deps.now ?? Date.now,
35756
+ (text) => {
35757
+ sink(
35758
+ parseEvent({
35759
+ type: "reply-delta",
35760
+ requestId: request.requestId,
35761
+ text
35762
+ })
35763
+ );
35764
+ },
35765
+ replyDataForwarder(request.requestId, sink)
35766
+ ),
35767
+ false,
35768
+ (agentName, conversationId) => ({
35769
+ messages: [message],
35770
+ agentName,
35771
+ conversationId
35772
+ }),
35773
+ classifyDecideRefusal
35047
35774
  );
35048
35775
  }
35049
35776
  async function historyCompanionMessages(request, sink, deps = {}) {
35777
+ return fetchConversationTurns(request, sink, deps, decodeTurns);
35778
+ }
35779
+ async function recallCompanionMessages(request, sink, deps = {}) {
35780
+ return fetchConversationTurns(request, sink, deps, decodeRichTurns);
35781
+ }
35782
+ async function fetchConversationTurns(request, sink, deps, decode) {
35050
35783
  const { controller, timer } = createDeadline(deps);
35051
35784
  const fetchImpl = deps.fetch ?? fetch;
35052
35785
  try {
@@ -35066,7 +35799,7 @@ async function historyCompanionMessages(request, sink, deps = {}) {
35066
35799
  agentName,
35067
35800
  controller.signal
35068
35801
  );
35069
- const turns = decodeTurns(
35802
+ const turns = decode(
35070
35803
  await fetchConversationMessages(
35071
35804
  fetchImpl,
35072
35805
  origin,
@@ -35222,7 +35955,10 @@ async function dispatchRequest(request, sink, handlers) {
35222
35955
  return terminal;
35223
35956
  }
35224
35957
  if (request.type === "history") return handlers.history(request, sink);
35958
+ if (request.type === "recall") return handlers.recall(request, sink);
35225
35959
  if (request.type === "converse") return handlers.converse(request, sink);
35960
+ if (request.type === "chat") return handlers.chat(request, sink);
35961
+ if (request.type === "decide") return handlers.decide(request, sink);
35226
35962
  return handlers.send(request, sink);
35227
35963
  }
35228
35964
  async function handleWithContext(request, sink, handlers, context) {
@@ -35258,7 +35994,10 @@ async function runCompanionBridgeServe(stdin, stdout, stderr, deps = {}) {
35258
35994
  roster: (sink2) => rosterCompanions(sink2, chat),
35259
35995
  send: (request, sink2) => sendCompanionMessage(request, sink2, chat),
35260
35996
  converse: (request, sink2) => converseCompanionMessage(request, sink2, chat),
35997
+ chat: (request, sink2) => chatCompanionMessage(request, sink2, chat),
35998
+ decide: (request, sink2) => decideCompanionMessage(request, sink2, chat),
35261
35999
  history: (request, sink2) => historyCompanionMessages(request, sink2, chat),
36000
+ recall: (request, sink2) => recallCompanionMessages(request, sink2, chat),
35262
36001
  update: deps.update ?? (() => checkCompanionUpdate(defaultLocalCompanionInstallOptions()))
35263
36002
  };
35264
36003
  const sink = (event) => {
@@ -35289,11 +36028,14 @@ async function runCompanionBridgeServe(stdin, stdout, stderr, deps = {}) {
35289
36028
  }
35290
36029
 
35291
36030
  // src/commands/companion/bridge.ts
35292
- var defaultDeps3 = {
36031
+ var defaultDeps4 = {
35293
36032
  roster: rosterCompanions,
35294
36033
  send: sendCompanionMessage,
35295
36034
  converse: converseCompanionMessage,
36035
+ chat: chatCompanionMessage,
36036
+ decide: decideCompanionMessage,
35296
36037
  history: historyCompanionMessages,
36038
+ recall: recallCompanionMessages,
35297
36039
  update: () => checkCompanionUpdate(defaultLocalCompanionInstallOptions())
35298
36040
  };
35299
36041
  async function readSingleRequest(stdin) {
@@ -35324,7 +36066,7 @@ function exitFor(terminal) {
35324
36066
  }
35325
36067
  return 2;
35326
36068
  }
35327
- async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps3) {
36069
+ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps4) {
35328
36070
  let request;
35329
36071
  try {
35330
36072
  request = parseRequestLine(await readSingleRequest(stdin));
@@ -35344,8 +36086,14 @@ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps3) {
35344
36086
  sink(terminal);
35345
36087
  } else if (request.type === "history") {
35346
36088
  terminal = await deps.history(request, sink);
36089
+ } else if (request.type === "recall") {
36090
+ terminal = await deps.recall(request, sink);
35347
36091
  } else if (request.type === "converse") {
35348
36092
  terminal = await deps.converse(request, sink);
36093
+ } else if (request.type === "chat") {
36094
+ terminal = await deps.chat(request, sink);
36095
+ } else if (request.type === "decide") {
36096
+ terminal = await deps.decide(request, sink);
35349
36097
  } else terminal = await deps.send(request, sink);
35350
36098
  return exitFor(terminal);
35351
36099
  } catch {
@@ -35353,14 +36101,14 @@ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps3) {
35353
36101
  return 3;
35354
36102
  }
35355
36103
  }
35356
- var CompanionBridgeCommand = class extends Command63 {
36104
+ var CompanionBridgeCommand = class extends Command64 {
35357
36105
  static paths = [["companion", "_bridge"]];
35358
36106
  /**
35359
36107
  * One process serving many requests instead of one per request, so the
35360
36108
  * session keeps its authenticated context between them. A CLI predating the
35361
36109
  * flag rejects it outright, which is how the app knows to fall back.
35362
36110
  */
35363
- serve = Option60.Boolean("--serve", false);
36111
+ serve = Option61.Boolean("--serve", false);
35364
36112
  async execute() {
35365
36113
  if (this.serve) {
35366
36114
  return runCompanionBridgeServe(
@@ -35378,7 +36126,7 @@ var CompanionBridgeCommand = class extends Command63 {
35378
36126
  };
35379
36127
 
35380
36128
  // src/commands/companion/status.ts
35381
- import { Command as Command64 } from "clipanion";
36129
+ import { Command as Command65 } from "clipanion";
35382
36130
  async function withTimeout(work, ms) {
35383
36131
  let timer;
35384
36132
  try {
@@ -35450,7 +36198,7 @@ Run: m8t companion install
35450
36198
  }
35451
36199
  var CompanionStatusCommand = class extends M8tCommand {
35452
36200
  static paths = [["companion", "status"]];
35453
- static usage = Command64.Usage({
36201
+ static usage = Command65.Usage({
35454
36202
  description: "Verify the installed desktop companion without launching it."
35455
36203
  });
35456
36204
  async executeCommand() {
@@ -35464,7 +36212,7 @@ var CompanionStatusCommand = class extends M8tCommand {
35464
36212
  };
35465
36213
 
35466
36214
  // src/commands/companion/repair.ts
35467
- import { Command as Command65, Option as Option61 } from "clipanion";
36215
+ import { Command as Command66, Option as Option62 } from "clipanion";
35468
36216
  async function runCompanionRepairCommand(stdout, repair) {
35469
36217
  const state = await repair();
35470
36218
  if (state.state === "not-released") {
@@ -35483,10 +36231,10 @@ async function runCompanionRepairCommand(stdout, repair) {
35483
36231
  }
35484
36232
  var CompanionRepairCommand = class extends M8tCommand {
35485
36233
  static paths = [["companion", "repair"]];
35486
- static usage = Command65.Usage({
36234
+ static usage = Command66.Usage({
35487
36235
  description: "Restore the desktop companions and start-at-login state."
35488
36236
  });
35489
- resourceGroup = Option61.String("--resource-group", {
36237
+ resourceGroup = Option62.String("--resource-group", {
35490
36238
  description: "Which deployment to bind to, when the subscription holds more than one."
35491
36239
  });
35492
36240
  async executeCommand() {
@@ -35500,7 +36248,7 @@ var CompanionRepairCommand = class extends M8tCommand {
35500
36248
  };
35501
36249
 
35502
36250
  // src/commands/companion/uninstall.ts
35503
- import { Command as Command66 } from "clipanion";
36251
+ import { Command as Command67 } from "clipanion";
35504
36252
  async function runCompanionUninstallCommand(stdout, uninstall) {
35505
36253
  const state = await uninstall();
35506
36254
  if (state.state !== "not-installed") {
@@ -35512,7 +36260,7 @@ async function runCompanionUninstallCommand(stdout, uninstall) {
35512
36260
  }
35513
36261
  var CompanionUninstallCommand = class extends M8tCommand {
35514
36262
  static paths = [["companion", "uninstall"]];
35515
- static usage = Command66.Usage({
36263
+ static usage = Command67.Usage({
35516
36264
  description: "Remove only this user's desktop companion installation."
35517
36265
  });
35518
36266
  async executeCommand() {
@@ -35582,6 +36330,7 @@ cli.register(A2aDisableCommand);
35582
36330
  cli.register(EvalSkillCommand);
35583
36331
  cli.register(EvalExamCommand);
35584
36332
  cli.register(DreamRunCommand);
36333
+ cli.register(ConversationsSweepCommand);
35585
36334
  cli.register(FoundryCreateCommand);
35586
36335
  cli.register(FoundryAwaitReadyCommand);
35587
36336
  cli.register(BootstrapPreflightCommand);