@pellux/goodvibes-tui 0.25.0 → 0.27.0
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/CHANGELOG.md +22 -3
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +2422 -1040
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +49 -2
- package/src/core/session-recovery.ts +24 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +12 -16
- package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +29 -24
- package/src/daemon/handlers/calendar/index.ts +316 -0
- package/src/daemon/handlers/context.ts +29 -0
- package/src/daemon/handlers/contracts.ts +77 -0
- package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
- package/src/daemon/handlers/drafts/index.ts +17 -0
- package/src/daemon/handlers/drafts/register.ts +331 -0
- package/src/daemon/handlers/email/config.ts +164 -0
- package/src/daemon/handlers/email/index.ts +43 -0
- package/src/daemon/handlers/email/read-handlers.ts +80 -0
- package/src/daemon/handlers/email/runtime.ts +140 -0
- package/src/daemon/handlers/email/validation.ts +147 -0
- package/src/daemon/handlers/email/write-handlers.ts +133 -0
- package/src/daemon/handlers/errors.ts +18 -0
- package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
- package/src/daemon/handlers/inbox/index.ts +211 -0
- package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
- package/src/daemon/{channels → handlers}/inbox/poller.ts +6 -5
- package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
- package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +10 -11
- package/src/daemon/{channels → handlers}/inbox/providers/email.ts +2 -1
- package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +4 -3
- package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +11 -12
- package/src/daemon/handlers/index.ts +107 -0
- package/src/daemon/handlers/register.ts +161 -0
- package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
- package/src/daemon/handlers/remote/backends/index.ts +40 -0
- package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
- package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
- package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
- package/src/daemon/handlers/remote/index.ts +119 -0
- package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
- package/src/daemon/handlers/remote/service.ts +191 -0
- package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
- package/src/daemon/handlers/routing/index.ts +261 -0
- package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
- package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
- package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
- package/src/daemon/handlers/triage/index.ts +57 -0
- package/src/daemon/handlers/triage/integration.ts +213 -0
- package/src/daemon/{triage → handlers/triage}/pipeline.ts +60 -71
- package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
- package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
- package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
- package/src/daemon/handlers/triage/tagger/index.ts +184 -0
- package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
- package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
- package/src/daemon/handlers/triage/types.ts +50 -0
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-ui-state.ts +1 -1
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -59
- package/src/panels/agent-inspector-panel.ts +72 -2
- package/src/panels/agent-logs-panel.ts +127 -19
- package/src/panels/base-panel.ts +2 -1
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +1 -0
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/context-visualizer-panel.ts +24 -14
- package/src/panels/cost-tracker-panel.ts +7 -13
- package/src/panels/debug-panel.ts +11 -22
- package/src/panels/docs-panel.ts +14 -24
- package/src/panels/file-explorer-panel.ts +2 -1
- package/src/panels/file-preview-panel.ts +2 -1
- package/src/panels/git-panel.ts +10 -17
- package/src/panels/marketplace-panel.ts +2 -1
- package/src/panels/memory-panel.ts +2 -1
- package/src/panels/project-planning-panel.ts +5 -4
- package/src/panels/provider-health-panel.ts +56 -67
- package/src/panels/schedule-panel.ts +4 -7
- package/src/panels/session-browser-panel.ts +13 -21
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/tasks-panel.ts +2 -7
- package/src/panels/thinking-panel.ts +6 -11
- package/src/panels/token-budget-panel.ts +31 -15
- package/src/panels/tool-inspector-panel.ts +10 -18
- package/src/panels/work-plan-panel.ts +2 -1
- package/src/panels/wrfc-panel.ts +37 -35
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/process-modal.ts +2 -2
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +52 -36
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/daemon/calendar/index.ts +0 -52
- package/src/daemon/calendar/register.ts +0 -527
- package/src/daemon/channels/drafts/index.ts +0 -22
- package/src/daemon/channels/drafts/register.ts +0 -449
- package/src/daemon/channels/inbox/index.ts +0 -58
- package/src/daemon/channels/inbox/register.ts +0 -247
- package/src/daemon/channels/routing/index.ts +0 -39
- package/src/daemon/channels/routing/register.ts +0 -296
- package/src/daemon/email/index.ts +0 -68
- package/src/daemon/email/register.ts +0 -715
- package/src/daemon/operator/index.ts +0 -43
- package/src/daemon/operator/register-helper.ts +0 -150
- package/src/daemon/operator/surfaces.ts +0 -137
- package/src/daemon/operator/types.ts +0 -207
- package/src/daemon/remote/backends/index.ts +0 -34
- package/src/daemon/remote/index.ts +0 -74
- package/src/daemon/remote/register.ts +0 -411
- package/src/daemon/triage/index.ts +0 -59
- package/src/daemon/triage/integration.ts +0 -179
- package/src/daemon/triage/register.ts +0 -231
- package/src/daemon/triage/tagger.ts +0 -777
- /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
- /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
- /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
- /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
- /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
- /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -99,7 +99,7 @@
|
|
|
99
99
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
100
100
|
"@ast-grep/napi": "^0.42.0",
|
|
101
101
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
102
|
-
"@pellux/goodvibes-sdk": "0.
|
|
102
|
+
"@pellux/goodvibes-sdk": "0.35.0",
|
|
103
103
|
"bash-language-server": "^5.6.0",
|
|
104
104
|
"fuse.js": "^7.1.0",
|
|
105
105
|
"graphql": "^16.13.2",
|
|
@@ -15,6 +15,7 @@ import { getPackageVersion } from './help.ts';
|
|
|
15
15
|
import { classifyProviderSetup } from './provider-classification.ts';
|
|
16
16
|
import { buildCliServicePosture } from './service-posture.ts';
|
|
17
17
|
import { REDACTED_VALUE, collectSensitiveConfigValues, isRedactedValue, redactConfig, redactSerializedSecrets } from './redaction.ts';
|
|
18
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
18
19
|
|
|
19
20
|
interface BundleInspectSummary {
|
|
20
21
|
readonly type: string;
|
|
@@ -43,7 +44,7 @@ function readJsonFile(path: string): { readonly ok: true; readonly value: Record
|
|
|
43
44
|
try {
|
|
44
45
|
return { ok: true, value: JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown> };
|
|
45
46
|
} catch (error) {
|
|
46
|
-
return { ok: false, error:
|
|
47
|
+
return { ok: false, error: summarizeError(error) };
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -10,6 +10,7 @@ import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
|
10
10
|
import type { RuntimeEndpointBinding, RuntimeEndpointId } from './endpoints.ts';
|
|
11
11
|
import { classifyBindPosture, isNetworkFacing } from './network-posture.ts';
|
|
12
12
|
import { redactText } from './redaction.ts';
|
|
13
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
13
14
|
|
|
14
15
|
export interface CliServiceRuntime {
|
|
15
16
|
readonly configManager: ConfigManager;
|
|
@@ -326,7 +327,7 @@ function readLogPosture(path: string | undefined, tailBytes: number): CliService
|
|
|
326
327
|
exists: true,
|
|
327
328
|
size: 0,
|
|
328
329
|
modifiedAt: null,
|
|
329
|
-
readError:
|
|
330
|
+
readError: summarizeError(error),
|
|
330
331
|
};
|
|
331
332
|
}
|
|
332
333
|
}
|
|
@@ -320,10 +320,10 @@ export function appendConversationMessages(
|
|
|
320
320
|
|
|
321
321
|
for (let msgIdx = 0; msgIdx < messages.length; msgIdx++) {
|
|
322
322
|
const message = messages[msgIdx];
|
|
323
|
-
messageLineRegistry[msgIdx] = context.history.getLineCount();
|
|
324
323
|
// absoluteIdx aligns the slice-relative loop counter with the absolute
|
|
325
|
-
// message index used as the key in messageKindRegistry.
|
|
324
|
+
// message index used as the key in messageKindRegistry and messageLineRegistry.
|
|
326
325
|
const absoluteIdx = msgIndexOffset + msgIdx;
|
|
326
|
+
messageLineRegistry[absoluteIdx] = context.history.getLineCount();
|
|
327
327
|
if (message.role === 'user') {
|
|
328
328
|
renderConversationUserMessage(context, message, width);
|
|
329
329
|
} else if (message.role === 'assistant') {
|
package/src/core/conversation.ts
CHANGED
|
@@ -64,7 +64,15 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
64
64
|
private collapseState: Map<string, boolean> = new Map();
|
|
65
65
|
/** Block registry: track rendered blocks for copy/apply. */
|
|
66
66
|
protected blockRegistry: BlockMeta[] = [];
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* Absolute message index -> first rendered line index in the history buffer.
|
|
69
|
+
* Keyed by the message's ABSOLUTE position in the full (unsliced) snapshot —
|
|
70
|
+
* appendConversationMessages writes `messageLineRegistry[absoluteIdx]`, not the
|
|
71
|
+
* slice-relative loop counter — so transcript-event navigation
|
|
72
|
+
* (next/prevTranscriptEventLine), whose event.messageIndex is absolute, resolves
|
|
73
|
+
* correctly even after clearDisplay() renders only a tail slice. Holes (indices
|
|
74
|
+
* not currently rendered) read back as undefined and are filtered out by consumers.
|
|
75
|
+
*/
|
|
68
76
|
private messageLineRegistry: number[] = [];
|
|
69
77
|
/**
|
|
70
78
|
* Registry of rendered line indices for system messages whose kind is
|
|
@@ -91,6 +99,13 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
91
99
|
* Reset to 0 on startStreamingBlock and finalizeStreamingBlock.
|
|
92
100
|
*/
|
|
93
101
|
private _lastStreamRenderMs = 0;
|
|
102
|
+
/**
|
|
103
|
+
* Terminal width captured when the current streaming block was anchored.
|
|
104
|
+
* If the width changes mid-stream, updateStreamingBlock forces a rebuild so
|
|
105
|
+
* streamingStartLine is re-anchored to the new buffer (see rebuildHistory).
|
|
106
|
+
* -1 when no stream is active.
|
|
107
|
+
*/
|
|
108
|
+
private _streamWidth = -1;
|
|
94
109
|
/**
|
|
95
110
|
* Message index at the time of the last clearDisplay() call.
|
|
96
111
|
* rebuildHistory() renders only messages at or after this index, so the
|
|
@@ -229,6 +244,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
229
244
|
// Reset the render throttle so the first delta always renders immediately.
|
|
230
245
|
this.flushHistory();
|
|
231
246
|
this.streamingStartLine = this.history.getLineCount();
|
|
247
|
+
this._streamWidth = this._getWidth();
|
|
232
248
|
this._lastStreamRenderMs = 0;
|
|
233
249
|
}
|
|
234
250
|
|
|
@@ -243,6 +259,13 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
243
259
|
// Throttle renderMarkdown to the 16ms render-coalescer cadence to avoid
|
|
244
260
|
// O(n) parse overhead on every delta token during streaming.
|
|
245
261
|
if (this.streamingStartLine >= 0) {
|
|
262
|
+
// If the terminal was resized mid-stream, force the pending width-change
|
|
263
|
+
// rebuild now. rebuildHistory() re-anchors streamingStartLine to the new
|
|
264
|
+
// buffer (see its streaming branch), so the incremental truncate below
|
|
265
|
+
// targets the correct offset instead of a stale one.
|
|
266
|
+
if (this._getWidth() !== this._streamWidth) {
|
|
267
|
+
this.flushHistory();
|
|
268
|
+
}
|
|
246
269
|
const now = Date.now();
|
|
247
270
|
if (now - this._lastStreamRenderMs >= 16) {
|
|
248
271
|
this._lastStreamRenderMs = now;
|
|
@@ -261,6 +284,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
261
284
|
public override finalizeStreamingBlock(): void {
|
|
262
285
|
super.finalizeStreamingBlock();
|
|
263
286
|
this.streamingStartLine = -1;
|
|
287
|
+
this._streamWidth = -1;
|
|
264
288
|
this._lastStreamRenderMs = 0;
|
|
265
289
|
this.markDirty();
|
|
266
290
|
}
|
|
@@ -367,12 +391,23 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
367
391
|
this.dirty = false;
|
|
368
392
|
|
|
369
393
|
const snapshot = this.getMessageSnapshot();
|
|
394
|
+
// During streaming, the in-progress placeholder (always the last message) is
|
|
395
|
+
// rendered here as EMPTY; the incremental streaming path (updateStreamingBlock)
|
|
396
|
+
// owns its content. This keeps streamingStartLine valid across width-change
|
|
397
|
+
// rebuilds — otherwise the placeholder would be re-rendered with its content at
|
|
398
|
+
// the new width while streamingStartLine still pointed into the old buffer.
|
|
399
|
+
const lastMsg = snapshot[snapshot.length - 1];
|
|
400
|
+
const isStreaming = this.streamingStartLine >= 0 && lastMsg?.role === 'assistant';
|
|
401
|
+
const renderSnapshot = isStreaming
|
|
402
|
+
? [...snapshot.slice(0, -1), { ...lastMsg, content: '' } as Message]
|
|
403
|
+
: snapshot;
|
|
404
|
+
|
|
370
405
|
// When _displayFromMessageIndex > 0, clearDisplay() was called. Only render
|
|
371
406
|
// messages added after the clear — the pre-clear history stays off-screen.
|
|
372
407
|
// On a full rebuild (e.g. width change), reset the display-start to 0 so the
|
|
373
408
|
// user can scroll back to the full history if needed.
|
|
374
409
|
const displayStart = this._displayFromMessageIndex;
|
|
375
|
-
const visibleSnapshot = displayStart > 0 ?
|
|
410
|
+
const visibleSnapshot = displayStart > 0 ? renderSnapshot.slice(displayStart) : renderSnapshot;
|
|
376
411
|
|
|
377
412
|
// Tool messages ARE rendered (as collapsed blocks); this filter is only
|
|
378
413
|
// for determining whether to show the splash screen (tool-only messages
|
|
@@ -388,6 +423,18 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
388
423
|
|
|
389
424
|
this.appendMessages(visibleSnapshot, width, displayStart);
|
|
390
425
|
this.appendedUpTo = snapshot.length;
|
|
426
|
+
|
|
427
|
+
if (isStreaming) {
|
|
428
|
+
// Re-anchor the streaming block to the freshly rebuilt buffer and re-render
|
|
429
|
+
// the in-progress content at the current width, mirroring startStreamingBlock.
|
|
430
|
+
this.streamingStartLine = this.history.getLineCount();
|
|
431
|
+
this._streamWidth = width;
|
|
432
|
+
this._lastStreamRenderMs = 0;
|
|
433
|
+
const streamingContent = lastMsg?.content;
|
|
434
|
+
if (typeof streamingContent === 'string' && streamingContent.length > 0) {
|
|
435
|
+
this.history.addLines(renderMarkdown(streamingContent, width, { isStreaming: true }));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
391
438
|
}
|
|
392
439
|
|
|
393
440
|
/**
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* journal silently and return.
|
|
27
27
|
* 3. If records are found, apply the final record's messages — each journal
|
|
28
28
|
* record carries the full conversation snapshot at that moment, so the
|
|
29
|
-
*
|
|
29
|
+
* record with the newest timestamp is the authoritative post-crash state
|
|
30
|
+
* (resilient to seq collisions across re-inits onto a stale journal file).
|
|
30
31
|
* 4. Rebuild the conversation history and call the snapshot writer so the
|
|
31
32
|
* gap is durably closed before the user sees the restored conversation.
|
|
32
33
|
* 5. Rotate the journal (it is no longer needed as a gap-filler).
|
|
@@ -64,8 +65,6 @@ export interface ReplayIntoConversationResult {
|
|
|
64
65
|
readonly replayed: number;
|
|
65
66
|
/** True if the journal tail was corrupt (quarantined). */
|
|
66
67
|
readonly hadCorruptTail: boolean;
|
|
67
|
-
/** True if the journal had an unrecognised schema version (quarantined). */
|
|
68
|
-
readonly hadVersionMismatch: boolean;
|
|
69
68
|
}
|
|
70
69
|
|
|
71
70
|
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
@@ -85,29 +84,36 @@ export function replayJournalIntoConversation(
|
|
|
85
84
|
try {
|
|
86
85
|
const { records, hadCorruptTail } = replayJournal(journalPath, snapshotTimestamp);
|
|
87
86
|
|
|
88
|
-
// Detect version mismatch: replayJournal quarantines and returns
|
|
89
|
-
// hadCorruptTail=true + 0 records when the header version is wrong.
|
|
90
|
-
// We distinguish it from a genuine corrupt tail by checking whether the
|
|
91
|
-
// journal file still exists (quarantine renames it away in both cases,
|
|
92
|
-
// so we cannot inspect the header at this point). We surface both cases
|
|
93
|
-
// through hadCorruptTail to the caller; hadVersionMismatch is derived
|
|
94
|
-
// from it to give the caller a distinct notice option.
|
|
95
|
-
const hadVersionMismatch = hadCorruptTail && records.length === 0;
|
|
96
|
-
|
|
97
87
|
const journal = openTranscriptJournal(journalPath, sessionId);
|
|
98
88
|
|
|
99
89
|
if (records.length === 0) {
|
|
100
90
|
// Nothing to replay — rotate the (now-stale) journal silently.
|
|
101
91
|
journal.rotate();
|
|
102
|
-
return { replayed: 0, hadCorruptTail
|
|
92
|
+
return { replayed: 0, hadCorruptTail };
|
|
103
93
|
}
|
|
104
94
|
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
95
|
+
// Select the authoritative record by newest wall-clock timestamp (tie-broken
|
|
96
|
+
// by highest seq). Each record carries the full conversation snapshot at its
|
|
97
|
+
// moment, so the newest ts is by definition the most recent state. This is
|
|
98
|
+
// resilient to seq collisions that arise when a fresh process appends to a
|
|
99
|
+
// pre-existing, non-rotated journal: its records restart at seq 0 after the
|
|
100
|
+
// surviving old records, so "last by seq" could otherwise pick a stale one.
|
|
101
|
+
const lastRecord = records.reduce((newest, candidate) =>
|
|
102
|
+
candidate.ts > newest.ts ||
|
|
103
|
+
(candidate.ts === newest.ts && candidate.seq > newest.seq)
|
|
104
|
+
? candidate
|
|
105
|
+
: newest,
|
|
106
|
+
);
|
|
108
107
|
const replayedMessages = lastRecord.messages as ConversationMessageSnapshot[];
|
|
109
108
|
|
|
109
|
+
// Preserve the session identity (title/titleSource/branches/currentBranch)
|
|
110
|
+
// that the resume seam already hydrated onto the live conversation. Journal
|
|
111
|
+
// records carry only messages, so a bare fromJSON would blank the title and
|
|
112
|
+
// reset titleSource to the system default — and the next TURN_COMPLETED
|
|
113
|
+
// snapshot would then persist the empty title, making the loss permanent.
|
|
114
|
+
const preserved = conversation.toJSON();
|
|
110
115
|
conversation.fromJSON({
|
|
116
|
+
...preserved,
|
|
111
117
|
messages: replayedMessages as never[],
|
|
112
118
|
});
|
|
113
119
|
conversation.rebuildHistory();
|
|
@@ -123,10 +129,10 @@ export function replayJournalIntoConversation(
|
|
|
123
129
|
// Rotate the journal — it is no longer needed as a gap-filler.
|
|
124
130
|
journal.rotate();
|
|
125
131
|
|
|
126
|
-
return { replayed: records.length, hadCorruptTail
|
|
132
|
+
return { replayed: records.length, hadCorruptTail };
|
|
127
133
|
} catch {
|
|
128
134
|
// Absolute last-resort guard — recovery must never crash a resume.
|
|
129
|
-
return { replayed: 0, hadCorruptTail: false
|
|
135
|
+
return { replayed: 0, hadCorruptTail: false };
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SystemMessageRouter — routes system messages to the appropriate surfaces
|
|
3
|
-
* based on
|
|
3
|
+
* based on their kind and configured routing target.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Delivery is resolved from the message KIND ('system' | 'wrfc' |
|
|
6
|
+
* 'operational'), which maps to a configurable routing target
|
|
7
|
+
* (ui.systemMessages / ui.wrfcMessages / ui.operationalMessages, each
|
|
8
|
+
* 'panel' | 'conversation' | 'both'). resolveSystemMessageDelivery() turns that
|
|
9
|
+
* target — plus whether a panel is attached — into a { toPanel, toConversation }
|
|
10
|
+
* decision. The priority ('high' | 'low') only sets the panel emphasis; it does
|
|
11
|
+
* not by itself decide conversation delivery.
|
|
12
|
+
*
|
|
13
|
+
* Critical override: errors, provider failovers, and compaction/context notices
|
|
14
|
+
* (see FORCE_CONVERSATION_PREFIXES) ALWAYS also reach the main conversation,
|
|
15
|
+
* regardless of target, so the user never has to open the SystemMessagesPanel
|
|
16
|
+
* to discover them.
|
|
13
17
|
*
|
|
14
18
|
* Usage:
|
|
15
19
|
* ```ts
|
|
@@ -18,9 +22,9 @@
|
|
|
18
22
|
* router.routeSystemMessage('[Session] Saved session abc123', 'high');
|
|
19
23
|
* ```
|
|
20
24
|
*
|
|
21
|
-
*
|
|
22
|
-
* conversation.addSystemMessage()
|
|
23
|
-
* automatically
|
|
25
|
+
* routeAuto() can be used as a drop-in replacement for
|
|
26
|
+
* conversation.addSystemMessage(): it classifies the message kind and priority
|
|
27
|
+
* automatically before routing.
|
|
24
28
|
*
|
|
25
29
|
* @remarks
|
|
26
30
|
* This router handles system messages (operational status, errors). It is
|
|
@@ -28,8 +32,6 @@
|
|
|
28
32
|
* notifications with policy-based routing.
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
|
-
import { getConfigSnapshot } from '../config/index.ts';
|
|
32
|
-
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
33
35
|
import type { ConversationManager } from './conversation';
|
|
34
36
|
import type { SystemMessagesPanel, SystemMessagePriority } from '../panels/system-messages-panel.ts';
|
|
35
37
|
import {
|
|
@@ -46,14 +48,23 @@ export type {
|
|
|
46
48
|
SystemMessageTarget,
|
|
47
49
|
} from '@/runtime/index.ts';
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Message categories that are operationally critical enough that the user must
|
|
53
|
+
* see them inline in the main conversation, regardless of the configured
|
|
54
|
+
* routing target (ui.systemMessages defaults to panel-only). Errors, provider
|
|
55
|
+
* failovers, and compaction/context notices fall here: a user should never have
|
|
56
|
+
* to open the SystemMessagesPanel to discover that a turn errored, a provider
|
|
57
|
+
* failed over, or the context was compacted.
|
|
58
|
+
*
|
|
59
|
+
* Detection is by the stable message prefix tag, mirroring how the SDK
|
|
60
|
+
* classifiers key off message content. This deliberately does NOT force every
|
|
61
|
+
* high-priority message inline (model/provider/session confirmations still
|
|
62
|
+
* honour the configured target), so the routing-target config stays meaningful.
|
|
63
|
+
*/
|
|
64
|
+
const FORCE_CONVERSATION_PREFIXES = ['[Error]', '[Failover]', '[Compaction]', '[Context]'] as const;
|
|
65
|
+
|
|
66
|
+
function mustReachConversation(message: string): boolean {
|
|
67
|
+
return FORCE_CONVERSATION_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
57
68
|
}
|
|
58
69
|
|
|
59
70
|
// ---------------------------------------------------------------------------
|
|
@@ -74,10 +85,11 @@ export class SystemMessageRouter {
|
|
|
74
85
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
75
86
|
|
|
76
87
|
/**
|
|
77
|
-
* Route a system message
|
|
88
|
+
* Route a system message.
|
|
78
89
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
90
|
+
* Delivery is resolved from the kind and its configured target; priority only
|
|
91
|
+
* sets panel emphasis. Critical notices (errors/failover/compaction, see
|
|
92
|
+
* FORCE_CONVERSATION_PREFIXES) are additionally forced into the conversation.
|
|
81
93
|
*
|
|
82
94
|
* @param message - Message text.
|
|
83
95
|
* @param priority - 'high' | 'low'.
|
|
@@ -94,7 +106,11 @@ export class SystemMessageRouter {
|
|
|
94
106
|
if (delivery.toPanel) {
|
|
95
107
|
this.panel?.push(message, priority);
|
|
96
108
|
}
|
|
97
|
-
|
|
109
|
+
// Critical notices (errors, failover, compaction) must surface inline even
|
|
110
|
+
// when the configured target is panel-only — otherwise a user without the
|
|
111
|
+
// SystemMessagesPanel open would never see them.
|
|
112
|
+
const toConversation = delivery.toConversation || mustReachConversation(message);
|
|
113
|
+
if (toConversation) {
|
|
98
114
|
// addTypedSystemMessage threads the kind into the conversation so the
|
|
99
115
|
// renderer can use kind-based navigability instead of substring matching.
|
|
100
116
|
this.conversation.addTypedSystemMessage(message, kind);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { UiRuntimeEvents } from '@/runtime/index.ts';
|
|
2
2
|
import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
|
|
3
|
-
import { maybeAutoCompact } from './context-auto-compact.ts';
|
|
4
3
|
import { logger } from '@pellux/goodvibes-sdk/platform/utils';
|
|
5
4
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
6
5
|
import type { HookDispatcher, HookPhase, HookCategory, HookEventPath } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
@@ -80,17 +79,21 @@ export interface WireTurnEventHandlersResult {
|
|
|
80
79
|
* Responsibilities:
|
|
81
80
|
* - Auto-save conversation to persistent store after each LLM turn
|
|
82
81
|
* - Fire the Lifecycle:session:save hook
|
|
83
|
-
* - Trigger auto-compact when context usage exceeds the configured threshold
|
|
84
82
|
* - Refresh git status after turns and tool results
|
|
85
83
|
*
|
|
84
|
+
* Note: auto-compaction is intentionally NOT performed here. The SDK
|
|
85
|
+
* Orchestrator already runs post-turn context maintenance every turn
|
|
86
|
+
* (handlePostTurnContextMaintenance) using the resolved context window and
|
|
87
|
+
* its own getAutoCompactDecision; re-triggering it from the TUI duplicated
|
|
88
|
+
* that work and raced the SDK's compaction on the shared message array.
|
|
89
|
+
*
|
|
86
90
|
* Returns refreshGit (callable externally) and unsubs (push into parent unsubs).
|
|
87
91
|
*/
|
|
88
92
|
export function wireTurnEventHandlers(
|
|
89
93
|
options: WireTurnEventHandlersOptions,
|
|
90
94
|
): WireTurnEventHandlersResult {
|
|
91
95
|
const {
|
|
92
|
-
events, conversation, runtime,
|
|
93
|
-
providerRegistry, systemMessageRouter, hookDispatcher,
|
|
96
|
+
events, conversation, runtime, configManager, hookDispatcher,
|
|
94
97
|
workingDir, homeDirectory, sessionManager, gitStatusProvider,
|
|
95
98
|
lastGitInfoRef, buildSessionContinuityHints, render, webhookNotifier,
|
|
96
99
|
_clock = Date.now,
|
|
@@ -161,18 +164,11 @@ export function wireTurnEventHandlers(
|
|
|
161
164
|
} catch { /* best-effort */ }
|
|
162
165
|
logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) });
|
|
163
166
|
}
|
|
164
|
-
// Auto-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
providerRegistry: providerRegistry as Parameters<typeof maybeAutoCompact>[0]['providerRegistry'],
|
|
170
|
-
systemMessageRouter: systemMessageRouter as Parameters<typeof maybeAutoCompact>[0]['systemMessageRouter'],
|
|
171
|
-
model: runtime.model,
|
|
172
|
-
provider: runtime.provider,
|
|
173
|
-
lastInputTokens: orchestrator.lastInputTokens,
|
|
174
|
-
contextWindow: currentModelForCompact.contextWindow,
|
|
175
|
-
}).catch((err: unknown) => logger.debug('maybeAutoCompact error', { error: summarizeError(err) }));
|
|
167
|
+
// Auto-compaction is owned by the SDK Orchestrator's post-turn maintenance
|
|
168
|
+
// (handlePostTurnContextMaintenance), which runs on every turn against the
|
|
169
|
+
// resolved context window (getContextWindowForModel) via the SDK's
|
|
170
|
+
// getAutoCompactDecision (percentage threshold + 15k safety buffer +
|
|
171
|
+
// small-window handling). The TUI must not independently re-trigger it.
|
|
176
172
|
refreshGit();
|
|
177
173
|
}));
|
|
178
174
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// CalDAV client for the calendar
|
|
1
|
+
// CalDAV client for the calendar handler surface.
|
|
2
2
|
//
|
|
3
3
|
// Authenticates to the user's CalDAV endpoint using credentials resolved from
|
|
4
4
|
// the daemon credential store (config keys under `surfaces.calendar.*`). It
|
|
5
|
-
// implements the CalDAV/WebDAV verbs the
|
|
5
|
+
// implements the CalDAV/WebDAV verbs the gateway methods need:
|
|
6
6
|
// - REPORT (calendar-query) -> list / get events
|
|
7
7
|
// - PUT -> create / import events
|
|
8
8
|
// - REPORT (multiget) / GET -> export a collection
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
// * Event hrefs are returned to callers as opaque relative identifiers, never
|
|
17
17
|
// as fully-qualified authenticated URLs.
|
|
18
18
|
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
19
|
+
import { HandlerError } from '../errors.ts';
|
|
20
|
+
import type { HandlerContext } from '../context.ts';
|
|
21
|
+
import type { DaemonCredentialStore } from '../credentials.ts';
|
|
22
22
|
import {
|
|
23
23
|
generateCalendar,
|
|
24
24
|
generateICS,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type GenerateICalInput,
|
|
27
27
|
type ParsedICalEvent,
|
|
28
28
|
} from './ics.ts';
|
|
29
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
29
30
|
|
|
30
31
|
// ---------------------------------------------------------------------------
|
|
31
32
|
// Configuration
|
|
@@ -112,12 +113,15 @@ export interface CalDavClient {
|
|
|
112
113
|
exportIcs(opts: { calendarId?: string; from?: string; to?: string }): Promise<ExportResult>;
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/** Subset of the handler context the CalDAV config resolver needs. */
|
|
117
|
+
export type CalDavConfigContext = Pick<HandlerContext, 'configManager' | 'credentials'>;
|
|
118
|
+
|
|
115
119
|
// ---------------------------------------------------------------------------
|
|
116
120
|
// Config resolution
|
|
117
121
|
// ---------------------------------------------------------------------------
|
|
118
122
|
|
|
119
123
|
function readConfigString(
|
|
120
|
-
ctx: Pick<
|
|
124
|
+
ctx: Pick<HandlerContext, 'configManager'>,
|
|
121
125
|
key: string,
|
|
122
126
|
): string | undefined {
|
|
123
127
|
const value = ctx.configManager.get(key as never);
|
|
@@ -147,16 +151,17 @@ function parseCollectionMap(raw: string | undefined): Record<string, string> {
|
|
|
147
151
|
* Resolve the full CalDAV configuration from config + credential store.
|
|
148
152
|
* The password is resolved from the daemon credential store — either as a
|
|
149
153
|
* goodvibes://secrets/ reference stored in config, or the config-key-derived
|
|
150
|
-
* secret. Throws
|
|
154
|
+
* secret. Throws HandlerError when the surface is not configured. The resolved
|
|
155
|
+
* password is held only in memory and never returned to callers.
|
|
151
156
|
*/
|
|
152
157
|
export async function resolveCalDavConfig(
|
|
153
|
-
ctx:
|
|
158
|
+
ctx: CalDavConfigContext,
|
|
154
159
|
): Promise<CalDavConfig> {
|
|
155
|
-
const credentials =
|
|
160
|
+
const credentials: DaemonCredentialStore = ctx.credentials;
|
|
156
161
|
const baseUrl = readConfigString(ctx, CFG_URL);
|
|
157
162
|
const username = readConfigString(ctx, CFG_USER);
|
|
158
163
|
if (!baseUrl || !username) {
|
|
159
|
-
throw new
|
|
164
|
+
throw new HandlerError(
|
|
160
165
|
'CalDAV is not configured. Set surfaces.calendar.caldavUrl and surfaces.calendar.caldavUser.',
|
|
161
166
|
'CALENDAR_NOT_CONFIGURED',
|
|
162
167
|
412,
|
|
@@ -174,7 +179,7 @@ export async function resolveCalDavConfig(
|
|
|
174
179
|
password = await credentials.resolveConfigSecret(CFG_PASSWORD);
|
|
175
180
|
}
|
|
176
181
|
if (!password) {
|
|
177
|
-
throw new
|
|
182
|
+
throw new HandlerError(
|
|
178
183
|
'CalDAV password is not available in the credential store.',
|
|
179
184
|
'CALENDAR_CREDENTIALS_MISSING',
|
|
180
185
|
412,
|
|
@@ -204,7 +209,7 @@ function joinUrl(base: string, segment: string): string {
|
|
|
204
209
|
/** Extract the scheme+host origin from an absolute URL (no trailing slash). */
|
|
205
210
|
export function originOf(url: string): string {
|
|
206
211
|
const match = /^(https?:\/\/[^/]+)/i.exec(url.trim());
|
|
207
|
-
return match ? match[1] : stripTrailingSlash(url);
|
|
212
|
+
return match ? match[1]! : stripTrailingSlash(url);
|
|
208
213
|
}
|
|
209
214
|
|
|
210
215
|
/**
|
|
@@ -227,7 +232,7 @@ function resolveCollectionUrl(baseUrl: string, path: string): string {
|
|
|
227
232
|
export function toRelativeHref(href: string): string {
|
|
228
233
|
const trimmed = href.trim();
|
|
229
234
|
const schemeMatch = /^https?:\/\/[^/]+(\/.*)$/i.exec(trimmed);
|
|
230
|
-
if (schemeMatch) return schemeMatch[1]
|
|
235
|
+
if (schemeMatch) return schemeMatch[1]!;
|
|
231
236
|
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
232
237
|
}
|
|
233
238
|
|
|
@@ -302,7 +307,7 @@ function propfindCalendarsBody(): string {
|
|
|
302
307
|
function toCalDavStamp(iso: string): string {
|
|
303
308
|
const date = new Date(iso);
|
|
304
309
|
if (Number.isNaN(date.getTime())) {
|
|
305
|
-
throw new
|
|
310
|
+
throw new HandlerError(`Invalid date range value: ${iso}`, 'CALENDAR_BAD_RANGE', 400);
|
|
306
311
|
}
|
|
307
312
|
const pad = (n: number): string => (n < 10 ? `0${n}` : String(n));
|
|
308
313
|
return (
|
|
@@ -353,7 +358,7 @@ export function parseMultiStatus(xml: string): MultiStatusEntry[] {
|
|
|
353
358
|
const responseRe = /<(?:[a-zA-Z0-9]+:)?response(?:\s[^>]*)?>([\s\S]*?)<\/(?:[a-zA-Z0-9]+:)?response>/gi;
|
|
354
359
|
let match: RegExpExecArray | null;
|
|
355
360
|
while ((match = responseRe.exec(xml)) !== null) {
|
|
356
|
-
const block = match[1]
|
|
361
|
+
const block = match[1]!;
|
|
357
362
|
const hrefRaw = tagContent(block, 'href');
|
|
358
363
|
if (!hrefRaw) continue;
|
|
359
364
|
const href = decodeXmlEntities(hrefRaw.trim());
|
|
@@ -425,18 +430,18 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
|
|
|
425
430
|
// (e.g. "getaddrinfo ENOTFOUND cal.example.com") leaks the CalDAV
|
|
426
431
|
// hostname/URL into caller-visible output (importIcs errors[]).
|
|
427
432
|
// Never include creds/URL/host or raw fetch detail.
|
|
428
|
-
throw new
|
|
433
|
+
throw new HandlerError('CalDAV request failed: network error.', 'CALENDAR_NETWORK_ERROR', 502);
|
|
429
434
|
}
|
|
430
435
|
const text = await response.text();
|
|
431
436
|
if (!response.ok) {
|
|
432
|
-
// Map auth/permission/not-found to
|
|
437
|
+
// Map auth/permission/not-found to handler errors. Never include creds/URL.
|
|
433
438
|
const status = response.status;
|
|
434
439
|
const code = status === 401 || status === 403
|
|
435
440
|
? 'CALENDAR_AUTH_FAILED'
|
|
436
441
|
: status === 404
|
|
437
442
|
? 'CALENDAR_NOT_FOUND'
|
|
438
443
|
: 'CALENDAR_REQUEST_FAILED';
|
|
439
|
-
throw new
|
|
444
|
+
throw new HandlerError(
|
|
440
445
|
`CalDAV server returned HTTP ${status}.`,
|
|
441
446
|
code,
|
|
442
447
|
status >= 400 && status < 500 ? status : 502,
|
|
@@ -472,7 +477,7 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
|
|
|
472
477
|
const rel = toRelativeHref(entry.href);
|
|
473
478
|
// Derive a stable logical id from the last path segment.
|
|
474
479
|
const segments = rel.split('/').filter((s) => s.length > 0);
|
|
475
|
-
const logical = segments.length > 0 ? decodeURIComponent(segments[segments.length - 1]) : config.defaultCalendarId;
|
|
480
|
+
const logical = segments.length > 0 ? decodeURIComponent(segments[segments.length - 1]!) : config.defaultCalendarId;
|
|
476
481
|
if (seen.has(logical)) continue;
|
|
477
482
|
seen.add(logical);
|
|
478
483
|
calendars.push({ calendarId: logical, displayName: entry.displayName ?? logical });
|
|
@@ -509,7 +514,7 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
|
|
|
509
514
|
single = await request(resourceUrl, 'GET', undefined, { Depth: '0' });
|
|
510
515
|
} catch (error) {
|
|
511
516
|
// A 404 maps to "not found"; any other failure propagates.
|
|
512
|
-
if (error instanceof
|
|
517
|
+
if (error instanceof HandlerError && error.code === 'CALENDAR_NOT_FOUND') {
|
|
513
518
|
return null;
|
|
514
519
|
}
|
|
515
520
|
throw error;
|
|
@@ -517,7 +522,7 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
|
|
|
517
522
|
const parsed = parseICS(single.text);
|
|
518
523
|
if (parsed.length === 0) return null;
|
|
519
524
|
const relHref = toRelativeHref(eventId);
|
|
520
|
-
return { ...parsed[0]
|
|
525
|
+
return { ...parsed[0]!, href: relHref, calendarId: id };
|
|
521
526
|
}
|
|
522
527
|
|
|
523
528
|
// Strategy 2 — bare UID: ask the server for ONLY the matching resource via
|
|
@@ -566,7 +571,7 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
|
|
|
566
571
|
const id = calendarId && calendarId.length > 0 ? calendarId : config.defaultCalendarId;
|
|
567
572
|
const parsed = parseICS(icsContent);
|
|
568
573
|
if (parsed.length === 0) {
|
|
569
|
-
throw new
|
|
574
|
+
throw new HandlerError('No VEVENT components found in .ics content.', 'CALENDAR_EMPTY_ICS', 400);
|
|
570
575
|
}
|
|
571
576
|
const collectionUrl = collectionUrlFor(id);
|
|
572
577
|
const eventIds: string[] = [];
|
|
@@ -582,7 +587,7 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
|
|
|
582
587
|
eventIds.push(toRelativeHref(joinUrl(collectionPathOrRoot(collectionUrl, config.baseUrl), resourcePath)));
|
|
583
588
|
imported += 1;
|
|
584
589
|
} catch (error) {
|
|
585
|
-
const message = error instanceof
|
|
590
|
+
const message = error instanceof HandlerError ? error.message : summarizeError(error);
|
|
586
591
|
errors.push(`${uid}: ${message}`);
|
|
587
592
|
}
|
|
588
593
|
}
|
|
@@ -646,7 +651,7 @@ function parsedToGenerateInput(event: ParsedICalEvent, uid: string): GenerateICa
|
|
|
646
651
|
description: event.description,
|
|
647
652
|
location: event.location,
|
|
648
653
|
// Re-emit attendees by their raw value so import preserves the original
|
|
649
|
-
// addressing; display-name-only redaction applies to
|
|
654
|
+
// addressing; display-name-only redaction applies to handler responses,
|
|
650
655
|
// not to server-side payloads.
|
|
651
656
|
attendees: event.attendees.map((a) => a.rawValue),
|
|
652
657
|
organizer: event.organizerRaw,
|