@vellumai/assistant 0.4.3 → 0.4.4
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/.env.example +3 -0
- package/ARCHITECTURE.md +40 -3
- package/README.md +43 -35
- package/package.json +1 -1
- package/scripts/ipc/generate-swift.ts +1 -0
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -120
- package/src/__tests__/actor-token-service.test.ts +1099 -0
- package/src/__tests__/agent-loop.test.ts +51 -0
- package/src/__tests__/approval-routes-http.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +7 -5
- package/src/__tests__/assistant-id-boundary-guard.test.ts +125 -0
- package/src/__tests__/call-controller.test.ts +49 -0
- package/src/__tests__/call-pointer-message-composer.test.ts +171 -0
- package/src/__tests__/call-pointer-messages.test.ts +93 -3
- package/src/__tests__/call-pointer-no-hardcoded-copy.guard.test.ts +42 -0
- package/src/__tests__/callback-handoff-copy.test.ts +186 -0
- package/src/__tests__/channel-approval-routes.test.ts +133 -12
- package/src/__tests__/channel-guardian.test.ts +0 -87
- package/src/__tests__/channel-readiness-service.test.ts +10 -16
- package/src/__tests__/checker.test.ts +33 -12
- package/src/__tests__/config-schema.test.ts +4 -0
- package/src/__tests__/confirmation-request-guardian-bridge.test.ts +410 -0
- package/src/__tests__/conversation-routes-guardian-reply.test.ts +256 -0
- package/src/__tests__/conversation-routes.test.ts +12 -3
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/daemon-server-session-init.test.ts +4 -0
- package/src/__tests__/guardian-actions-endpoint.test.ts +19 -14
- package/src/__tests__/guardian-dispatch.test.ts +8 -0
- package/src/__tests__/guardian-outbound-http.test.ts +4 -4
- package/src/__tests__/guardian-question-mode.test.ts +200 -0
- package/src/__tests__/guardian-routing-invariants.test.ts +178 -0
- package/src/__tests__/guardian-routing-state.test.ts +525 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +2 -0
- package/src/__tests__/handlers-telegram-config.test.ts +0 -83
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +55 -0
- package/src/__tests__/headless-browser-navigate.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +18 -51
- package/src/__tests__/non-member-access-request.test.ts +131 -8
- package/src/__tests__/notification-decision-fallback.test.ts +129 -4
- package/src/__tests__/notification-decision-strategy.test.ts +62 -2
- package/src/__tests__/notification-guardian-path.test.ts +3 -0
- package/src/__tests__/recording-intent-handler.test.ts +1 -0
- package/src/__tests__/relay-server.test.ts +841 -39
- package/src/__tests__/send-endpoint-busy.test.ts +5 -0
- package/src/__tests__/session-agent-loop.test.ts +1 -0
- package/src/__tests__/session-confirmation-signals.test.ts +523 -0
- package/src/__tests__/session-init.benchmark.test.ts +0 -1
- package/src/__tests__/session-surfaces-task-progress.test.ts +1 -1
- package/src/__tests__/session-tool-setup-app-refresh.test.ts +81 -2
- package/src/__tests__/session-tool-setup-memory-scope.test.ts +1 -1
- package/src/__tests__/session-tool-setup-side-effect-flag.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +21 -2
- package/src/__tests__/tool-grant-request-escalation.test.ts +333 -27
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +678 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1064 -0
- package/src/__tests__/twilio-config.test.ts +2 -13
- package/src/agent/loop.ts +1 -1
- package/src/approvals/guardian-decision-primitive.ts +10 -2
- package/src/approvals/guardian-request-resolvers.ts +128 -9
- package/src/calls/call-constants.ts +21 -0
- package/src/calls/call-controller.ts +9 -2
- package/src/calls/call-domain.ts +28 -7
- package/src/calls/call-pointer-message-composer.ts +154 -0
- package/src/calls/call-pointer-messages.ts +106 -27
- package/src/calls/guardian-dispatch.ts +4 -2
- package/src/calls/relay-server.ts +424 -12
- package/src/calls/twilio-config.ts +4 -11
- package/src/calls/twilio-routes.ts +1 -1
- package/src/calls/types.ts +3 -1
- package/src/cli.ts +5 -4
- package/src/config/bundled-skills/agentmail/SKILL.md +4 -0
- package/src/config/bundled-skills/app-builder/SKILL.md +146 -10
- package/src/config/bundled-skills/app-builder/TOOLS.json +1 -1
- package/src/config/bundled-skills/email-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +105 -81
- package/src/config/bundled-skills/messaging/SKILL.md +61 -12
- package/src/config/bundled-skills/messaging/TOOLS.json +58 -0
- package/src/config/bundled-skills/messaging/tools/gmail-sender-digest.ts +6 -1
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +35 -0
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +52 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +30 -39
- package/src/config/bundled-skills/twitter/SKILL.md +3 -3
- package/src/config/bundled-skills/vercel-token-setup/SKILL.md +1 -0
- package/src/config/calls-schema.ts +24 -0
- package/src/config/env.ts +22 -0
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/schema.ts +2 -2
- package/src/config/skills.ts +11 -0
- package/src/config/system-prompt.ts +11 -1
- package/src/config/templates/SOUL.md +2 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +71 -82
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +10 -9
- package/src/config/vellum-skills/twilio-setup/SKILL.md +88 -73
- package/src/daemon/call-pointer-generators.ts +59 -0
- package/src/daemon/computer-use-session.ts +2 -5
- package/src/daemon/handlers/apps.ts +76 -20
- package/src/daemon/handlers/config-channels.ts +5 -55
- package/src/daemon/handlers/config-inbox.ts +9 -3
- package/src/daemon/handlers/config-ingress.ts +28 -3
- package/src/daemon/handlers/config-telegram.ts +12 -0
- package/src/daemon/handlers/config.ts +2 -6
- package/src/daemon/handlers/pairing.ts +2 -0
- package/src/daemon/handlers/sessions.ts +48 -3
- package/src/daemon/handlers/shared.ts +17 -2
- package/src/daemon/ipc-contract/integrations.ts +1 -99
- package/src/daemon/ipc-contract/messages.ts +47 -1
- package/src/daemon/ipc-contract/notifications.ts +11 -0
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +17 -0
- package/src/daemon/server.ts +14 -1
- package/src/daemon/session-agent-loop-handlers.ts +20 -0
- package/src/daemon/session-agent-loop.ts +22 -11
- package/src/daemon/session-lifecycle.ts +1 -1
- package/src/daemon/session-process.ts +11 -1
- package/src/daemon/session-runtime-assembly.ts +3 -0
- package/src/daemon/session-surfaces.ts +3 -2
- package/src/daemon/session.ts +88 -1
- package/src/daemon/tool-side-effects.ts +22 -0
- package/src/home-base/prebuilt/brain-graph.html +1483 -0
- package/src/home-base/prebuilt/index.html +40 -0
- package/src/inbound/platform-callback-registration.ts +157 -0
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/038-actor-token-records.ts +39 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/schema.ts +16 -0
- package/src/messaging/provider-types.ts +24 -0
- package/src/messaging/provider.ts +7 -0
- package/src/messaging/providers/gmail/adapter.ts +127 -0
- package/src/messaging/providers/sms/adapter.ts +40 -37
- package/src/notifications/adapters/macos.ts +45 -2
- package/src/notifications/broadcaster.ts +16 -0
- package/src/notifications/copy-composer.ts +39 -1
- package/src/notifications/decision-engine.ts +22 -9
- package/src/notifications/destination-resolver.ts +16 -2
- package/src/notifications/emit-signal.ts +16 -8
- package/src/notifications/guardian-question-mode.ts +419 -0
- package/src/notifications/signal.ts +14 -3
- package/src/permissions/checker.ts +13 -1
- package/src/permissions/prompter.ts +14 -0
- package/src/providers/anthropic/client.ts +20 -0
- package/src/providers/provider-send-message.ts +15 -3
- package/src/runtime/access-request-helper.ts +71 -1
- package/src/runtime/actor-token-service.ts +234 -0
- package/src/runtime/actor-token-store.ts +236 -0
- package/src/runtime/channel-approvals.ts +5 -3
- package/src/runtime/channel-readiness-service.ts +23 -64
- package/src/runtime/channel-readiness-types.ts +3 -4
- package/src/runtime/channel-retry-sweep.ts +4 -1
- package/src/runtime/confirmation-request-guardian-bridge.ts +197 -0
- package/src/runtime/guardian-action-followup-executor.ts +1 -1
- package/src/runtime/guardian-context-resolver.ts +82 -0
- package/src/runtime/guardian-outbound-actions.ts +0 -3
- package/src/runtime/guardian-reply-router.ts +67 -30
- package/src/runtime/guardian-vellum-migration.ts +57 -0
- package/src/runtime/http-server.ts +65 -12
- package/src/runtime/http-types.ts +13 -0
- package/src/runtime/invite-redemption-service.ts +8 -0
- package/src/runtime/local-actor-identity.ts +76 -0
- package/src/runtime/middleware/actor-token.ts +271 -0
- package/src/runtime/routes/approval-routes.ts +82 -7
- package/src/runtime/routes/brain-graph-routes.ts +222 -0
- package/src/runtime/routes/channel-readiness-routes.ts +71 -0
- package/src/runtime/routes/conversation-routes.ts +140 -52
- package/src/runtime/routes/events-routes.ts +20 -5
- package/src/runtime/routes/guardian-action-routes.ts +45 -3
- package/src/runtime/routes/guardian-approval-interception.ts +29 -0
- package/src/runtime/routes/guardian-bootstrap-routes.ts +145 -0
- package/src/runtime/routes/inbound-message-handler.ts +143 -2
- package/src/runtime/routes/integration-routes.ts +7 -15
- package/src/runtime/routes/pairing-routes.ts +163 -0
- package/src/runtime/routes/twilio-routes.ts +934 -0
- package/src/runtime/tool-grant-request-helper.ts +3 -1
- package/src/security/oauth2.ts +27 -2
- package/src/security/token-manager.ts +46 -10
- package/src/tools/browser/browser-execution.ts +4 -3
- package/src/tools/browser/browser-handoff.ts +10 -18
- package/src/tools/browser/browser-manager.ts +80 -25
- package/src/tools/browser/browser-screencast.ts +35 -119
- package/src/tools/permission-checker.ts +15 -4
- package/src/tools/tool-approval-handler.ts +242 -18
- package/src/__tests__/handlers-twilio-config.test.ts +0 -1928
- package/src/daemon/handlers/config-twilio.ts +0 -1082
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildPointerGenerationPrompt,
|
|
3
|
+
getPointerFallbackMessage,
|
|
4
|
+
includesRequiredFacts,
|
|
5
|
+
POINTER_COPY_MAX_TOKENS,
|
|
6
|
+
POINTER_COPY_SYSTEM_PROMPT,
|
|
7
|
+
POINTER_COPY_TIMEOUT_MS,
|
|
8
|
+
} from '../calls/call-pointer-message-composer.js';
|
|
9
|
+
import { loadConfig } from '../config/loader.js';
|
|
10
|
+
import { getFailoverProvider } from '../providers/registry.js';
|
|
11
|
+
import type { PointerCopyGenerator } from '../runtime/http-types.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create the daemon-owned pointer copy generator that resolves providers
|
|
15
|
+
* and calls `provider.sendMessage` to generate pointer message text.
|
|
16
|
+
* This keeps all provider awareness in the daemon lifecycle, away from
|
|
17
|
+
* the runtime composer.
|
|
18
|
+
*/
|
|
19
|
+
export function createPointerCopyGenerator(): PointerCopyGenerator {
|
|
20
|
+
return async (context, options = {}) => {
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
let provider;
|
|
23
|
+
try {
|
|
24
|
+
provider = getFailoverProvider(config.provider, config.providerOrder);
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fallbackText = options.fallbackText?.trim() || getPointerFallbackMessage(context);
|
|
30
|
+
const requiredFacts = options.requiredFacts
|
|
31
|
+
?.map((f) => f.trim())
|
|
32
|
+
.filter((f) => f.length > 0);
|
|
33
|
+
const prompt = buildPointerGenerationPrompt(context, fallbackText, requiredFacts);
|
|
34
|
+
|
|
35
|
+
const response = await provider.sendMessage(
|
|
36
|
+
[{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
|
37
|
+
[],
|
|
38
|
+
POINTER_COPY_SYSTEM_PROMPT,
|
|
39
|
+
{
|
|
40
|
+
config: {
|
|
41
|
+
max_tokens: options.maxTokens ?? POINTER_COPY_MAX_TOKENS,
|
|
42
|
+
modelIntent: 'latency-optimized',
|
|
43
|
+
},
|
|
44
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? POINTER_COPY_TIMEOUT_MS),
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const block = response.content.find((entry) => entry.type === 'text');
|
|
49
|
+
const text = block && 'text' in block ? block.text.trim() : '';
|
|
50
|
+
if (!text) return null;
|
|
51
|
+
const cleaned = text
|
|
52
|
+
.replace(/^["'`]+/, '')
|
|
53
|
+
.replace(/["'`]+$/, '')
|
|
54
|
+
.trim();
|
|
55
|
+
if (!cleaned) return null;
|
|
56
|
+
if (!includesRequiredFacts(cleaned, requiredFacts)) return null;
|
|
57
|
+
return cleaned;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -79,7 +79,7 @@ export class ComputerUseSession {
|
|
|
79
79
|
private pendingSurfaceActions = new Map<string, {
|
|
80
80
|
resolve: (result: ToolExecutionResult) => void;
|
|
81
81
|
}>();
|
|
82
|
-
private surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData }>();
|
|
82
|
+
private surfaceState = new Map<string, { surfaceType: SurfaceType; data: SurfaceData; title?: string }>();
|
|
83
83
|
private terminalNotified = false;
|
|
84
84
|
private prompter: PermissionPrompter | null = null;
|
|
85
85
|
|
|
@@ -337,7 +337,7 @@ export class ComputerUseSession {
|
|
|
337
337
|
const awaitAction = (input.await_action as boolean) ?? isInteractive;
|
|
338
338
|
|
|
339
339
|
// Track surface state for ui_update merging
|
|
340
|
-
this.surfaceState.set(surfaceId, { surfaceType, data });
|
|
340
|
+
this.surfaceState.set(surfaceId, { surfaceType, data, title });
|
|
341
341
|
|
|
342
342
|
this.sendToClient({
|
|
343
343
|
type: 'ui_surface_show',
|
|
@@ -532,9 +532,6 @@ export class ComputerUseSession {
|
|
|
532
532
|
maxTokens: 4096,
|
|
533
533
|
maxInputTokens: cuConfig.contextWindow.maxInputTokens,
|
|
534
534
|
toolChoice: { type: 'any' },
|
|
535
|
-
// Allow MAX_STEPS non-terminal actions plus one terminal turn
|
|
536
|
-
// (computer_use_done/computer_use_respond), since AgentLoop caps tool turns globally.
|
|
537
|
-
maxToolUseTurns: MAX_STEPS + 1,
|
|
538
535
|
},
|
|
539
536
|
toolDefs,
|
|
540
537
|
toolExecutor,
|
|
@@ -7,6 +7,8 @@ import { v4 as uuid } from 'uuid';
|
|
|
7
7
|
|
|
8
8
|
import { packageApp } from '../../bundler/app-bundler.js';
|
|
9
9
|
import { defaultGallery } from '../../gallery/default-gallery.js';
|
|
10
|
+
import { resolveHomeBaseAppId } from '../../home-base/bootstrap.js';
|
|
11
|
+
import { isPrebuiltHomeBaseApp } from '../../home-base/prebuilt-home-base-updater.js';
|
|
10
12
|
import { getAppDiff, getAppFileAtVersion, getAppHistory, restoreAppVersion } from '../../memory/app-git-service.js';
|
|
11
13
|
import { createApp, createAppRecord, deleteAppRecord, getApp, getAppPreview, listApps, queryAppRecords, updateApp,updateAppRecord } from '../../memory/app-store.js';
|
|
12
14
|
import { createSharedAppLink } from '../../memory/shared-app-links-store.js';
|
|
@@ -77,28 +79,54 @@ export function handleAppDataRequest(
|
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
export function handleAppOpenRequest(msg: { appId: string }, socket: net.Socket, ctx: HandlerContext): void {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
try {
|
|
83
|
+
const appId = msg.appId;
|
|
84
|
+
if (!appId) {
|
|
85
|
+
ctx.send(socket, { type: 'error', message: 'app_open_request requires appId' });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const app = getApp(appId);
|
|
90
|
+
if (app) {
|
|
91
|
+
const surfaceId = `app-open-${uuid()}`;
|
|
92
|
+
ctx.send(socket, {
|
|
93
|
+
type: 'ui_surface_show',
|
|
94
|
+
sessionId: 'app-panel',
|
|
95
|
+
surfaceId,
|
|
96
|
+
surfaceType: 'dynamic_page',
|
|
97
|
+
title: app.name,
|
|
98
|
+
data: { html: app.htmlDefinition, appId: app.id, appType: app.appType },
|
|
99
|
+
display: 'panel',
|
|
100
|
+
} as UiSurfaceShow);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fallback: the ID might be a surfaceId from an ephemeral ui_show surface
|
|
105
|
+
// (not a persistent app). Search active sessions for cached surface data.
|
|
106
|
+
for (const session of ctx.sessions.values()) {
|
|
107
|
+
const cached = session.surfaceState.get(appId);
|
|
108
|
+
if (cached && cached.surfaceType === 'dynamic_page') {
|
|
109
|
+
const newSurfaceId = `app-open-${uuid()}`;
|
|
110
|
+
ctx.send(socket, {
|
|
111
|
+
type: 'ui_surface_show',
|
|
112
|
+
sessionId: 'app-panel',
|
|
113
|
+
surfaceId: newSurfaceId,
|
|
114
|
+
surfaceType: 'dynamic_page',
|
|
115
|
+
title: cached.title ?? (cached.data as { preview?: { title?: string } }).preview?.title,
|
|
116
|
+
data: cached.data,
|
|
117
|
+
display: 'panel',
|
|
118
|
+
} as UiSurfaceShow);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
85
122
|
|
|
86
|
-
|
|
87
|
-
if (!app) {
|
|
123
|
+
log.warn({ appId }, 'App not found in store or session surfaces');
|
|
88
124
|
ctx.send(socket, { type: 'error', message: `App not found: ${appId}` });
|
|
89
|
-
|
|
125
|
+
} catch (err) {
|
|
126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
127
|
+
log.error({ err, appId: msg.appId }, 'Failed to handle app open request');
|
|
128
|
+
ctx.send(socket, { type: 'error', message: `Failed to open app: ${message}` });
|
|
90
129
|
}
|
|
91
|
-
|
|
92
|
-
const surfaceId = `app-open-${uuid()}`;
|
|
93
|
-
ctx.send(socket, {
|
|
94
|
-
type: 'ui_surface_show',
|
|
95
|
-
sessionId: 'app-panel',
|
|
96
|
-
surfaceId,
|
|
97
|
-
surfaceType: 'dynamic_page',
|
|
98
|
-
title: app.name,
|
|
99
|
-
data: { html: app.htmlDefinition, appId: app.id, appType: app.appType },
|
|
100
|
-
display: 'panel',
|
|
101
|
-
} as UiSurfaceShow);
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
export function handleAppUpdatePreview(msg: AppUpdatePreviewRequest, socket: net.Socket, ctx: HandlerContext): void {
|
|
@@ -114,7 +142,35 @@ export function handleAppUpdatePreview(msg: AppUpdatePreviewRequest, socket: net
|
|
|
114
142
|
|
|
115
143
|
export function handleAppsList(socket: net.Socket, ctx: HandlerContext): void {
|
|
116
144
|
try {
|
|
117
|
-
const
|
|
145
|
+
const allApps = listApps();
|
|
146
|
+
const homeBaseId = resolveHomeBaseAppId();
|
|
147
|
+
|
|
148
|
+
// When no home base was found by ID, do a single targeted search for an app
|
|
149
|
+
// matching the HTML marker. listApps() returns metadata-only (no htmlDefinition),
|
|
150
|
+
// so the HTML marker check requires loading the full app from disk. We limit
|
|
151
|
+
// this expensive operation to the case where homeBaseId is null.
|
|
152
|
+
const excludeIds = new Set<string>();
|
|
153
|
+
if (homeBaseId) {
|
|
154
|
+
excludeIds.add(homeBaseId);
|
|
155
|
+
} else {
|
|
156
|
+
for (const a of allApps) {
|
|
157
|
+
if (isPrebuiltHomeBaseApp(a)) {
|
|
158
|
+
excludeIds.add(a.id);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const fullApp = getApp(a.id);
|
|
162
|
+
if (fullApp && isPrebuiltHomeBaseApp(fullApp)) {
|
|
163
|
+
excludeIds.add(a.id);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const apps = allApps.filter((a) => {
|
|
170
|
+
if (excludeIds.has(a.id)) return false;
|
|
171
|
+
if (isPrebuiltHomeBaseApp(a)) return false;
|
|
172
|
+
return true;
|
|
173
|
+
});
|
|
118
174
|
ctx.send(socket, {
|
|
119
175
|
type: 'apps_list_response',
|
|
120
176
|
apps: apps.map((a) => {
|
|
@@ -19,7 +19,6 @@ import {
|
|
|
19
19
|
startOutbound,
|
|
20
20
|
} from '../../runtime/guardian-outbound-actions.js';
|
|
21
21
|
import type {
|
|
22
|
-
ChannelReadinessRequest,
|
|
23
22
|
GuardianVerificationRequest,
|
|
24
23
|
GuardianVerificationResponse,
|
|
25
24
|
} from '../ipc-protocol.js';
|
|
@@ -60,7 +59,6 @@ export function getReadinessService(): ChannelReadinessService {
|
|
|
60
59
|
|
|
61
60
|
export function createGuardianChallenge(
|
|
62
61
|
channel?: ChannelId,
|
|
63
|
-
assistantId?: string,
|
|
64
62
|
rebind?: boolean,
|
|
65
63
|
sessionId?: string,
|
|
66
64
|
): GuardianVerificationResult {
|
|
@@ -89,7 +87,6 @@ export function createGuardianChallenge(
|
|
|
89
87
|
|
|
90
88
|
export function getGuardianStatus(
|
|
91
89
|
channel?: ChannelId,
|
|
92
|
-
_assistantId?: string,
|
|
93
90
|
): GuardianVerificationResult {
|
|
94
91
|
const resolvedAssistantId = DAEMON_INTERNAL_ASSISTANT_ID;
|
|
95
92
|
const resolvedChannel = channel ?? 'telegram';
|
|
@@ -166,10 +163,10 @@ export function handleGuardianVerification(
|
|
|
166
163
|
|
|
167
164
|
try {
|
|
168
165
|
if (msg.action === 'create_challenge') {
|
|
169
|
-
const result = createGuardianChallenge(channel,
|
|
166
|
+
const result = createGuardianChallenge(channel, msg.rebind, msg.sessionId);
|
|
170
167
|
ctx.send(socket, { type: 'guardian_verification_response', ...result });
|
|
171
168
|
} else if (msg.action === 'status') {
|
|
172
|
-
const result = getGuardianStatus(channel
|
|
169
|
+
const result = getGuardianStatus(channel);
|
|
173
170
|
ctx.send(socket, { type: 'guardian_verification_response', ...result });
|
|
174
171
|
} else if (msg.action === 'revoke') {
|
|
175
172
|
// Capture binding before revoking so we can revoke the guardian's
|
|
@@ -198,13 +195,13 @@ export function handleGuardianVerification(
|
|
|
198
195
|
channel,
|
|
199
196
|
});
|
|
200
197
|
} else if (msg.action === 'start_outbound') {
|
|
201
|
-
const result = startOutbound({ channel,
|
|
198
|
+
const result = startOutbound({ channel, destination: msg.destination, rebind: msg.rebind, originConversationId: msg.originConversationId });
|
|
202
199
|
ctx.send(socket, { type: 'guardian_verification_response', ...result });
|
|
203
200
|
} else if (msg.action === 'resend_outbound') {
|
|
204
|
-
const result = resendOutbound({ channel,
|
|
201
|
+
const result = resendOutbound({ channel, originConversationId: msg.originConversationId });
|
|
205
202
|
ctx.send(socket, { type: 'guardian_verification_response', ...result });
|
|
206
203
|
} else if (msg.action === 'cancel_outbound') {
|
|
207
|
-
const result = cancelOutbound({ channel
|
|
204
|
+
const result = cancelOutbound({ channel });
|
|
208
205
|
ctx.send(socket, { type: 'guardian_verification_response', ...result });
|
|
209
206
|
} else {
|
|
210
207
|
ctx.send(socket, {
|
|
@@ -227,53 +224,6 @@ export function handleGuardianVerification(
|
|
|
227
224
|
}
|
|
228
225
|
|
|
229
226
|
|
|
230
|
-
// ---------------------------------------------------------------------------
|
|
231
|
-
// Channel readiness handler
|
|
232
|
-
// ---------------------------------------------------------------------------
|
|
233
|
-
|
|
234
|
-
export async function handleChannelReadiness(
|
|
235
|
-
msg: ChannelReadinessRequest,
|
|
236
|
-
socket: net.Socket,
|
|
237
|
-
ctx: HandlerContext,
|
|
238
|
-
): Promise<void> {
|
|
239
|
-
try {
|
|
240
|
-
const service = getReadinessService();
|
|
241
|
-
|
|
242
|
-
if (msg.action === 'refresh') {
|
|
243
|
-
if (msg.channel) {
|
|
244
|
-
service.invalidateChannel(msg.channel, msg.assistantId);
|
|
245
|
-
} else {
|
|
246
|
-
service.invalidateAll();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const snapshots = await service.getReadiness(msg.channel, msg.includeRemote, msg.assistantId);
|
|
251
|
-
|
|
252
|
-
ctx.send(socket, {
|
|
253
|
-
type: 'channel_readiness_response',
|
|
254
|
-
success: true,
|
|
255
|
-
snapshots: snapshots.map((s) => ({
|
|
256
|
-
channel: s.channel,
|
|
257
|
-
ready: s.ready,
|
|
258
|
-
checkedAt: s.checkedAt,
|
|
259
|
-
stale: s.stale,
|
|
260
|
-
reasons: s.reasons,
|
|
261
|
-
localChecks: s.localChecks,
|
|
262
|
-
remoteChecks: s.remoteChecks,
|
|
263
|
-
})),
|
|
264
|
-
});
|
|
265
|
-
} catch (err) {
|
|
266
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
267
|
-
log.error({ err }, 'Failed to handle channel readiness');
|
|
268
|
-
ctx.send(socket, {
|
|
269
|
-
type: 'channel_readiness_response',
|
|
270
|
-
success: false,
|
|
271
|
-
error: message,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
227
|
export const channelHandlers = defineHandlers({
|
|
277
|
-
channel_readiness: handleChannelReadiness,
|
|
278
228
|
guardian_verification: handleGuardianVerification,
|
|
279
229
|
});
|
|
@@ -305,9 +305,15 @@ async function executeApprove(
|
|
|
305
305
|
}
|
|
306
306
|
}
|
|
307
307
|
session.setAssistantId(assistantId);
|
|
308
|
-
// The guardian approved this escalation
|
|
309
|
-
//
|
|
310
|
-
|
|
308
|
+
// The guardian already approved this escalation via the inbox, so we
|
|
309
|
+
// directly set guardian trust. Going through resolveLocalIpcGuardianContext
|
|
310
|
+
// would look up the vellum binding's guardian ID and compare it against
|
|
311
|
+
// a different channel's binding (e.g. telegram/sms), misclassifying the
|
|
312
|
+
// actor as 'unknown'.
|
|
313
|
+
session.setGuardianContext({
|
|
314
|
+
sourceChannel: sourceChannel ?? 'vellum',
|
|
315
|
+
trustClass: 'guardian',
|
|
316
|
+
});
|
|
311
317
|
session.setCommandIntent(null);
|
|
312
318
|
|
|
313
319
|
// Process the message through the agent loop (no IPC event callback
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
setIngressPublicBaseUrl,
|
|
11
11
|
} from '../../config/env.js';
|
|
12
12
|
import { loadRawConfig, saveRawConfig } from '../../config/loader.js';
|
|
13
|
+
import { registerCallbackRoute, resolveCallbackUrl, shouldUsePlatformCallbacks } from '../../inbound/platform-callback-registration.js';
|
|
13
14
|
import {
|
|
14
15
|
getTwilioSmsWebhookUrl,
|
|
15
16
|
getTwilioStatusCallbackUrl,
|
|
@@ -92,9 +93,21 @@ export async function syncTwilioWebhooks(
|
|
|
92
93
|
ingressConfig: IngressConfig,
|
|
93
94
|
): Promise<{ success: boolean; warning?: string }> {
|
|
94
95
|
try {
|
|
95
|
-
const voiceUrl =
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
const voiceUrl = await resolveCallbackUrl(
|
|
97
|
+
() => getTwilioVoiceWebhookUrl(ingressConfig),
|
|
98
|
+
'webhooks/twilio/voice',
|
|
99
|
+
'twilio_voice',
|
|
100
|
+
);
|
|
101
|
+
const statusCallbackUrl = await resolveCallbackUrl(
|
|
102
|
+
() => getTwilioStatusCallbackUrl(ingressConfig),
|
|
103
|
+
'webhooks/twilio/status',
|
|
104
|
+
'twilio_status',
|
|
105
|
+
);
|
|
106
|
+
const smsUrl = await resolveCallbackUrl(
|
|
107
|
+
() => getTwilioSmsWebhookUrl(ingressConfig),
|
|
108
|
+
'webhooks/twilio/sms',
|
|
109
|
+
'twilio_sms',
|
|
110
|
+
);
|
|
98
111
|
await updatePhoneNumberWebhooks(accountSid, authToken, phoneNumber, {
|
|
99
112
|
voiceUrl,
|
|
100
113
|
statusCallbackUrl,
|
|
@@ -183,6 +196,18 @@ export async function handleIngressConfig(
|
|
|
183
196
|
// Use the effective URL from process.env (which accounts for the
|
|
184
197
|
// fallback branch above) rather than the raw `value` from the UI.
|
|
185
198
|
const effectiveUrl = isEnabled ? getIngressPublicBaseUrl() : undefined;
|
|
199
|
+
|
|
200
|
+
// When containerized with a platform, register the Telegram callback
|
|
201
|
+
// route so the platform knows how to forward Telegram webhooks.
|
|
202
|
+
// This must happen independently of effectiveUrl — in containerized
|
|
203
|
+
// deployments without ingress.publicBaseUrl, platform callbacks are the
|
|
204
|
+
// only way to receive Telegram webhooks.
|
|
205
|
+
if (shouldUsePlatformCallbacks()) {
|
|
206
|
+
registerCallbackRoute('webhooks/telegram', 'telegram').catch((err) => {
|
|
207
|
+
log.warn({ err }, 'Failed to register Telegram platform callback route');
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
186
211
|
triggerGatewayReconcile(effectiveUrl);
|
|
187
212
|
|
|
188
213
|
// Best-effort Twilio webhook reconciliation: when ingress is being
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as net from 'node:net';
|
|
2
2
|
|
|
3
3
|
import { getIngressPublicBaseUrl } from '../../config/env.js';
|
|
4
|
+
import { registerCallbackRoute, shouldUsePlatformCallbacks } from '../../inbound/platform-callback-registration.js';
|
|
4
5
|
import { deleteSecureKey,getSecureKey, setSecureKey } from '../../security/secure-keys.js';
|
|
5
6
|
import { deleteCredentialMetadata, getCredentialMetadata,upsertCredentialMetadata } from '../../tools/credentials/metadata-store.js';
|
|
6
7
|
import type { TelegramConfigRequest, TelegramConfigResponse } from '../ipc-protocol.js';
|
|
@@ -161,6 +162,17 @@ export async function setTelegramConfig(botToken?: string): Promise<TelegramConf
|
|
|
161
162
|
hasWebhookSecret,
|
|
162
163
|
};
|
|
163
164
|
|
|
165
|
+
// When containerized with a platform, register the Telegram callback
|
|
166
|
+
// route so the platform knows how to forward Telegram webhooks.
|
|
167
|
+
// This must happen independently of effectiveUrl — in containerized
|
|
168
|
+
// deployments without ingress.publicBaseUrl, platform callbacks are the
|
|
169
|
+
// only way to receive Telegram webhooks.
|
|
170
|
+
if (shouldUsePlatformCallbacks()) {
|
|
171
|
+
registerCallbackRoute('webhooks/telegram', 'telegram').catch((err) => {
|
|
172
|
+
log.warn({ err }, 'Failed to register Telegram platform callback route');
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
164
176
|
// Trigger gateway reconcile so the webhook registration updates immediately
|
|
165
177
|
const effectiveUrl = getIngressPublicBaseUrl();
|
|
166
178
|
if (effectiveUrl) {
|
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* config-platform.ts — Platform base URL configuration
|
|
12
12
|
* config-integrations.ts — Vercel API & Twitter integration
|
|
13
13
|
* config-telegram.ts — Telegram bot configuration
|
|
14
|
-
* config-
|
|
15
|
-
* config-channels.ts — Channel guardian & readiness
|
|
14
|
+
* config-channels.ts — Channel guardian verification
|
|
16
15
|
* config-tools.ts — Env vars, tool permission simulation, tool names
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
// Re-export individual handlers for direct import by tests and other modules
|
|
20
|
-
export { getReadinessService,
|
|
19
|
+
export { getReadinessService, handleGuardianVerification } from './config-channels.js';
|
|
21
20
|
export { handleHeartbeatChecklistRead, handleHeartbeatChecklistWrite,handleHeartbeatConfig, handleHeartbeatRunNow, handleHeartbeatRunsList } from './config-heartbeat.js';
|
|
22
21
|
export { computeGatewayTarget, handleIngressConfig, syncTwilioWebhooks,triggerGatewayReconcile } from './config-ingress.js';
|
|
23
22
|
export { handleTwitterIntegrationConfig,handleVercelApiConfig } from './config-integrations.js';
|
|
@@ -28,7 +27,6 @@ export { handleShareToSlack, handleSlackWebhookConfig } from './config-slack.js'
|
|
|
28
27
|
export { handleTelegramConfig, summarizeTelegramError } from './config-telegram.js';
|
|
29
28
|
export { handleEnvVarsRequest, handleToolNamesList,handleToolPermissionSimulate } from './config-tools.js';
|
|
30
29
|
export { handleAcceptStarterBundle,handleAddTrustRule, handleRemoveTrustRule, handleTrustRulesList, handleUpdateTrustRule } from './config-trust.js';
|
|
31
|
-
export { handleTwilioConfig } from './config-twilio.js';
|
|
32
30
|
export { broadcastClientSettingsUpdate, handleVoiceConfigUpdate, normalizeActivationKey } from './config-voice.js';
|
|
33
31
|
|
|
34
32
|
// Assemble the combined dispatch map from domain-specific handler groups
|
|
@@ -43,7 +41,6 @@ import { slackHandlers } from './config-slack.js';
|
|
|
43
41
|
import { telegramHandlers } from './config-telegram.js';
|
|
44
42
|
import { toolHandlers } from './config-tools.js';
|
|
45
43
|
import { trustHandlers } from './config-trust.js';
|
|
46
|
-
import { twilioHandlers } from './config-twilio.js';
|
|
47
44
|
import { voiceHandlers } from './config-voice.js';
|
|
48
45
|
|
|
49
46
|
export const configHandlers = {
|
|
@@ -55,7 +52,6 @@ export const configHandlers = {
|
|
|
55
52
|
...platformHandlers,
|
|
56
53
|
...integrationHandlers,
|
|
57
54
|
...telegramHandlers,
|
|
58
|
-
...twilioHandlers,
|
|
59
55
|
...channelHandlers,
|
|
60
56
|
...toolHandlers,
|
|
61
57
|
...heartbeatHandlers,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as net from 'node:net';
|
|
2
2
|
|
|
3
|
+
import { cleanupPairingState } from '../../runtime/routes/pairing-routes.js';
|
|
3
4
|
import {
|
|
4
5
|
approveDevice,
|
|
5
6
|
clearAllDevices,
|
|
@@ -46,6 +47,7 @@ function handlePairingApprovalResponse(
|
|
|
46
47
|
|
|
47
48
|
if (msg.decision === 'deny') {
|
|
48
49
|
pairingStoreRef.deny(msg.pairingRequestId);
|
|
50
|
+
cleanupPairingState(msg.pairingRequestId);
|
|
49
51
|
log.info({ pairingRequestId: msg.pairingRequestId }, 'Pairing request denied');
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
@@ -19,6 +19,7 @@ import { GENERATING_TITLE, queueGenerateConversationTitle, UNTITLED_FALLBACK } f
|
|
|
19
19
|
import * as externalConversationStore from '../../memory/external-conversation-store.js';
|
|
20
20
|
import { DAEMON_INTERNAL_ASSISTANT_ID } from '../../runtime/assistant-scope.js';
|
|
21
21
|
import { routeGuardianReply } from '../../runtime/guardian-reply-router.js';
|
|
22
|
+
import { resolveLocalIpcGuardianContext } from '../../runtime/local-actor-identity.js';
|
|
22
23
|
import * as pendingInteractions from '../../runtime/pending-interactions.js';
|
|
23
24
|
import { checkIngressForSecrets } from '../../security/secret-ingress.js';
|
|
24
25
|
import { compileCustomPatterns, redactSecrets } from '../../security/secret-scanner.js';
|
|
@@ -176,6 +177,10 @@ export async function handleUserMessage(
|
|
|
176
177
|
conversationId: msg.sessionId,
|
|
177
178
|
sourceChannel: ipcChannel,
|
|
178
179
|
});
|
|
180
|
+
// Route prompter-originated events (confirmation_request/secret_request)
|
|
181
|
+
// through the IPC wrapper so pending-interactions + canonical tracking
|
|
182
|
+
// are updated before the message is sent to the client.
|
|
183
|
+
session.updateClient(sendEvent, false);
|
|
179
184
|
const ipcInterface = parseInterfaceId(msg.interface);
|
|
180
185
|
if (!ipcInterface) {
|
|
181
186
|
ctx.send(socket, {
|
|
@@ -264,6 +269,7 @@ export async function handleUserMessage(
|
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
rlog.info({ source }, 'Processing user message');
|
|
272
|
+
session.emitActivityState('thinking', 'message_dequeued', 'assistant_turn', dispatchRequestId);
|
|
267
273
|
session.setTurnChannelContext({
|
|
268
274
|
userMessageChannel: ipcChannel,
|
|
269
275
|
assistantMessageChannel: ipcChannel,
|
|
@@ -273,9 +279,11 @@ export async function handleUserMessage(
|
|
|
273
279
|
assistantMessageInterface: ipcInterface,
|
|
274
280
|
});
|
|
275
281
|
session.setAssistantId(DAEMON_INTERNAL_ASSISTANT_ID);
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
|
|
282
|
+
// Resolve local IPC actor identity through the same trust pipeline
|
|
283
|
+
// used by HTTP channel ingress. The vellum guardian binding provides
|
|
284
|
+
// the guardianPrincipalId, and resolveGuardianContext classifies the
|
|
285
|
+
// local user as 'guardian' via binding match.
|
|
286
|
+
session.setGuardianContext(resolveLocalIpcGuardianContext(ipcChannel));
|
|
279
287
|
session.setCommandIntent(null);
|
|
280
288
|
// Fire-and-forget: don't block the IPC handler so the connection can
|
|
281
289
|
// continue receiving messages (e.g. cancel, confirmations, or
|
|
@@ -600,9 +608,29 @@ export async function handleUserMessage(
|
|
|
600
608
|
conversationId: msg.sessionId,
|
|
601
609
|
pendingRequestIds: pendingRequestIdsForConversation,
|
|
602
610
|
approvalConversationGenerator: desktopApprovalConversationGenerator,
|
|
611
|
+
emissionContext: {
|
|
612
|
+
source: 'inline_nl',
|
|
613
|
+
causedByRequestId: requestId,
|
|
614
|
+
decisionText: messageText.trim(),
|
|
615
|
+
},
|
|
603
616
|
});
|
|
604
617
|
|
|
605
618
|
if (routerResult.consumed && routerResult.type !== 'nl_keep_pending') {
|
|
619
|
+
// Success-path emissions (approved/denied) are handled centrally
|
|
620
|
+
// by handleConfirmationResponse (called via the resolver chain).
|
|
621
|
+
// However, stale/failed paths never reach handleConfirmationResponse,
|
|
622
|
+
// so we emit resolved_stale here for those cases.
|
|
623
|
+
if (routerResult.requestId && !routerResult.decisionApplied) {
|
|
624
|
+
session.emitConfirmationStateChanged({
|
|
625
|
+
sessionId: msg.sessionId,
|
|
626
|
+
requestId: routerResult.requestId,
|
|
627
|
+
state: 'resolved_stale',
|
|
628
|
+
source: 'inline_nl',
|
|
629
|
+
causedByRequestId: requestId,
|
|
630
|
+
decisionText: messageText.trim(),
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
606
634
|
const consumedChannelMeta = {
|
|
607
635
|
userMessageChannel: ipcChannel,
|
|
608
636
|
assistantMessageChannel: ipcChannel,
|
|
@@ -686,6 +714,19 @@ export async function handleUserMessage(
|
|
|
686
714
|
// will see the denial and can re-request the tool if still needed.
|
|
687
715
|
if (session.hasAnyPendingConfirmation()) {
|
|
688
716
|
rlog.info('Auto-denying pending confirmation(s) due to new user message');
|
|
717
|
+
// Emit authoritative confirmation state for each auto-denied request
|
|
718
|
+
// before the prompter clears them.
|
|
719
|
+
for (const interaction of pendingInteractions.getByConversation(msg.sessionId)) {
|
|
720
|
+
if (interaction.session === session && interaction.kind === 'confirmation') {
|
|
721
|
+
session.emitConfirmationStateChanged({
|
|
722
|
+
sessionId: msg.sessionId,
|
|
723
|
+
requestId: interaction.requestId,
|
|
724
|
+
state: 'denied',
|
|
725
|
+
source: 'auto_deny',
|
|
726
|
+
causedByRequestId: requestId,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
689
730
|
session.denyAllPendingConfirmations();
|
|
690
731
|
// Keep the pending-interaction tracker aligned with the prompter so
|
|
691
732
|
// stale request IDs are not reused as routing candidates.
|
|
@@ -731,6 +772,8 @@ export function handleConfirmationResponse(
|
|
|
731
772
|
msg.decision,
|
|
732
773
|
msg.selectedPattern,
|
|
733
774
|
msg.selectedScope,
|
|
775
|
+
undefined,
|
|
776
|
+
{ source: 'button' },
|
|
734
777
|
);
|
|
735
778
|
syncCanonicalStatusFromIpcConfirmationDecision(msg.requestId, msg.decision);
|
|
736
779
|
pendingInteractions.resolve(msg.requestId);
|
|
@@ -910,6 +953,7 @@ export async function handleSessionCreate(
|
|
|
910
953
|
conversationId: conversation.id,
|
|
911
954
|
sourceChannel: transportChannel,
|
|
912
955
|
});
|
|
956
|
+
session.updateClient(sendEvent, false);
|
|
913
957
|
session.setTurnChannelContext({
|
|
914
958
|
userMessageChannel: transportChannel,
|
|
915
959
|
assistantMessageChannel: transportChannel,
|
|
@@ -1260,6 +1304,7 @@ export async function handleRegenerate(
|
|
|
1260
1304
|
conversationId: msg.sessionId,
|
|
1261
1305
|
sourceChannel: regenerateChannel,
|
|
1262
1306
|
});
|
|
1307
|
+
session.updateClient(sendEvent, false);
|
|
1263
1308
|
const requestId = uuid();
|
|
1264
1309
|
session.traceEmitter.emit('request_received', 'Regenerate requested', {
|
|
1265
1310
|
requestId,
|
|
@@ -338,9 +338,24 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
|
|
|
338
338
|
let currentSegmentParts: string[] = [];
|
|
339
339
|
let hasOpenSegment = false;
|
|
340
340
|
|
|
341
|
+
function joinWithSpacing(parts: string[]): string {
|
|
342
|
+
let result = parts[0] ?? '';
|
|
343
|
+
for (let i = 1; i < parts.length; i++) {
|
|
344
|
+
const prev = result[result.length - 1];
|
|
345
|
+
const next = parts[i][0];
|
|
346
|
+
// Only insert a space when neither side already has whitespace
|
|
347
|
+
if (prev && next && prev !== ' ' && prev !== '\n' && prev !== '\t' &&
|
|
348
|
+
next !== ' ' && next !== '\n' && next !== '\t') {
|
|
349
|
+
result += ' ';
|
|
350
|
+
}
|
|
351
|
+
result += parts[i];
|
|
352
|
+
}
|
|
353
|
+
return result;
|
|
354
|
+
}
|
|
355
|
+
|
|
341
356
|
function finalizeSegment(): void {
|
|
342
357
|
if (hasOpenSegment) {
|
|
343
|
-
textSegments[textSegments.length - 1] = currentSegmentParts
|
|
358
|
+
textSegments[textSegments.length - 1] = joinWithSpacing(currentSegmentParts);
|
|
344
359
|
currentSegmentParts = [];
|
|
345
360
|
hasOpenSegment = false;
|
|
346
361
|
}
|
|
@@ -445,7 +460,7 @@ export function renderHistoryContent(content: unknown): RenderedHistoryContent {
|
|
|
445
460
|
|
|
446
461
|
finalizeSegment();
|
|
447
462
|
|
|
448
|
-
const text = textParts
|
|
463
|
+
const text = joinWithSpacing(textParts);
|
|
449
464
|
let rendered: string;
|
|
450
465
|
if (attachmentParts.length === 0) {
|
|
451
466
|
rendered = text;
|