@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.
Files changed (146) hide show
  1. package/CHANGELOG.md +22 -3
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +2422 -1040
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/conversation-rendering.ts +2 -2
  8. package/src/core/conversation.ts +49 -2
  9. package/src/core/session-recovery.ts +24 -18
  10. package/src/core/system-message-router.ts +42 -26
  11. package/src/core/turn-event-wiring.ts +12 -16
  12. package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +29 -24
  13. package/src/daemon/handlers/calendar/index.ts +316 -0
  14. package/src/daemon/handlers/context.ts +29 -0
  15. package/src/daemon/handlers/contracts.ts +77 -0
  16. package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
  17. package/src/daemon/handlers/drafts/index.ts +17 -0
  18. package/src/daemon/handlers/drafts/register.ts +331 -0
  19. package/src/daemon/handlers/email/config.ts +164 -0
  20. package/src/daemon/handlers/email/index.ts +43 -0
  21. package/src/daemon/handlers/email/read-handlers.ts +80 -0
  22. package/src/daemon/handlers/email/runtime.ts +140 -0
  23. package/src/daemon/handlers/email/validation.ts +147 -0
  24. package/src/daemon/handlers/email/write-handlers.ts +133 -0
  25. package/src/daemon/handlers/errors.ts +18 -0
  26. package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
  27. package/src/daemon/handlers/inbox/index.ts +211 -0
  28. package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
  29. package/src/daemon/{channels → handlers}/inbox/poller.ts +6 -5
  30. package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
  31. package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +10 -11
  32. package/src/daemon/{channels → handlers}/inbox/providers/email.ts +2 -1
  33. package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
  34. package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +4 -3
  35. package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +11 -12
  36. package/src/daemon/handlers/index.ts +107 -0
  37. package/src/daemon/handlers/register.ts +161 -0
  38. package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
  39. package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
  40. package/src/daemon/handlers/remote/backends/index.ts +40 -0
  41. package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
  42. package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
  43. package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
  44. package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
  45. package/src/daemon/handlers/remote/index.ts +119 -0
  46. package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
  47. package/src/daemon/handlers/remote/service.ts +191 -0
  48. package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
  49. package/src/daemon/handlers/routing/index.ts +261 -0
  50. package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
  51. package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
  52. package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
  53. package/src/daemon/handlers/triage/index.ts +57 -0
  54. package/src/daemon/handlers/triage/integration.ts +213 -0
  55. package/src/daemon/{triage → handlers/triage}/pipeline.ts +60 -71
  56. package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
  57. package/src/daemon/handlers/triage/tagger/discord.ts +187 -0
  58. package/src/daemon/handlers/triage/tagger/imap.ts +384 -0
  59. package/src/daemon/handlers/triage/tagger/index.ts +184 -0
  60. package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
  61. package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
  62. package/src/daemon/handlers/triage/types.ts +50 -0
  63. package/src/export/gist-uploader.ts +3 -1
  64. package/src/input/command-registry.ts +1 -0
  65. package/src/input/commands/cloudflare-runtime.ts +2 -1
  66. package/src/input/commands/guidance-runtime.ts +2 -4
  67. package/src/input/commands/health-runtime.ts +13 -7
  68. package/src/input/commands/knowledge.ts +2 -1
  69. package/src/input/commands/runtime-services.ts +40 -10
  70. package/src/input/handler-command-route.ts +1 -1
  71. package/src/input/handler-interactions.ts +2 -1
  72. package/src/input/handler-modal-routes.ts +2 -1
  73. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  74. package/src/input/handler-onboarding.ts +8 -8
  75. package/src/input/handler-ui-state.ts +1 -1
  76. package/src/input/tts-settings-actions.ts +2 -1
  77. package/src/main.ts +52 -59
  78. package/src/panels/agent-inspector-panel.ts +72 -2
  79. package/src/panels/agent-logs-panel.ts +127 -19
  80. package/src/panels/base-panel.ts +2 -1
  81. package/src/panels/builtin/agent.ts +3 -1
  82. package/src/panels/builtin/development.ts +1 -0
  83. package/src/panels/builtin/session.ts +1 -1
  84. package/src/panels/context-visualizer-panel.ts +24 -14
  85. package/src/panels/cost-tracker-panel.ts +7 -13
  86. package/src/panels/debug-panel.ts +11 -22
  87. package/src/panels/docs-panel.ts +14 -24
  88. package/src/panels/file-explorer-panel.ts +2 -1
  89. package/src/panels/file-preview-panel.ts +2 -1
  90. package/src/panels/git-panel.ts +10 -17
  91. package/src/panels/marketplace-panel.ts +2 -1
  92. package/src/panels/memory-panel.ts +2 -1
  93. package/src/panels/project-planning-panel.ts +5 -4
  94. package/src/panels/provider-health-panel.ts +56 -67
  95. package/src/panels/schedule-panel.ts +4 -7
  96. package/src/panels/session-browser-panel.ts +13 -21
  97. package/src/panels/skills-panel.ts +2 -1
  98. package/src/panels/tasks-panel.ts +2 -7
  99. package/src/panels/thinking-panel.ts +6 -11
  100. package/src/panels/token-budget-panel.ts +31 -15
  101. package/src/panels/tool-inspector-panel.ts +10 -18
  102. package/src/panels/work-plan-panel.ts +2 -1
  103. package/src/panels/wrfc-panel.ts +37 -35
  104. package/src/renderer/agent-detail-modal.ts +2 -2
  105. package/src/renderer/modal-utils.ts +0 -10
  106. package/src/renderer/process-modal.ts +2 -2
  107. package/src/runtime/bootstrap-command-context.ts +3 -0
  108. package/src/runtime/bootstrap-command-parts.ts +3 -1
  109. package/src/runtime/bootstrap-core.ts +31 -31
  110. package/src/runtime/bootstrap-shell.ts +1 -0
  111. package/src/runtime/onboarding/apply.ts +4 -3
  112. package/src/runtime/onboarding/snapshot.ts +4 -3
  113. package/src/runtime/process-lifecycle.ts +195 -0
  114. package/src/runtime/services.ts +52 -36
  115. package/src/runtime/wrfc-persistence.ts +20 -5
  116. package/src/shell/ui-openers.ts +3 -2
  117. package/src/verification/live-verifier.ts +4 -3
  118. package/src/version.ts +1 -1
  119. package/src/core/context-auto-compact.ts +0 -110
  120. package/src/daemon/calendar/index.ts +0 -52
  121. package/src/daemon/calendar/register.ts +0 -527
  122. package/src/daemon/channels/drafts/index.ts +0 -22
  123. package/src/daemon/channels/drafts/register.ts +0 -449
  124. package/src/daemon/channels/inbox/index.ts +0 -58
  125. package/src/daemon/channels/inbox/register.ts +0 -247
  126. package/src/daemon/channels/routing/index.ts +0 -39
  127. package/src/daemon/channels/routing/register.ts +0 -296
  128. package/src/daemon/email/index.ts +0 -68
  129. package/src/daemon/email/register.ts +0 -715
  130. package/src/daemon/operator/index.ts +0 -43
  131. package/src/daemon/operator/register-helper.ts +0 -150
  132. package/src/daemon/operator/surfaces.ts +0 -137
  133. package/src/daemon/operator/types.ts +0 -207
  134. package/src/daemon/remote/backends/index.ts +0 -34
  135. package/src/daemon/remote/index.ts +0 -74
  136. package/src/daemon/remote/register.ts +0 -411
  137. package/src/daemon/triage/index.ts +0 -59
  138. package/src/daemon/triage/integration.ts +0 -179
  139. package/src/daemon/triage/register.ts +0 -231
  140. package/src/daemon/triage/tagger.ts +0 -777
  141. /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
  142. /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
  143. /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
  144. /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
  145. /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
  146. /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
@@ -0,0 +1,184 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Daemon-internal triage TAGGER (composition).
3
+ //
4
+ // Applies user-defined triage labels back on the provider side:
5
+ // - IMAP : STORE a keyword flag on the message (IMAP4rev1 over TLS).
6
+ // - Slack: reactions.add emoji on the source message.
7
+ // - Discord: real forum thread tags (PATCH applied_tags, merge) when a
8
+ // forum-tag mapping is configured, else a unicode reaction analog.
9
+ //
10
+ // Hard rules honored here:
11
+ // - All provider credentials come ONLY from the daemon credential store.
12
+ // They are never returned in results and never logged.
13
+ // - The whole tagger is gated behind a config flag (surfaces.triage.autoTag);
14
+ // when disabled, applyTags() is a no-op that reports skipped:true.
15
+ // - Provider-side writes are EFFECTFUL; callers must pass an explicitly
16
+ // confirmed request (confirm === true && explicitUserRequest === true).
17
+ // Unconfirmed calls throw HandlerError(REQUIRE_CONFIRM).
18
+ //
19
+ // Contract-fidelity note: the triage surface (`inbox.triage.*`) is daemon-
20
+ // internal and NOT a published operator method, so there is no external
21
+ // request/response schema to certify these tag shapes against. What is
22
+ // guaranteed is the provider-side behavior: no silent data loss (Discord thread
23
+ // tags are merged, never blindly overwritten) and no command injection (IMAP
24
+ // quoting rejects control characters).
25
+ // ---------------------------------------------------------------------------
26
+
27
+ import type { HandlerContext } from '../../context.ts';
28
+ import type { DaemonCredentialStore } from '../../credentials.ts';
29
+ import { HandlerError, REQUIRE_CONFIRM } from '../../errors.ts';
30
+ import { labelToTag } from '../scorer.ts';
31
+ import { applyImap, imapStoreFlagOverTls, makeRetryingImapStoreFlag } from './imap.ts';
32
+ import type { ImapRetryOptions, ImapStoreFlag } from './imap.ts';
33
+ import { applySlack } from './slack.ts';
34
+ import { applyDiscord } from './discord.ts';
35
+ import type { ApplyTagsRequest, ApplyTagsResult, TaggerProviderConfig } from './shared.ts';
36
+
37
+ export type {
38
+ ApplyTagsRequest,
39
+ ApplyTagsResult,
40
+ TaggerProviderConfig,
41
+ } from './shared.ts';
42
+ export type { ImapRetryOptions, ImapStoreArgs, ImapStoreFlag } from './imap.ts';
43
+
44
+ export const TRIAGE_AUTOTAG_FLAG = 'surfaces.triage.autoTag';
45
+
46
+ export interface TriageTaggerOptions {
47
+ credentials?: DaemonCredentialStore;
48
+ /** Override the autotag flag lookup (used in tests). */
49
+ autoTagEnabled?: boolean;
50
+ /** Per-surface provider config; usually derived from configManager. */
51
+ providers?: TaggerProviderConfig;
52
+ /** Injectable fetch (Slack/Discord HTTP). Defaults to global fetch. */
53
+ fetchImpl?: typeof fetch;
54
+ /** Injectable IMAP flag-setter (used in tests to avoid a live socket). */
55
+ imapStoreFlag?: ImapStoreFlag;
56
+ /**
57
+ * Transient-failure retry policy for the default IMAP store implementation.
58
+ * Ignored when imapStoreFlag is injected and succeeds first-try.
59
+ */
60
+ imapRetry?: ImapRetryOptions;
61
+ }
62
+
63
+ export interface TriageTagger {
64
+ /** Whether provider-side tagging is currently enabled. */
65
+ enabled(): boolean;
66
+ applyTags(request: ApplyTagsRequest): Promise<ApplyTagsResult>;
67
+ }
68
+
69
+ function readBoolFlag(
70
+ configManager: HandlerContext['configManager'],
71
+ key: string,
72
+ ): boolean {
73
+ try {
74
+ const value = configManager.get(key as never) as unknown;
75
+ return value === true || value === 'true' || value === 1;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ function safeGet(configManager: HandlerContext['configManager'], key: string): unknown {
82
+ try {
83
+ return configManager.get(key as never) as unknown;
84
+ } catch {
85
+ return undefined;
86
+ }
87
+ }
88
+
89
+ function resolveProvidersFromConfig(
90
+ configManager: HandlerContext['configManager'],
91
+ ): TaggerProviderConfig {
92
+ const out: TaggerProviderConfig = {};
93
+ const slackToken = safeGet(configManager, 'surfaces.slack.botToken');
94
+ if (typeof slackToken === 'string' && slackToken.length > 0) {
95
+ out.slack = { tokenConfigKey: 'surfaces.slack.botToken' };
96
+ }
97
+ const discordToken = safeGet(configManager, 'surfaces.discord.botToken');
98
+ if (typeof discordToken === 'string' && discordToken.length > 0) {
99
+ out.discord = { tokenConfigKey: 'surfaces.discord.botToken' };
100
+ }
101
+ const imapHost = safeGet(configManager, 'surfaces.email.imap.host');
102
+ const imapUser = safeGet(configManager, 'surfaces.email.imap.user');
103
+ if (typeof imapHost === 'string' && imapHost.length > 0 && typeof imapUser === 'string') {
104
+ const portRaw = safeGet(configManager, 'surfaces.email.imap.port');
105
+ const mailbox = safeGet(configManager, 'surfaces.email.imap.mailbox');
106
+ out.imap = {
107
+ host: imapHost,
108
+ port: typeof portRaw === 'number' ? portRaw : 993,
109
+ user: imapUser,
110
+ passwordConfigKey: 'surfaces.email.imap.password',
111
+ mailbox: typeof mailbox === 'string' && mailbox.length > 0 ? mailbox : 'INBOX',
112
+ };
113
+ }
114
+ return out;
115
+ }
116
+
117
+ function resolveTags(request: ApplyTagsRequest): string[] {
118
+ if (request.tags && request.tags.length > 0) {
119
+ return [...new Set(request.tags.map((t) => t.trim()).filter((t) => t.length > 0))];
120
+ }
121
+ if (request.label) return [labelToTag(request.label)];
122
+ return [];
123
+ }
124
+
125
+ /**
126
+ * Create the triage tagger. Reads the autotag flag and provider config from the
127
+ * handler context; credentials are resolved lazily, per apply, from the daemon
128
+ * credential store.
129
+ */
130
+ export function createTriageTagger(
131
+ ctx: HandlerContext,
132
+ options: TriageTaggerOptions = {},
133
+ ): TriageTagger {
134
+ const credentials = options.credentials ?? ctx.credentials;
135
+ const fetchImpl = options.fetchImpl ?? fetch;
136
+ const providers = options.providers ?? resolveProvidersFromConfig(ctx.configManager);
137
+ // Retry wraps whichever store impl is in use (default TLS client OR an
138
+ // injected one), so transient failures are retried uniformly.
139
+ const imapStoreFlag = makeRetryingImapStoreFlag(
140
+ options.imapStoreFlag ?? imapStoreFlagOverTls,
141
+ options.imapRetry,
142
+ );
143
+ const enabled = (): boolean =>
144
+ options.autoTagEnabled ?? readBoolFlag(ctx.configManager, TRIAGE_AUTOTAG_FLAG);
145
+
146
+ return {
147
+ enabled,
148
+ async applyTags(request: ApplyTagsRequest): Promise<ApplyTagsResult> {
149
+ const { item } = request;
150
+ const tags = resolveTags(request);
151
+ const base: ApplyTagsResult = {
152
+ surface: item.surface,
153
+ itemId: item.id,
154
+ appliedTags: [],
155
+ skipped: true,
156
+ };
157
+
158
+ if (!enabled()) {
159
+ return { ...base, reason: 'autotag-disabled' };
160
+ }
161
+
162
+ // Provider-side mutation is effectful — require explicit confirmation.
163
+ if (request.confirm !== true || request.explicitUserRequest !== true) {
164
+ throw new HandlerError(
165
+ 'Provider-side triage tagging requires explicit user confirmation.',
166
+ REQUIRE_CONFIRM,
167
+ 403,
168
+ );
169
+ }
170
+
171
+ switch (item.surface) {
172
+ case 'email':
173
+ case 'imap':
174
+ return applyImap(item, tags, providers, credentials, imapStoreFlag, base);
175
+ case 'slack':
176
+ return applySlack(item, tags, providers, credentials, fetchImpl, ctx, base);
177
+ case 'discord':
178
+ return applyDiscord(item, tags, providers, credentials, fetchImpl, ctx, base);
179
+ default:
180
+ return { ...base, reason: `unsupported-surface:${item.surface}` };
181
+ }
182
+ },
183
+ };
184
+ }
@@ -0,0 +1,70 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Triage tagger — shared types and provider-agnostic helpers.
3
+ //
4
+ // Provider config shapes, the apply request/result contract, and the tag
5
+ // normalization helpers used by the IMAP/Slack/Discord modules. No I/O here.
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { InboundChannelItem, TriageLabel } from '../types.ts';
9
+
10
+ export interface TaggerProviderConfig {
11
+ /** IMAP host:port (default port 993, TLS). Credentials resolved separately. */
12
+ imap?: { host: string; port?: number; user: string; passwordConfigKey: string; mailbox?: string };
13
+ /** Slack bot token config key (resolved from credential store). */
14
+ slack?: { tokenConfigKey: string };
15
+ /**
16
+ * Discord bot token config key (resolved from credential store), plus an
17
+ * optional forum-tag mapping. When `forumTagIds` maps a GoodVibes triage tag
18
+ * (e.g. 'GoodVibes/Spam') to a forum tag SNOWFLAKE id, items that target a
19
+ * forum/media-channel thread get that REAL thread tag applied (PATCH
20
+ * applied_tags) — exact fidelity to the contract's "Discord thread tags".
21
+ * Without a mapping (or for non-thread messages) tagging degrades to a
22
+ * unicode reaction analog.
23
+ */
24
+ discord?: { tokenConfigKey: string; forumTagIds?: Record<string, string> };
25
+ }
26
+
27
+ export interface ApplyTagsRequest {
28
+ item: InboundChannelItem;
29
+ /** Provider-side tags to apply. Defaults to [labelToTag(label)] when omitted. */
30
+ tags?: readonly string[];
31
+ label?: TriageLabel;
32
+ /** Must be true — provider-side mutation requires explicit confirmation. */
33
+ confirm?: boolean;
34
+ /** Mirror of the operator invocation context flag. */
35
+ explicitUserRequest?: boolean;
36
+ }
37
+
38
+ export interface ApplyTagsResult {
39
+ surface: string;
40
+ itemId: string;
41
+ appliedTags: string[];
42
+ /** True when the autotag flag is disabled or no provider matched. */
43
+ skipped: boolean;
44
+ reason?: string;
45
+ }
46
+
47
+ /** IMAP keywords cannot contain spaces or '/'; normalize the canonical tag. */
48
+ export function imapKeywordForTag(tag: string): string {
49
+ return tag.replace(/[^A-Za-z0-9_]+/g, '_');
50
+ }
51
+
52
+ export function slackEmojiForTag(tag: string): string {
53
+ const lower = tag.toLowerCase();
54
+ if (lower.includes('spam')) return 'no_entry_sign';
55
+ if (lower.includes('priority')) return 'rotating_light';
56
+ return 'inbox_tray';
57
+ }
58
+
59
+ export function discordEmojiForTag(tag: string): string {
60
+ const lower = tag.toLowerCase();
61
+ if (lower.includes('spam')) return '\u{1F6AB}';
62
+ if (lower.includes('priority')) return '\u{1F6A8}';
63
+ return '\u{1F4E5}';
64
+ }
65
+
66
+ /** Read a non-empty string from the item's opaque metadata bag. */
67
+ export function stringMeta(item: InboundChannelItem, key: string): string | undefined {
68
+ const value = item.metadata?.[key];
69
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
70
+ }
@@ -0,0 +1,69 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Triage tagger — Slack provider.
3
+ //
4
+ // Applies a triage label as a Slack message reaction (reactions.add). The bot
5
+ // token is resolved per-apply from the daemon credential store and is never
6
+ // logged or returned. `already_reacted` is treated as idempotent success.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import type { HandlerContext } from '../../context.ts';
10
+ import type { DaemonCredentialStore } from '../../credentials.ts';
11
+ import { HandlerError } from '../../errors.ts';
12
+ import type { InboundChannelItem } from '../types.ts';
13
+ import type { ApplyTagsResult, TaggerProviderConfig } from './shared.ts';
14
+ import { slackEmojiForTag, stringMeta } from './shared.ts';
15
+
16
+ export async function applySlack(
17
+ item: InboundChannelItem,
18
+ tags: string[],
19
+ providers: TaggerProviderConfig,
20
+ credentials: DaemonCredentialStore,
21
+ fetchImpl: typeof fetch,
22
+ ctx: HandlerContext,
23
+ base: ApplyTagsResult,
24
+ ): Promise<ApplyTagsResult> {
25
+ const cfg = providers.slack;
26
+ if (!cfg) return { ...base, reason: 'slack-not-configured' };
27
+ const channel = stringMeta(item, 'channelId') ?? item.conversationId;
28
+ const ts = stringMeta(item, 'ts') ?? stringMeta(item, 'messageTs');
29
+ if (!channel || !ts) return { ...base, reason: 'slack-missing-target' };
30
+ if (tags.length === 0) return { ...base, reason: 'no-tags' };
31
+
32
+ const token = await credentials.resolveConfigSecret(cfg.tokenConfigKey);
33
+ if (!token) return { ...base, reason: 'slack-no-credentials' };
34
+
35
+ const applied: string[] = [];
36
+ for (const tag of tags) {
37
+ const emoji = slackEmojiForTag(tag);
38
+ const response = await fetchImpl('https://slack.com/api/reactions.add', {
39
+ method: 'POST',
40
+ headers: {
41
+ Authorization: `Bearer ${token}`,
42
+ 'Content-Type': 'application/json; charset=utf-8',
43
+ },
44
+ body: JSON.stringify({ channel, timestamp: ts, name: emoji }),
45
+ });
46
+ const payload = (await response.json().catch(() => ({}))) as {
47
+ ok?: boolean;
48
+ error?: string;
49
+ };
50
+ if (!response.ok) {
51
+ throw new HandlerError(
52
+ `Slack reactions.add HTTP ${response.status}`,
53
+ 'TRIAGE_SLACK_TAG_FAILED',
54
+ 502,
55
+ );
56
+ }
57
+ // 'already_reacted' is an idempotent success for our purposes.
58
+ if (payload.ok !== true && payload.error !== 'already_reacted') {
59
+ ctx.logger.warn('triage: slack reaction rejected', { error: payload.error });
60
+ throw new HandlerError(
61
+ `Slack reactions.add rejected: ${payload.error ?? 'unknown'}`,
62
+ 'TRIAGE_SLACK_TAG_FAILED',
63
+ 502,
64
+ );
65
+ }
66
+ applied.push(tag);
67
+ }
68
+ return { surface: item.surface, itemId: item.id, appliedTags: applied, skipped: false };
69
+ }
@@ -0,0 +1,50 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Daemon-internal triage domain types.
3
+ //
4
+ // `InboundChannelItem` is the daemon-internal shape the inbox poller produces
5
+ // and the triage pipeline scores. It is NOT an SDK catalog contract: the
6
+ // published `channels.inbox.list` output schema (CHANNEL_INBOX_ITEM_SCHEMA) is
7
+ // owned by the SDK and never re-declared here. This is the internal poller
8
+ // item per the handoff doc — it carries `fromDigest` (never a raw sender id),
9
+ // `subjectPreview`/`bodyPreview` (PII-stripped, length-bounded) and an opaque
10
+ // `metadata` bag the tagger reads provider targeting from (imapUid, channelId,
11
+ // Slack ts, Discord messageId/threadId).
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** Triage label assigned by the scorer. */
15
+ export type TriageLabel = 'spam' | 'priority' | 'normal';
16
+
17
+ /** 1:1 vs group/channel/thread conversation hint (priority signal). */
18
+ export type ConversationKind = 'direct' | 'group' | 'channel' | 'thread' | 'service';
19
+
20
+ /**
21
+ * Internal inbound feed item. Mirrors the handoff `InboundChannelItem` shape
22
+ * plus the optional fields the scorer/tagger consult. `surface` is the provider
23
+ * family ('email' | 'imap' | 'slack' | 'discord' | ...) the tagger dispatches
24
+ * on; `provider` is the handoff-facing provider id. They are usually equal.
25
+ */
26
+ export interface InboundChannelItem {
27
+ /** Stable, provider-scoped dedup key. */
28
+ readonly id: string;
29
+ /** Provider family the tagger dispatches on (email/imap/slack/discord/...). */
30
+ readonly surface: string;
31
+ /** Handoff-facing provider id ("slack" | "discord" | "email" | ...). */
32
+ readonly provider?: string;
33
+ readonly kind?: 'dm' | 'thread' | 'mention' | 'reaction';
34
+ /** SHA-256 first-N of sender external id — NEVER a raw identifier. */
35
+ readonly fromDigest?: string;
36
+ /** Conversation id (Slack/Discord channel, IMAP mailbox-scoped). */
37
+ readonly conversationId?: string;
38
+ readonly conversationKind?: ConversationKind;
39
+ /** Display subject (<= 200 chars). */
40
+ readonly subject?: string;
41
+ /** Display body preview (<= 500 chars, PII-stripped). Alias: bodyPreview. */
42
+ readonly snippet?: string;
43
+ /** Optional daemon route binding id. */
44
+ readonly routeId?: string;
45
+ /** Unix ms. */
46
+ readonly receivedAt?: number;
47
+ readonly unread?: boolean;
48
+ /** Opaque provider targeting bag (imapUid/uid, channelId, ts, messageId, threadId). */
49
+ readonly metadata?: Record<string, unknown>;
50
+ }
@@ -16,6 +16,8 @@
16
16
  // the URL can view it).
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
20
+
19
21
  export type UploadResult =
20
22
  | { ok: true; url: string }
21
23
  | { ok: false; error: string };
@@ -96,7 +98,7 @@ export class GistUploadTarget implements UploadTarget {
96
98
  body,
97
99
  });
98
100
  } catch (fetchErr: unknown) {
99
- const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
101
+ const msg = summarizeError(fetchErr);
100
102
  return { ok: false, error: `Network error: ${msg}` };
101
103
  }
102
104
 
@@ -139,6 +139,7 @@ export interface CommandSessionServices {
139
139
  readonly sessionManager?: import('@pellux/goodvibes-sdk/platform/sessions').SessionManager;
140
140
  readonly sessionMemoryStore?: import('@pellux/goodvibes-sdk/platform/core').SessionMemoryStore;
141
141
  readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
142
+ readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
142
143
  readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
143
144
  }
144
145
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../runtime/cloudflare-control-plane.ts';
12
12
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
13
13
  import { requireShellPaths } from './runtime-services.ts';
14
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
14
15
 
15
16
  interface ParsedCloudflareArgs {
16
17
  readonly positional: readonly string[];
@@ -366,5 +367,5 @@ function formatCloudflareError(error: unknown): string {
366
367
  if (error instanceof CloudflareDaemonRouteError) {
367
368
  return `${error.message} (HTTP ${error.status}, ${error.code})`;
368
369
  }
369
- return error instanceof Error ? error.message : String(error);
370
+ return summarizeError(error);
370
371
  }
@@ -2,7 +2,7 @@ import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core'
2
2
  import { evaluateSessionMaintenance, formatSessionMaintenanceLines, getGuidanceMode } from '@/runtime/index.ts';
3
3
  import { dismissGuidance, evaluateContextualGuidance, formatGuidanceItems, resetGuidance } from '@/runtime/index.ts';
4
4
  import type { CommandRegistry } from '../command-registry.ts';
5
- import { requireProviderApi, requireReadModels, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
5
+ import { requireReadModels, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
6
6
 
7
7
  export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void {
8
8
  registry.register({
@@ -68,8 +68,6 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
68
68
  return;
69
69
  }
70
70
 
71
- const providerApi = requireProviderApi(ctx);
72
- const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
73
71
  const llmMessages = ctx.session.conversationManager.getMessagesForLLM();
74
72
  const readModels = requireReadModels(ctx);
75
73
  const session = readModels.session.getSnapshot();
@@ -80,7 +78,7 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
80
78
  const maintenance = evaluateSessionMaintenance({
81
79
  configManager: ctx.platform.configManager,
82
80
  currentTokens: estimateConversationTokens(llmMessages),
83
- contextWindow: currentModel?.contextWindow ?? 0,
81
+ contextWindow: ctx.provider.providerRegistry.getContextWindowForModel(ctx.provider.providerRegistry.getCurrentModel()),
84
82
  messageCount: llmMessages.length,
85
83
  sessionMemoryCount: requireSessionMemoryStore(ctx).list().length,
86
84
  session: session.session,
@@ -13,7 +13,6 @@ import {
13
13
  openCommandPanel,
14
14
  requireLocalUserAuthManager,
15
15
  requireOperatorClient,
16
- requireProviderApi,
17
16
  requireReadModels,
18
17
  requireSecretsManager,
19
18
  requireServiceRegistry,
@@ -240,15 +239,19 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
240
239
 
241
240
  if (sub === 'maintenance') {
242
241
  const session = readModels.session.getSnapshot();
243
- const providerApi = requireProviderApi(ctx);
244
- const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
242
+ const providerRegistry = ctx.provider.providerRegistry;
243
+ // Resolve the context window the same way the Tokens panel does so the
244
+ // maintenance usage %/remaining agree across every diagnostics surface.
245
+ const contextWindow = providerRegistry.getContextWindowForModel(
246
+ providerRegistry.getCurrentModel(),
247
+ );
245
248
  const llmMessages = typeof ctx.session.conversationManager.getMessagesForLLM === 'function'
246
249
  ? ctx.session.conversationManager.getMessagesForLLM()
247
250
  : [];
248
251
  const maintenance = evaluateSessionMaintenance({
249
252
  configManager: ctx.platform.configManager,
250
253
  currentTokens: estimateConversationTokens(llmMessages),
251
- contextWindow: currentModel?.contextWindow ?? 0,
254
+ contextWindow,
252
255
  messageCount: llmMessages.length,
253
256
  sessionMemoryCount: requireSessionMemoryStore(ctx).list().length,
254
257
  session: session.session,
@@ -386,12 +389,15 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
386
389
  }
387
390
 
388
391
  const session = readModels.session.getSnapshot();
389
- const providerApi = requireProviderApi(ctx);
390
- const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
392
+ const providerRegistry = ctx.provider.providerRegistry;
391
393
  const llmMessages = typeof ctx.session.conversationManager.getMessagesForLLM === 'function'
392
394
  ? ctx.session.conversationManager.getMessagesForLLM()
393
395
  : [];
394
- const contextWindow = currentModel?.contextWindow ?? 0;
396
+ // Resolve the context window the same way the Tokens panel does so the
397
+ // maintenance usage %/remaining agree across every diagnostics surface.
398
+ const contextWindow = providerRegistry.getContextWindowForModel(
399
+ providerRegistry.getCurrentModel(),
400
+ );
395
401
  const maintenance = evaluateSessionMaintenance({
396
402
  configManager: ctx.platform.configManager,
397
403
  currentTokens: estimateConversationTokens(llmMessages),
@@ -1,5 +1,6 @@
1
1
  import type { KnowledgeService } from '@pellux/goodvibes-sdk/platform/knowledge';
2
2
  import type { CommandContext, SlashCommand } from '../command-registry.ts';
3
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
3
4
 
4
5
  const KNOWLEDGE_REVIEW_ACTIONS = ['accept', 'reject', 'resolve', 'reopen', 'edit', 'forget'] as const;
5
6
 
@@ -397,7 +398,7 @@ export const knowledgeCommand: SlashCommand = {
397
398
  ...(result.appliedFacts ? [` applied facts: ${Object.keys(result.appliedFacts).join(', ') || 'none'}`] : []),
398
399
  ].join('\n'));
399
400
  } catch (error) {
400
- context.print(`[knowledge] ${error instanceof Error ? error.message : String(error)}`);
401
+ context.print(`[knowledge] ${summarizeError(error)}`);
401
402
  }
402
403
  break;
403
404
  }
@@ -7,7 +7,7 @@ import type {
7
7
  CommandSessionServices,
8
8
  CommandWorkspaceServices,
9
9
  } from '../command-registry.ts';
10
- import { getLastCompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
10
+ import { compactSmallWindow, getLastCompactionEvent, SMALL_WINDOW_THRESHOLD } from '@pellux/goodvibes-sdk/platform/core';
11
11
  import type { CompactionContext, CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
12
12
  import type { UiReadModels } from '../../runtime/ui-read-models.ts';
13
13
  import type { ShellPathService } from '@/runtime/index.ts';
@@ -244,23 +244,53 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
244
244
  * change).
245
245
  */
246
246
  export async function compactConversation(context: CommandContext): Promise<CompactionEvent | null> {
247
+ const conversationManager = context.session.conversationManager;
248
+ const providerRegistry = context.provider.providerRegistry;
249
+ // Resolve the live context window for the current model so manual /compact
250
+ // matches the auto-compaction path (which scales behaviour by window size)
251
+ // instead of passing a meaningless 0.
252
+ const contextWindow = providerRegistry.getContextWindowForModel(
253
+ providerRegistry.getCurrentModel(),
254
+ );
255
+
256
+ // Small-window models lack room for the structured (multi-LLM) extraction
257
+ // pipeline, so mirror the SDK auto-compaction path and degrade to the
258
+ // simplified "keep the most recent messages" compaction.
259
+ if (contextWindow > 0 && contextWindow < SMALL_WINDOW_THRESHOLD) {
260
+ const compacted = compactSmallWindow(conversationManager.getMessagesForLLM(), 10);
261
+ conversationManager.replaceMessagesForLLM(compacted);
262
+ return null;
263
+ }
264
+
247
265
  const eventBefore = getLastCompactionEvent();
248
266
  const sessionMemories = context.session.sessionMemoryStore?.list() ?? [];
267
+ const lineageTracker = context.session.sessionLineageTracker;
268
+ const agentManager = context.ops.agentManager;
269
+ const planManager = context.ops.planManager;
249
270
  const compactionCtx: CompactionContext = {
250
- messages: context.session.conversationManager.getMessagesForLLM(),
271
+ messages: conversationManager.getMessagesForLLM(),
251
272
  sessionMemories,
252
- agents: [],
253
- wrfcChains: [],
254
- activePlan: null,
255
- lineageEntries: [],
256
- compactionCount: 0,
257
- contextWindow: 0,
273
+ // Mirror buildAutoCompactionContext: pass the running/pending agents, the
274
+ // active execution plan, and the session-lineage log so the handoff summary
275
+ // and lineage section are populated correctly (not "compaction #0").
276
+ agents:
277
+ agentManager
278
+ ?.exportState()
279
+ .filter((agent) => agent.status === 'running' || agent.status === 'pending') ?? [],
280
+ // Mirror buildAutoCompactionContext: include the live WRFC chains (wired
281
+ // through context.session.wrfcController) so the handoff summary's
282
+ // orchestration section matches the SDK auto-compaction path.
283
+ wrfcChains: context.session.wrfcController?.listChains() ?? [],
284
+ activePlan: planManager?.getActive(context.session.runtime.sessionId) ?? null,
285
+ lineageEntries: lineageTracker?.getEntries() ?? [],
286
+ compactionCount: lineageTracker?.getCompactionCount() ?? 0,
287
+ contextWindow,
258
288
  trigger: 'manual',
259
289
  extractionModelId: context.session.runtime.model,
260
290
  extractionProvider: context.session.runtime.provider,
261
291
  };
262
- await context.session.conversationManager.compact(
263
- context.provider.providerRegistry,
292
+ await conversationManager.compact(
293
+ providerRegistry,
264
294
  context.session.runtime.model,
265
295
  'manual',
266
296
  context.session.runtime.provider,
@@ -117,7 +117,7 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
117
117
  return true;
118
118
  }
119
119
 
120
- return token.logicalName !== 'left' && token.logicalName !== 'right';
120
+ return token.logicalName !== 'left' && token.logicalName !== 'right' && token.logicalName !== 'home' && token.logicalName !== 'end';
121
121
  }
122
122
 
123
123
  function withPanelFocusSync(context: CommandContext, state: CommandModeRouteState): CommandContext {
@@ -5,6 +5,7 @@ import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, handleBlockCopy,
5
5
  import { clearModalStack, handleEscape, modalOpened } from './handler-modal-stack.ts';
6
6
  import { openOnboardingWizardState, type OpenOnboardingWizardOptions } from './handler-ui-state.ts';
7
7
  import type { InputHandlerLike as InputHandler } from './handler-types.ts';
8
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
8
9
 
9
10
  export function openOnboardingWizardForHandler(
10
11
  handler: InputHandler,
@@ -49,7 +50,7 @@ export async function hydrateOnboardingWizardFromRuntimeForHandler(handler: Inpu
49
50
  handler.requestRender();
50
51
  } catch (error) {
51
52
  if (!handler.onboardingWizard.active || hydrationSerial !== handler.onboardingHydrationSerial) return;
52
- const message = error instanceof Error ? error.message : String(error);
53
+ const message = summarizeError(error);
53
54
  handler.onboardingWizard.failRuntimeHydration(message);
54
55
  handler.commandContext?.print?.(`Onboarding runtime snapshot failed: ${message}`);
55
56
  handler.requestRender();
@@ -3,6 +3,7 @@ import type { SelectionResult, SelectionAction } from './selection-modal.ts';
3
3
  import type { CommandContext } from './command-registry.ts';
4
4
  import { openTtsProviderPicker, openTtsVoicePicker } from './tts-settings-actions.ts';
5
5
  import { isTextBackspace } from './delete-key-policy.ts';
6
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
7
 
7
8
  type SelectionRouteState = {
8
9
  selectionModal: {
@@ -285,7 +286,7 @@ function consumeSettingsPickerRequest(state: SettingsRouteState): void {
285
286
  return;
286
287
  }
287
288
  void openTtsVoicePicker(state.commandContext).catch((error: unknown) => {
288
- state.commandContext?.print(`Unable to list TTS voices: ${error instanceof Error ? error.message : String(error)}`);
289
+ state.commandContext?.print(`Unable to list TTS voices: ${summarizeError(error)}`);
289
290
  state.requestRender();
290
291
  });
291
292
  return;
@@ -22,6 +22,7 @@ import {
22
22
  getCloudflareSetupSource,
23
23
  shouldShowCloudflareStep,
24
24
  } from './onboarding/onboarding-wizard-cloudflare.ts';
25
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
25
26
 
26
27
  type CloudflareOnboardingAction = Extract<OnboardingWizardAction,
27
28
  | 'cloudflare-token-requirements'
@@ -44,7 +45,7 @@ function normalizeCloudflareActionError(error: unknown): string {
44
45
  if (error instanceof CloudflareDaemonRouteError) {
45
46
  return `${error.message} (HTTP ${error.status}, ${error.code})`;
46
47
  }
47
- return error instanceof Error ? error.message : String(error);
48
+ return summarizeError(error);
48
49
  }
49
50
 
50
51
  function setCloudflareWizardStatusForHandler(