@vellumai/assistant 0.5.7 → 0.5.9
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/Dockerfile +2 -1
- package/docker-entrypoint.sh +9 -0
- package/docs/architecture/memory.md +13 -11
- package/eslint.config.mjs +0 -31
- package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/approval-cascade.test.ts +0 -1
- package/src/__tests__/browser-fill-credential.test.ts +1 -1
- package/src/__tests__/call-controller.test.ts +0 -1
- package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
- package/src/__tests__/ces-startup-timeout.test.ts +40 -0
- package/src/__tests__/config-schema-cmd.test.ts +0 -1
- package/src/__tests__/config-schema.test.ts +2 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
- package/src/__tests__/conversation-agent-loop.test.ts +2 -4
- package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
- package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
- package/src/__tests__/conversation-queue.test.ts +0 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +227 -0
- package/src/__tests__/conversation-slash-queue.test.ts +0 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/credential-execution-client.test.ts +5 -2
- package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
- package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
- package/src/__tests__/credential-security-e2e.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -5
- package/src/__tests__/credentials-cli.test.ts +4 -3
- package/src/__tests__/daemon-credential-client.test.ts +123 -0
- package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
- package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
- package/src/__tests__/journal-context.test.ts +335 -0
- package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
- package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
- package/src/__tests__/memory-recall-quality.test.ts +48 -17
- package/src/__tests__/memory-regressions.test.ts +408 -363
- package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
- package/src/__tests__/non-member-access-request.test.ts +2 -2
- package/src/__tests__/notification-decision-strategy.test.ts +71 -0
- package/src/__tests__/oauth-cli.test.ts +5 -1
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
- package/src/__tests__/provider-error-scenarios.test.ts +0 -267
- package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
- package/src/__tests__/relay-server.test.ts +1 -2
- package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +1 -1
- package/src/__tests__/secure-keys.test.ts +18 -15
- package/src/__tests__/skill-memory.test.ts +17 -3
- package/src/__tests__/stale-approval-dedup.test.ts +171 -0
- package/src/__tests__/stt-hints.test.ts +437 -0
- package/src/__tests__/task-memory-cleanup.test.ts +14 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
- package/src/__tests__/voice-quality.test.ts +58 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
- package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
- package/src/acp/agent-process.ts +9 -1
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-request-resolvers.ts +164 -38
- package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
- package/src/calls/call-controller.ts +9 -5
- package/src/calls/fish-audio-client.ts +26 -14
- package/src/calls/stt-hints.ts +189 -0
- package/src/calls/tts-text-sanitizer.ts +61 -0
- package/src/calls/twilio-routes.ts +32 -4
- package/src/calls/voice-quality.ts +15 -3
- package/src/calls/voice-session-bridge.ts +1 -0
- package/src/cli/commands/avatar.ts +2 -2
- package/src/cli/commands/credentials.ts +110 -94
- package/src/cli/commands/doctor.ts +2 -2
- package/src/cli/commands/keys.ts +7 -7
- package/src/cli/commands/memory.ts +1 -1
- package/src/cli/commands/oauth/connections.ts +11 -29
- package/src/cli/commands/oauth/platform.ts +389 -43
- package/src/cli/lib/daemon-credential-client.ts +284 -0
- package/src/cli.ts +1 -1
- package/src/config/bundled-skills/AGENTS.md +34 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
- package/src/config/bundled-skills/settings/SKILL.md +15 -2
- package/src/config/bundled-skills/settings/TOOLS.json +46 -1
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
- package/src/config/bundled-skills/slack/SKILL.md +1 -1
- package/src/config/bundled-tool-registry.ts +4 -0
- package/src/config/defaults.ts +0 -2
- package/src/config/env-registry.ts +4 -4
- package/src/config/env.ts +14 -1
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +8 -11
- package/src/config/schema.ts +5 -16
- package/src/config/schemas/calls.ts +17 -0
- package/src/config/schemas/inference.ts +2 -2
- package/src/config/schemas/journal.ts +16 -0
- package/src/config/schemas/memory-processing.ts +2 -2
- package/src/config/types.ts +1 -0
- package/src/contacts/contact-store.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +1 -1
- package/src/credential-execution/startup-timeout.ts +36 -0
- package/src/daemon/approval-generators.ts +3 -9
- package/src/daemon/conversation-agent-loop.ts +6 -0
- package/src/daemon/conversation-error.ts +13 -1
- package/src/daemon/conversation-memory.ts +1 -2
- package/src/daemon/conversation-process.ts +18 -1
- package/src/daemon/conversation-runtime-assembly.ts +61 -1
- package/src/daemon/conversation-surfaces.ts +30 -1
- package/src/daemon/conversation.ts +20 -9
- package/src/daemon/guardian-action-generators.ts +3 -9
- package/src/daemon/lifecycle.ts +18 -11
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/server.ts +2 -3
- package/src/memory/app-store.ts +31 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/indexer.ts +19 -10
- package/src/memory/items-extractor.ts +315 -322
- package/src/memory/job-handlers/summarization.ts +26 -16
- package/src/memory/jobs-store.ts +33 -1
- package/src/memory/journal-memory.ts +214 -0
- package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/retriever.test.ts +37 -25
- package/src/memory/retriever.ts +24 -49
- package/src/memory/schema/memory-core.ts +2 -0
- package/src/memory/search/formatting.ts +7 -44
- package/src/memory/search/staleness.ts +4 -0
- package/src/memory/search/tier-classifier.ts +10 -2
- package/src/memory/search/types.ts +2 -5
- package/src/memory/task-memory-cleanup.ts +4 -3
- package/src/notifications/adapters/slack.ts +168 -6
- package/src/notifications/broadcaster.ts +1 -0
- package/src/notifications/copy-composer.ts +59 -2
- package/src/notifications/signal.ts +2 -0
- package/src/notifications/types.ts +2 -0
- package/src/prompts/journal-context.ts +133 -0
- package/src/prompts/persona-resolver.ts +80 -24
- package/src/prompts/system-prompt.ts +30 -0
- package/src/prompts/templates/NOW.md +26 -0
- package/src/prompts/templates/SOUL.md +20 -0
- package/src/prompts/update-bulletin-format.ts +0 -2
- package/src/providers/provider-send-message.ts +3 -32
- package/src/providers/registry.ts +2 -139
- package/src/providers/types.ts +1 -1
- package/src/runtime/access-request-helper.ts +4 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
- package/src/runtime/auth/route-policy.ts +2 -0
- package/src/runtime/gateway-client.ts +47 -4
- package/src/runtime/guardian-decision-types.ts +45 -4
- package/src/runtime/http-server.ts +5 -2
- package/src/runtime/routes/access-request-decision.ts +2 -2
- package/src/runtime/routes/app-management-routes.ts +2 -1
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
- package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
- package/src/runtime/routes/channel-readiness-routes.ts +9 -4
- package/src/runtime/routes/debug-routes.ts +12 -9
- package/src/runtime/routes/guardian-approval-interception.ts +168 -11
- package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
- package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
- package/src/runtime/routes/identity-routes.ts +1 -1
- package/src/runtime/routes/inbound-message-handler.ts +31 -1
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
- package/src/runtime/routes/integrations/twilio.ts +52 -10
- package/src/runtime/routes/memory-item-routes.test.ts +3 -3
- package/src/runtime/routes/memory-item-routes.ts +25 -11
- package/src/runtime/routes/secret-routes.ts +141 -10
- package/src/runtime/routes/tts-routes.ts +11 -1
- package/src/security/ces-credential-client.ts +18 -9
- package/src/security/ces-rpc-credential-backend.ts +4 -3
- package/src/security/credential-backend.ts +10 -4
- package/src/security/secure-keys.ts +21 -4
- package/src/skills/catalog-install.ts +4 -36
- package/src/skills/inline-command-expansions.ts +7 -7
- package/src/skills/skill-memory.ts +1 -0
- package/src/subagent/manager.ts +2 -5
- package/src/tools/acp/spawn.ts +78 -1
- package/src/tools/credentials/vault.ts +5 -3
- package/src/tools/memory/definitions.ts +3 -2
- package/src/tools/memory/handlers.ts +10 -7
- package/src/tools/sensitive-output-placeholders.ts +2 -2
- package/src/tools/terminal/safe-env.ts +1 -0
- package/src/util/browser.ts +15 -0
- package/src/util/platform.ts +1 -1
- package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
- package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
- package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
- package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/provider-commit-message-generator.ts +12 -21
- package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
- package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
- package/src/memory/search/lexical.ts +0 -48
- package/src/providers/failover.ts +0 -186
package/src/memory/retriever.ts
CHANGED
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
IDENTITY_KINDS,
|
|
30
30
|
PREFERENCE_KINDS,
|
|
31
31
|
} from "./search/formatting.js";
|
|
32
|
-
import { recencySearch } from "./search/lexical.js";
|
|
33
32
|
import { isQdrantConnectionError, semanticSearch } from "./search/semantic.js";
|
|
34
33
|
import { applyStaleDemotion, computeStaleness } from "./search/staleness.js";
|
|
35
34
|
import {
|
|
@@ -139,7 +138,7 @@ function buildDegradationStatus(
|
|
|
139
138
|
return {
|
|
140
139
|
semanticUnavailable: true,
|
|
141
140
|
reason,
|
|
142
|
-
fallbackSources: [
|
|
141
|
+
fallbackSources: [],
|
|
143
142
|
};
|
|
144
143
|
}
|
|
145
144
|
|
|
@@ -243,13 +242,12 @@ async function generateQueryEmbedding(
|
|
|
243
242
|
* 1. Build query text (caller provides via buildMemoryQuery)
|
|
244
243
|
* 2. Generate dense + sparse embeddings
|
|
245
244
|
* 3. Hybrid search on Qdrant (dense + sparse RRF fusion)
|
|
246
|
-
* 4.
|
|
247
|
-
* 5.
|
|
248
|
-
* 6.
|
|
249
|
-
* 7.
|
|
250
|
-
* 8.
|
|
251
|
-
* 9.
|
|
252
|
-
* 10. Build two-layer XML injection with budget allocation
|
|
245
|
+
* 4. Deduplicate results
|
|
246
|
+
* 5. Classify tiers (score > 0.6 → tier 1, > 0.4 → tier 2)
|
|
247
|
+
* 6. Enrich item candidates with metadata for staleness
|
|
248
|
+
* 7. Compute staleness per item
|
|
249
|
+
* 8. Demote very_stale tier 1 → tier 2
|
|
250
|
+
* 9. Build two-layer XML injection with budget allocation
|
|
253
251
|
*/
|
|
254
252
|
export async function buildMemoryRecall(
|
|
255
253
|
query: string,
|
|
@@ -327,21 +325,15 @@ export async function buildMemoryRecall(
|
|
|
327
325
|
if (isQdrantConnectionError(err)) {
|
|
328
326
|
log.warn({ err }, "Qdrant unavailable — hybrid search disabled");
|
|
329
327
|
} else {
|
|
330
|
-
log.warn({ err }, "Hybrid search failed
|
|
328
|
+
log.warn({ err }, "Hybrid search failed");
|
|
331
329
|
}
|
|
332
330
|
}
|
|
333
331
|
}
|
|
334
332
|
const hybridSearchMs = Date.now() - hybridSearchStart;
|
|
335
333
|
|
|
336
|
-
// ── Step 4:
|
|
337
|
-
const recencyLimit = 5;
|
|
338
|
-
const recencyCandidates = conversationId
|
|
339
|
-
? recencySearch(conversationId, recencyLimit, excludeMessageIds, scopeIds)
|
|
340
|
-
: [];
|
|
341
|
-
|
|
342
|
-
// ── Step 5: Merge and deduplicate ──────────────────────────────
|
|
334
|
+
// ── Step 4: Deduplicate ────────────────────────────────────────
|
|
343
335
|
const candidateMap = new Map<string, Candidate>();
|
|
344
|
-
for (const c of [...hybridCandidates
|
|
336
|
+
for (const c of [...hybridCandidates]) {
|
|
345
337
|
const existing = candidateMap.get(c.key);
|
|
346
338
|
if (!existing) {
|
|
347
339
|
candidateMap.set(c.key, { ...c });
|
|
@@ -356,8 +348,7 @@ export async function buildMemoryRecall(
|
|
|
356
348
|
existing.text = c.text;
|
|
357
349
|
}
|
|
358
350
|
// Propagate metadata that the first source may lack (e.g. legacy
|
|
359
|
-
// Qdrant points missing conversation_id / message_id).
|
|
360
|
-
// source always has these from the DB, so merging fills the gap.
|
|
351
|
+
// Qdrant points missing conversation_id / message_id).
|
|
361
352
|
if (c.conversationId && !existing.conversationId) {
|
|
362
353
|
existing.conversationId = c.conversationId;
|
|
363
354
|
}
|
|
@@ -366,7 +357,7 @@ export async function buildMemoryRecall(
|
|
|
366
357
|
}
|
|
367
358
|
}
|
|
368
359
|
|
|
369
|
-
// ── Step
|
|
360
|
+
// ── Step 4b: Filter out current-conversation segments still in context ──
|
|
370
361
|
// Segments whose source message is still in the conversation's context
|
|
371
362
|
// window are redundant (already visible to the model). However, segments
|
|
372
363
|
// from messages that were removed by context compaction should be kept —
|
|
@@ -443,39 +434,26 @@ export async function buildMemoryRecall(
|
|
|
443
434
|
// Compute RRF-style final scores for the merged candidates
|
|
444
435
|
const allCandidates = [...candidateMap.values()];
|
|
445
436
|
for (const c of allCandidates) {
|
|
446
|
-
//
|
|
447
|
-
//
|
|
448
|
-
|
|
437
|
+
// Multiplicative scoring: importance, confidence, and recency amplify semantic
|
|
438
|
+
// relevance but can't substitute for it. An irrelevant item (semantic ≈ 0)
|
|
439
|
+
// stays low regardless of metadata. Multiplier range: 0.4 (all zero) to 1.0.
|
|
440
|
+
const metadataMultiplier =
|
|
441
|
+
0.4 + c.importance * 0.25 + c.confidence * 0.15 + c.recency * 0.2;
|
|
442
|
+
c.finalScore = c.semantic * metadataMultiplier;
|
|
449
443
|
}
|
|
450
444
|
allCandidates.sort((a, b) => b.finalScore - a.finalScore);
|
|
451
445
|
|
|
452
|
-
// ── Step
|
|
453
|
-
// Recency-only candidates (semantic=0) can never reach the tier 2 threshold
|
|
454
|
-
// (>0.6) since their max finalScore is 0.3. Promote them directly to tier 2
|
|
455
|
-
// so recent conversation context is preserved even without semantic signal.
|
|
456
|
-
const recencyOnlyKeys = new Set(
|
|
457
|
-
allCandidates
|
|
458
|
-
.filter((c) => c.semantic === 0 && c.recency > 0)
|
|
459
|
-
.map((c) => c.key),
|
|
460
|
-
);
|
|
446
|
+
// ── Step 5: Tier classification ─────────────────────────────────
|
|
461
447
|
const tiered = classifyTiers(allCandidates);
|
|
462
|
-
if (recencyOnlyKeys.size > 0) {
|
|
463
|
-
const alreadyTiered = new Set(tiered.map((c) => c.key));
|
|
464
|
-
for (const c of allCandidates) {
|
|
465
|
-
if (recencyOnlyKeys.has(c.key) && !alreadyTiered.has(c.key)) {
|
|
466
|
-
tiered.push({ ...c, tier: 2 });
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
448
|
|
|
471
|
-
// ── Step
|
|
449
|
+
// ── Step 5b: Enrich candidates with source labels ──────────────
|
|
472
450
|
enrichSourceLabels(tiered);
|
|
473
451
|
|
|
474
|
-
// ── Step
|
|
452
|
+
// ── Step 6: Enrich with item metadata for staleness ─────────────
|
|
475
453
|
const itemIds = tiered.filter((c) => c.type === "item").map((c) => c.id);
|
|
476
454
|
const itemMetadataMap = enrichItemMetadata(itemIds);
|
|
477
455
|
|
|
478
|
-
// ── Step
|
|
456
|
+
// ── Step 7: Compute staleness per item ──────────────────────────
|
|
479
457
|
const now = Date.now();
|
|
480
458
|
for (const c of tiered) {
|
|
481
459
|
if (c.type !== "item") continue;
|
|
@@ -492,10 +470,10 @@ export async function buildMemoryRecall(
|
|
|
492
470
|
c.staleness = level;
|
|
493
471
|
}
|
|
494
472
|
|
|
495
|
-
// ── Step
|
|
473
|
+
// ── Step 8: Demote very_stale tier 1 → tier 2 ──────────────────
|
|
496
474
|
const afterDemotion = applyStaleDemotion(tiered);
|
|
497
475
|
|
|
498
|
-
// ── Step
|
|
476
|
+
// ── Step 9: Budget allocation and two-layer injection ──────────
|
|
499
477
|
const maxInjectTokens = Math.max(
|
|
500
478
|
1,
|
|
501
479
|
Math.floor(
|
|
@@ -581,7 +559,6 @@ export async function buildMemoryRecall(
|
|
|
581
559
|
{
|
|
582
560
|
query: truncate(query, 120),
|
|
583
561
|
hybridHits: hybridCandidates.length,
|
|
584
|
-
recencyHits: recencyCandidates.length,
|
|
585
562
|
mergedCount: allCandidates.length,
|
|
586
563
|
tier1Count,
|
|
587
564
|
tier2Count,
|
|
@@ -602,7 +579,6 @@ export async function buildMemoryRecall(
|
|
|
602
579
|
provider: embeddingResult.provider,
|
|
603
580
|
model: embeddingResult.model,
|
|
604
581
|
semanticHits: hybridCandidates.length,
|
|
605
|
-
recencyHits: recencyCandidates.length,
|
|
606
582
|
mergedCount: allCandidates.length,
|
|
607
583
|
selectedCount,
|
|
608
584
|
injectedTokens: estimateTextTokens(injectedText),
|
|
@@ -887,7 +863,6 @@ function emptyResult(
|
|
|
887
863
|
provider: init.provider,
|
|
888
864
|
model: init.model,
|
|
889
865
|
semanticHits: 0,
|
|
890
|
-
recencyHits: 0,
|
|
891
866
|
mergedCount: 0,
|
|
892
867
|
selectedCount: 0,
|
|
893
868
|
injectedTokens: 0,
|
|
@@ -56,6 +56,8 @@ export const memoryItems = sqliteTable(
|
|
|
56
56
|
supersedes: text("supersedes"),
|
|
57
57
|
supersededBy: text("superseded_by"),
|
|
58
58
|
overrideConfidence: text("override_confidence").default("inferred"),
|
|
59
|
+
sourceType: text("source_type").notNull().default("extraction"),
|
|
60
|
+
sourceMessageRole: text("source_message_role"),
|
|
59
61
|
},
|
|
60
62
|
(table) => [
|
|
61
63
|
index("idx_memory_items_scope_id").on(table.scopeId),
|
|
@@ -80,15 +80,6 @@ export const PREFERENCE_KINDS = new Set(["preference", "constraint"]);
|
|
|
80
80
|
/** Kinds classified as capabilities for the <available_capabilities> section. */
|
|
81
81
|
export const CAPABILITY_KINDS = new Set(["capability"]);
|
|
82
82
|
|
|
83
|
-
/** Per-item token budget for tier 1 items. */
|
|
84
|
-
const TIER1_PER_ITEM_TOKENS = 150;
|
|
85
|
-
|
|
86
|
-
/** Per-item token budget for tier 2 items. */
|
|
87
|
-
const TIER2_PER_ITEM_TOKENS = 100;
|
|
88
|
-
|
|
89
|
-
/** Approximate chars-per-token for truncation (matches token-estimator). */
|
|
90
|
-
const CHARS_PER_TOKEN = 4;
|
|
91
|
-
|
|
92
83
|
/**
|
|
93
84
|
* Build a two-layer XML injection block from tiered candidates.
|
|
94
85
|
*
|
|
@@ -151,38 +142,21 @@ export function buildTwoLayerInjection(params: {
|
|
|
151
142
|
: Infinity;
|
|
152
143
|
|
|
153
144
|
// Render tier 1 items first (identity, relevant context, preferences)
|
|
154
|
-
const identityLines = renderPlainStatements(
|
|
155
|
-
identityItems,
|
|
156
|
-
TIER1_PER_ITEM_TOKENS,
|
|
157
|
-
remainingTokens,
|
|
158
|
-
);
|
|
145
|
+
const identityLines = renderPlainStatements(identityItems, remainingTokens);
|
|
159
146
|
remainingTokens -= estimateTextTokens(identityLines.join("\n"));
|
|
160
147
|
|
|
161
|
-
const relevantEpisodes = renderEpisodes(
|
|
162
|
-
tier1Candidates,
|
|
163
|
-
TIER1_PER_ITEM_TOKENS,
|
|
164
|
-
remainingTokens,
|
|
165
|
-
);
|
|
148
|
+
const relevantEpisodes = renderEpisodes(tier1Candidates, remainingTokens);
|
|
166
149
|
remainingTokens -= estimateTextTokens(relevantEpisodes.join("\n"));
|
|
167
150
|
|
|
168
|
-
const preferenceLines = renderPlainStatements(
|
|
169
|
-
preferences,
|
|
170
|
-
TIER1_PER_ITEM_TOKENS,
|
|
171
|
-
remainingTokens,
|
|
172
|
-
);
|
|
151
|
+
const preferenceLines = renderPlainStatements(preferences, remainingTokens);
|
|
173
152
|
remainingTokens -= estimateTextTokens(preferenceLines.join("\n"));
|
|
174
153
|
|
|
175
|
-
const capabilityLines = renderPlainStatements(
|
|
176
|
-
capabilities,
|
|
177
|
-
TIER1_PER_ITEM_TOKENS,
|
|
178
|
-
remainingTokens,
|
|
179
|
-
);
|
|
154
|
+
const capabilityLines = renderPlainStatements(capabilities, remainingTokens);
|
|
180
155
|
remainingTokens -= estimateTextTokens(capabilityLines.join("\n"));
|
|
181
156
|
|
|
182
157
|
// Tier 2 uses remaining budget
|
|
183
158
|
const possiblyRelevantEpisodes = renderEpisodesWithStaleness(
|
|
184
159
|
tier2Candidates,
|
|
185
|
-
TIER2_PER_ITEM_TOKENS,
|
|
186
160
|
remainingTokens,
|
|
187
161
|
);
|
|
188
162
|
|
|
@@ -229,15 +203,13 @@ export function buildTwoLayerInjection(params: {
|
|
|
229
203
|
*/
|
|
230
204
|
function renderPlainStatements(
|
|
231
205
|
items: TieredCandidate[],
|
|
232
|
-
perItemBudgetTokens: number,
|
|
233
206
|
remainingBudget: number,
|
|
234
207
|
): string[] {
|
|
235
208
|
const lines: string[] = [];
|
|
236
209
|
let used = 0;
|
|
237
210
|
for (const item of items) {
|
|
238
211
|
if (used >= remainingBudget) break;
|
|
239
|
-
const
|
|
240
|
-
const text = escapeXmlTags(truncate(item.text, maxChars));
|
|
212
|
+
const text = escapeXmlTags(item.text);
|
|
241
213
|
const tokens = estimateTextTokens(text);
|
|
242
214
|
if (used + tokens > remainingBudget) break;
|
|
243
215
|
lines.push(text);
|
|
@@ -251,15 +223,13 @@ function renderPlainStatements(
|
|
|
251
223
|
*/
|
|
252
224
|
function renderEpisodes(
|
|
253
225
|
items: TieredCandidate[],
|
|
254
|
-
perItemBudgetTokens: number,
|
|
255
226
|
remainingBudget: number,
|
|
256
227
|
): string[] {
|
|
257
228
|
const lines: string[] = [];
|
|
258
229
|
let used = 0;
|
|
259
230
|
for (const item of items) {
|
|
260
231
|
if (used >= remainingBudget) break;
|
|
261
|
-
const
|
|
262
|
-
const text = escapeXmlTags(truncate(item.text, maxChars));
|
|
232
|
+
const text = escapeXmlTags(item.text);
|
|
263
233
|
const sourceAttr = buildSourceAttr(item);
|
|
264
234
|
const line = `<episode${sourceAttr}>\n${text}\n</episode>`;
|
|
265
235
|
const tokens = estimateTextTokens(line);
|
|
@@ -275,15 +245,13 @@ function renderEpisodes(
|
|
|
275
245
|
*/
|
|
276
246
|
function renderEpisodesWithStaleness(
|
|
277
247
|
items: TieredCandidate[],
|
|
278
|
-
perItemBudgetTokens: number,
|
|
279
248
|
remainingBudget: number,
|
|
280
249
|
): string[] {
|
|
281
250
|
const lines: string[] = [];
|
|
282
251
|
let used = 0;
|
|
283
252
|
for (const item of items) {
|
|
284
253
|
if (used >= remainingBudget) break;
|
|
285
|
-
const
|
|
286
|
-
const text = escapeXmlTags(truncate(item.text, maxChars));
|
|
254
|
+
const text = escapeXmlTags(item.text);
|
|
287
255
|
const sourceAttr = buildSourceAttr(item);
|
|
288
256
|
const stalenessAttr =
|
|
289
257
|
item.staleness && item.staleness !== "fresh"
|
|
@@ -347,8 +315,3 @@ function formatShortDate(epochMs: number): string {
|
|
|
347
315
|
}
|
|
348
316
|
return `${month} ${day} ${date.getFullYear()}`;
|
|
349
317
|
}
|
|
350
|
-
|
|
351
|
-
function truncate(text: string, max: number): string {
|
|
352
|
-
if (text.length <= max) return text;
|
|
353
|
-
return `${text.slice(0, max - 3)}...`;
|
|
354
|
-
}
|
|
@@ -8,6 +8,10 @@ const BASE_LIFETIME_MS: Record<string, number> = {
|
|
|
8
8
|
project: 14 * 86_400_000, // 2 weeks
|
|
9
9
|
decision: 14 * 86_400_000, // 2 weeks
|
|
10
10
|
event: 3 * 86_400_000, // 3 days
|
|
11
|
+
// Journals are experiential reflections and forward-looking notes — more
|
|
12
|
+
// durable than ephemeral events or decisions, but not as permanent as
|
|
13
|
+
// identity. 90 days mirrors "preference" lifetime.
|
|
14
|
+
journal: 90 * 86_400_000, // 3 months
|
|
11
15
|
capability: Infinity,
|
|
12
16
|
};
|
|
13
17
|
|
|
@@ -8,9 +8,17 @@ export interface TieredCandidate extends Candidate {
|
|
|
8
8
|
sourceLabel?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Map a composite relevance score to an injection tier.
|
|
13
|
+
*
|
|
14
|
+
* Thresholds are intentionally set lower than raw-embedding ceilings because
|
|
15
|
+
* the multiplicative scoring pipeline (semantic × recency × metadata) compresses
|
|
16
|
+
* the effective score range. Lowering the gates lets moderately-relevant items
|
|
17
|
+
* surface rather than being silently dropped.
|
|
18
|
+
*/
|
|
11
19
|
export function classifyTier(score: number): Tier | null {
|
|
12
|
-
if (score > 0.
|
|
13
|
-
if (score > 0.
|
|
20
|
+
if (score > 0.6) return 1;
|
|
21
|
+
if (score > 0.4) return 2;
|
|
14
22
|
return null;
|
|
15
23
|
}
|
|
16
24
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type CandidateType = "segment" | "item" | "summary" | "media";
|
|
2
|
-
export type CandidateSource = "semantic"
|
|
2
|
+
export type CandidateSource = "semantic";
|
|
3
3
|
|
|
4
4
|
export type StalenessLevel = "fresh" | "aging" | "stale" | "very_stale";
|
|
5
5
|
|
|
@@ -39,12 +39,10 @@ export type DegradationReason =
|
|
|
39
39
|
| "qdrant_unavailable"
|
|
40
40
|
| "embedding_generation_failed";
|
|
41
41
|
|
|
42
|
-
export type FallbackSource = "recency";
|
|
43
|
-
|
|
44
42
|
export interface DegradationStatus {
|
|
45
43
|
semanticUnavailable: boolean;
|
|
46
44
|
reason: DegradationReason;
|
|
47
|
-
fallbackSources:
|
|
45
|
+
fallbackSources: string[];
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
export interface MemoryRecallResult {
|
|
@@ -55,7 +53,6 @@ export interface MemoryRecallResult {
|
|
|
55
53
|
provider?: string;
|
|
56
54
|
model?: string;
|
|
57
55
|
semanticHits: number;
|
|
58
|
-
recencyHits: number;
|
|
59
56
|
mergedCount: number;
|
|
60
57
|
selectedCount: number;
|
|
61
58
|
injectedTokens: number;
|
|
@@ -24,14 +24,14 @@ export function isConversationFailed(conversationId: string): boolean {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Invalidate
|
|
27
|
+
* Invalidate assistant-extracted memory items sourced *exclusively* from
|
|
28
28
|
* messages in the given conversation. Called when a background task or
|
|
29
29
|
* schedule fails — the assistant's optimistic claims (e.g., "I booked an
|
|
30
30
|
* appointment") are not trustworthy if the task didn't complete.
|
|
31
31
|
*
|
|
32
32
|
* The failed state is derived from durable storage (task_runs / cron_runs),
|
|
33
33
|
* so any pending or future extraction jobs for this conversation are blocked
|
|
34
|
-
* from creating new
|
|
34
|
+
* from creating new assistant-extracted items — even after daemon restarts.
|
|
35
35
|
*
|
|
36
36
|
* Items that also have sources from other conversations are left alone
|
|
37
37
|
* only when those conversations come from non-failed task/schedule runs
|
|
@@ -55,7 +55,8 @@ export function invalidateAssistantInferredItemsForConversation(
|
|
|
55
55
|
`UPDATE memory_items
|
|
56
56
|
SET status = 'invalidated',
|
|
57
57
|
invalid_at = ?
|
|
58
|
-
WHERE
|
|
58
|
+
WHERE source_type = 'extraction'
|
|
59
|
+
AND source_message_role = 'assistant'
|
|
59
60
|
AND status = 'active'
|
|
60
61
|
AND id IN (
|
|
61
62
|
SELECT mis.memory_item_id
|
|
@@ -13,7 +13,12 @@ import { mintDaemonDeliveryToken } from "../../runtime/auth/token-service.js";
|
|
|
13
13
|
import { deliverChannelReply } from "../../runtime/gateway-client.js";
|
|
14
14
|
import { getLogger } from "../../util/logger.js";
|
|
15
15
|
import { isConversationSeedSane } from "../conversation-seed-composer.js";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
buildAccessRequestIdentityLine,
|
|
18
|
+
buildAccessRequestInviteDirective,
|
|
19
|
+
nonEmpty,
|
|
20
|
+
sanitizeIdentityField,
|
|
21
|
+
} from "../copy-composer.js";
|
|
17
22
|
import type {
|
|
18
23
|
ChannelAdapter,
|
|
19
24
|
ChannelDeliveryPayload,
|
|
@@ -41,6 +46,149 @@ function resolveSlackMessageText(payload: ChannelDeliveryPayload): string {
|
|
|
41
46
|
return payload.sourceEventName.replace(/[._]/g, " ");
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Block Kit helpers for access request notifications
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build Block Kit blocks for an access request notification.
|
|
55
|
+
*
|
|
56
|
+
* Returns an array of Slack Block Kit block objects with structured layout:
|
|
57
|
+
* - Header: "New access request"
|
|
58
|
+
* - Section: requester identity details
|
|
59
|
+
* - Optional context: message preview
|
|
60
|
+
* - Context: approval code instructions + invite directive
|
|
61
|
+
*/
|
|
62
|
+
export function buildAccessRequestBlocks(
|
|
63
|
+
payload: Record<string, unknown>,
|
|
64
|
+
): unknown[] {
|
|
65
|
+
const blocks: unknown[] = [];
|
|
66
|
+
|
|
67
|
+
// Header
|
|
68
|
+
blocks.push({
|
|
69
|
+
type: "header",
|
|
70
|
+
text: { type: "plain_text", text: "New access request", emoji: true },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Requester identity section
|
|
74
|
+
const identityLine = buildAccessRequestIdentityLine(payload);
|
|
75
|
+
blocks.push({
|
|
76
|
+
type: "section",
|
|
77
|
+
text: { type: "mrkdwn", text: identityLine },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Build fields for structured requester details
|
|
81
|
+
const fields: Array<{ type: "mrkdwn"; text: string }> = [];
|
|
82
|
+
|
|
83
|
+
const senderIdentifier = nonEmpty(
|
|
84
|
+
typeof payload.senderIdentifier === "string"
|
|
85
|
+
? sanitizeIdentityField(payload.senderIdentifier)
|
|
86
|
+
: undefined,
|
|
87
|
+
);
|
|
88
|
+
if (senderIdentifier) {
|
|
89
|
+
fields.push({ type: "mrkdwn", text: `*Name:*\n${senderIdentifier}` });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const actorUsername = nonEmpty(
|
|
93
|
+
typeof payload.actorUsername === "string"
|
|
94
|
+
? sanitizeIdentityField(payload.actorUsername)
|
|
95
|
+
: undefined,
|
|
96
|
+
);
|
|
97
|
+
if (actorUsername) {
|
|
98
|
+
fields.push({ type: "mrkdwn", text: `*Username:*\n@${actorUsername}` });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const sourceChannel = nonEmpty(
|
|
102
|
+
typeof payload.sourceChannel === "string"
|
|
103
|
+
? payload.sourceChannel
|
|
104
|
+
: undefined,
|
|
105
|
+
);
|
|
106
|
+
if (sourceChannel) {
|
|
107
|
+
fields.push({ type: "mrkdwn", text: `*Channel:*\n${sourceChannel}` });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const actorExternalId = nonEmpty(
|
|
111
|
+
typeof payload.actorExternalId === "string"
|
|
112
|
+
? sanitizeIdentityField(payload.actorExternalId)
|
|
113
|
+
: undefined,
|
|
114
|
+
);
|
|
115
|
+
if (actorExternalId && actorExternalId !== senderIdentifier) {
|
|
116
|
+
fields.push({ type: "mrkdwn", text: `*ID:*\n${actorExternalId}` });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (fields.length > 0) {
|
|
120
|
+
blocks.push({
|
|
121
|
+
type: "section",
|
|
122
|
+
fields,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Previously revoked warning
|
|
127
|
+
const previousMemberStatus =
|
|
128
|
+
typeof payload.previousMemberStatus === "string"
|
|
129
|
+
? payload.previousMemberStatus
|
|
130
|
+
: undefined;
|
|
131
|
+
if (previousMemberStatus === "revoked") {
|
|
132
|
+
blocks.push({
|
|
133
|
+
type: "context",
|
|
134
|
+
elements: [
|
|
135
|
+
{
|
|
136
|
+
type: "mrkdwn",
|
|
137
|
+
text: ":warning: This user was previously revoked.",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Divider before instructions
|
|
144
|
+
blocks.push({ type: "divider" });
|
|
145
|
+
|
|
146
|
+
// Approval code instructions
|
|
147
|
+
const requestCode = nonEmpty(
|
|
148
|
+
typeof payload.requestCode === "string" ? payload.requestCode : undefined,
|
|
149
|
+
);
|
|
150
|
+
if (requestCode) {
|
|
151
|
+
const code = requestCode.toUpperCase();
|
|
152
|
+
blocks.push({
|
|
153
|
+
type: "section",
|
|
154
|
+
text: {
|
|
155
|
+
type: "mrkdwn",
|
|
156
|
+
text: `Reply *\`${code} approve\`* to grant access or *\`${code} reject\`* to deny.`,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Invite directive
|
|
162
|
+
const inviteDirective = buildAccessRequestInviteDirective();
|
|
163
|
+
blocks.push({
|
|
164
|
+
type: "context",
|
|
165
|
+
elements: [{ type: "mrkdwn", text: inviteDirective }],
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Guardian verification note
|
|
169
|
+
const guardianResolutionSource =
|
|
170
|
+
typeof payload.guardianResolutionSource === "string"
|
|
171
|
+
? payload.guardianResolutionSource
|
|
172
|
+
: undefined;
|
|
173
|
+
if (
|
|
174
|
+
(guardianResolutionSource === "vellum-anchor" ||
|
|
175
|
+
guardianResolutionSource === "none") &&
|
|
176
|
+
sourceChannel
|
|
177
|
+
) {
|
|
178
|
+
blocks.push({
|
|
179
|
+
type: "context",
|
|
180
|
+
elements: [
|
|
181
|
+
{
|
|
182
|
+
type: "mrkdwn",
|
|
183
|
+
text: `_You haven't verified your identity on ${sourceChannel} yet. If this was you trying to message your assistant, say "help me verify as guardian on ${sourceChannel}" to set up direct access._`,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return blocks;
|
|
190
|
+
}
|
|
191
|
+
|
|
44
192
|
export class SlackAdapter implements ChannelAdapter {
|
|
45
193
|
readonly channel: NotificationChannel = "slack";
|
|
46
194
|
|
|
@@ -65,12 +213,26 @@ export class SlackAdapter implements ChannelAdapter {
|
|
|
65
213
|
|
|
66
214
|
const messageText = resolveSlackMessageText(payload);
|
|
67
215
|
|
|
216
|
+
// Build Block Kit blocks for access request notifications
|
|
217
|
+
const isAccessRequest =
|
|
218
|
+
payload.sourceEventName === "ingress.access_request" &&
|
|
219
|
+
payload.contextPayload != null;
|
|
220
|
+
|
|
68
221
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
222
|
+
if (isAccessRequest) {
|
|
223
|
+
const blocks = buildAccessRequestBlocks(payload.contextPayload!);
|
|
224
|
+
await deliverChannelReply(
|
|
225
|
+
deliverUrl,
|
|
226
|
+
{ chatId, text: messageText, blocks },
|
|
227
|
+
mintDaemonDeliveryToken(),
|
|
228
|
+
);
|
|
229
|
+
} else {
|
|
230
|
+
await deliverChannelReply(
|
|
231
|
+
deliverUrl,
|
|
232
|
+
{ chatId, text: messageText, useBlocks: true },
|
|
233
|
+
mintDaemonDeliveryToken(),
|
|
234
|
+
);
|
|
235
|
+
}
|
|
74
236
|
|
|
75
237
|
log.info(
|
|
76
238
|
{ sourceEventName: payload.sourceEventName, chatId },
|
|
@@ -99,7 +99,14 @@ export function buildAccessRequestIdentityLine(
|
|
|
99
99
|
const sanitizedExternalId = actorExternalId
|
|
100
100
|
? sanitizeIdentityField(actorExternalId)
|
|
101
101
|
: undefined;
|
|
102
|
-
|
|
102
|
+
// When the requester is a raw Slack user ID (e.g. the fallback path in
|
|
103
|
+
// access-request-helper sets senderIdentifier to the raw actorExternalId),
|
|
104
|
+
// format it as a Slack mention so it renders as a clickable display name.
|
|
105
|
+
const formattedRequester =
|
|
106
|
+
sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(requester)
|
|
107
|
+
? `<@${requester}>`
|
|
108
|
+
: requester;
|
|
109
|
+
const parts = [formattedRequester];
|
|
103
110
|
if (sanitizedUsername && sanitizedUsername !== requester) {
|
|
104
111
|
parts.push(`@${sanitizedUsername}`);
|
|
105
112
|
}
|
|
@@ -108,7 +115,13 @@ export function buildAccessRequestIdentityLine(
|
|
|
108
115
|
sanitizedExternalId !== requester &&
|
|
109
116
|
sanitizedExternalId !== sanitizedUsername
|
|
110
117
|
) {
|
|
111
|
-
|
|
118
|
+
// For Slack, use the <@U...> mention format so Slack auto-renders
|
|
119
|
+
// the user ID as a clickable display name.
|
|
120
|
+
const formattedId =
|
|
121
|
+
sourceChannel === "slack" && /^U[A-Z0-9]+$/i.test(sanitizedExternalId)
|
|
122
|
+
? `<@${sanitizedExternalId}>`
|
|
123
|
+
: `[${sanitizedExternalId}]`;
|
|
124
|
+
parts.push(formattedId);
|
|
112
125
|
}
|
|
113
126
|
if (sourceChannel) {
|
|
114
127
|
parts.push(`via ${sourceChannel}`);
|
|
@@ -117,6 +130,46 @@ export function buildAccessRequestIdentityLine(
|
|
|
117
130
|
return `${parts.join(" ")} is requesting access to the assistant.`;
|
|
118
131
|
}
|
|
119
132
|
|
|
133
|
+
export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sanitize an untrusted message preview for inclusion in notification copy.
|
|
137
|
+
*
|
|
138
|
+
* Like {@link sanitizeIdentityField} but uses the higher
|
|
139
|
+
* MESSAGE_PREVIEW_MAX_LENGTH limit (200 chars) instead of the identity
|
|
140
|
+
* field limit (120 chars).
|
|
141
|
+
*/
|
|
142
|
+
export function sanitizeMessagePreview(value: string): string {
|
|
143
|
+
const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
|
|
144
|
+
const clamped =
|
|
145
|
+
stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
|
|
146
|
+
? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
|
|
147
|
+
: stripped;
|
|
148
|
+
return clamped;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build a quoted preview of the requester's original message for inclusion
|
|
153
|
+
* in guardian-facing access-request copy. Sanitizes and truncates to keep
|
|
154
|
+
* the notification concise.
|
|
155
|
+
*
|
|
156
|
+
* Returns `undefined` when no usable preview is available.
|
|
157
|
+
*/
|
|
158
|
+
export function buildAccessRequestMessagePreview(
|
|
159
|
+
payload: Record<string, unknown>,
|
|
160
|
+
): string | undefined {
|
|
161
|
+
const raw =
|
|
162
|
+
typeof payload.messagePreview === "string"
|
|
163
|
+
? payload.messagePreview
|
|
164
|
+
: undefined;
|
|
165
|
+
if (!raw) return undefined;
|
|
166
|
+
|
|
167
|
+
const sanitized = sanitizeMessagePreview(raw);
|
|
168
|
+
if (sanitized.length === 0) return undefined;
|
|
169
|
+
|
|
170
|
+
return `> Their message: "${sanitized}"`;
|
|
171
|
+
}
|
|
172
|
+
|
|
120
173
|
export function buildAccessRequestInviteDirective(): string {
|
|
121
174
|
return 'Reply "open invite flow" to start Trusted Contacts invite flow.';
|
|
122
175
|
}
|
|
@@ -227,6 +280,10 @@ export function buildAccessRequestContractText(
|
|
|
227
280
|
|
|
228
281
|
const lines: string[] = [];
|
|
229
282
|
lines.push(buildAccessRequestIdentityLine(payload));
|
|
283
|
+
const preview = buildAccessRequestMessagePreview(payload);
|
|
284
|
+
if (preview) {
|
|
285
|
+
lines.push(preview);
|
|
286
|
+
}
|
|
230
287
|
if (previousMemberStatus === "revoked") {
|
|
231
288
|
lines.push("Note: this user was previously revoked.");
|
|
232
289
|
}
|