@pellux/goodvibes-agent 1.8.0 → 1.8.2

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 (88) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +1 -1
  3. package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-44ar68xs.node} +0 -0
  4. package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-asvc2tv1.node} +0 -0
  5. package/dist/package/main.js +135434 -118764
  6. package/dist/package/{web-tree-sitter-vmqc5d26.wasm → web-tree-sitter-jbz042ba.wasm} +0 -0
  7. package/docs/tools-and-commands.md +9 -0
  8. package/package.json +3 -2
  9. package/src/agent/capability-registry.ts +300 -0
  10. package/src/agent/email/style-reply-lane.ts +13 -4
  11. package/src/agent/memory-consolidation-receipts.ts +79 -0
  12. package/src/agent/memory-prompt.ts +3 -1
  13. package/src/agent/operator-gateway-call.ts +134 -0
  14. package/src/agent/principal-attribution.ts +97 -0
  15. package/src/agent/prompt-context-receipts.ts +5 -1
  16. package/src/agent/unified-inbox.ts +17 -1
  17. package/src/cli/bundle-command.ts +2 -1
  18. package/src/cli/channel-profiles-command.ts +168 -0
  19. package/src/cli/ci-command.ts +214 -0
  20. package/src/cli/completion.ts +5 -0
  21. package/src/cli/connected-host-metrics.ts +165 -0
  22. package/src/cli/entrypoint.ts +25 -1
  23. package/src/cli/fleet-command.ts +178 -0
  24. package/src/cli/help.ts +115 -0
  25. package/src/cli/import-command.ts +208 -0
  26. package/src/cli/index.ts +1 -0
  27. package/src/cli/management-commands.ts +9 -8
  28. package/src/cli/management.ts +42 -0
  29. package/src/cli/memory-command.ts +4 -3
  30. package/src/cli/openclaw-import.ts +382 -0
  31. package/src/cli/operator-command-args.ts +88 -0
  32. package/src/cli/parser.ts +12 -0
  33. package/src/cli/personas-command.ts +3 -2
  34. package/src/cli/principals-command.ts +230 -0
  35. package/src/cli/profiles-command.ts +2 -1
  36. package/src/cli/relay-command.ts +100 -0
  37. package/src/cli/routines-command.ts +4 -3
  38. package/src/cli/skill-bundle-command.ts +3 -2
  39. package/src/cli/skills-command.ts +3 -2
  40. package/src/cli/status.ts +114 -2
  41. package/src/cli/temporal-label.ts +89 -0
  42. package/src/cli/types.ts +7 -0
  43. package/src/cli/workspaces-command.ts +135 -0
  44. package/src/config/agent-settings-policy.ts +8 -0
  45. package/src/config/checkpoint-settings.ts +113 -0
  46. package/src/config/workspace-registration.ts +299 -0
  47. package/src/core/rewind-turn-anchors.ts +82 -0
  48. package/src/input/agent-workspace-categories.ts +1 -0
  49. package/src/input/agent-workspace-channel-triage.ts +27 -8
  50. package/src/input/agent-workspace-local-library-snapshot.ts +5 -1
  51. package/src/input/agent-workspace-onboarding-categories.ts +17 -1
  52. package/src/input/agent-workspace-snapshot.ts +25 -3
  53. package/src/input/agent-workspace-types.ts +5 -5
  54. package/src/input/commands/connected-host-admin-runtime.ts +75 -0
  55. package/src/input/commands.ts +2 -0
  56. package/src/input/settings-modal-types.ts +16 -5
  57. package/src/main.ts +17 -12
  58. package/src/permissions/approval-posture.ts +40 -5
  59. package/src/permissions/prompt.ts +35 -1
  60. package/src/renderer/settings-modal-helpers.ts +11 -0
  61. package/src/renderer/settings-modal.ts +21 -0
  62. package/src/renderer/terminal-escapes.ts +65 -12
  63. package/src/runtime/agent-runtime-events.ts +102 -3
  64. package/src/runtime/bootstrap-core.ts +111 -10
  65. package/src/runtime/bootstrap-external-services.ts +156 -0
  66. package/src/runtime/bootstrap.ts +73 -69
  67. package/src/runtime/context-accounting-source.ts +114 -0
  68. package/src/runtime/conversation-rewind-port.ts +133 -0
  69. package/src/runtime/index.ts +8 -28
  70. package/src/runtime/memory-consolidation-scheduler.ts +67 -0
  71. package/src/runtime/memory-consolidation-wiring.ts +56 -0
  72. package/src/runtime/memory-usage-wiring.ts +75 -0
  73. package/src/runtime/services.ts +257 -6
  74. package/src/runtime/tool-permission-safety.ts +18 -1
  75. package/src/shell/blocking-input.ts +38 -8
  76. package/src/shell/startup-wiring.ts +26 -1
  77. package/src/tools/agent-context-policy.ts +1 -1
  78. package/src/tools/agent-harness-cli-command-policy.ts +29 -3
  79. package/src/tools/agent-harness-learning-auto-promote.ts +7 -3
  80. package/src/tools/agent-harness-memory-posture.ts +57 -2
  81. package/src/tools/agent-harness-metadata.ts +12 -0
  82. package/src/tools/agent-harness-mode-catalog.ts +1 -1
  83. package/src/tools/agent-harness-personal-ops-lanes.ts +8 -6
  84. package/src/tools/agent-harness-prompt-context.ts +3 -1
  85. package/src/tools/agent-harness-tool.ts +3 -2
  86. package/src/tools/agent-policy-explanation.ts +40 -0
  87. package/src/tools/tool-definition-compaction.ts +2 -0
  88. package/src/version.ts +1 -1
@@ -15,6 +15,13 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
15
15
  * automatically, before permissions.mode is even read.
16
16
  * 2. permissions.mode === 'allow-all' -> every tool call is approved
17
17
  * automatically.
18
+ * 2a. permissions.mode === 'plan' -> read-only tools are allowed; every
19
+ * write, execute, or delegate tool call is REFUSED outright (not
20
+ * asked) with a structured plan-mode denial, steering the model to
21
+ * present a plan instead of acting.
22
+ * 2b. permissions.mode === 'accept-edits' -> read and file write/edit
23
+ * tool calls are approved automatically; execute and every other
24
+ * risky class still fall through to the prompt/cache path below.
18
25
  * 3. permissions.mode === 'custom' -> each tool category is allowed,
19
26
  * prompted, or denied per its own configured rule; bypassesPrompts is
20
27
  * true only when EVERY configured tool category resolves to 'allow'
@@ -26,10 +33,16 @@ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
26
33
  * computeApprovalPosture (or the config-reading convenience below) rather
27
34
  * than re-deriving the precedence locally — that re-derivation is exactly
28
35
  * how cli/status.ts drifted from the gate (it read permissions.mode alone
29
- * and never looked at behavior.autoApprove).
36
+ * and never looked at behavior.autoApprove). It is also how 'plan' and
37
+ * 'accept-edits' modes previously drifted: normalizeMode used to fold any
38
+ * mode it did not recognize into 'prompt', so a surface reading the posture
39
+ * label for those two modes silently mislabeled them as "Ask before
40
+ * powerful actions" — wrong for plan mode (which never asks; it refuses)
41
+ * and wrong for accept-edits mode (which auto-approves file writes without
42
+ * asking).
30
43
  */
31
44
 
32
- export type ApprovalPostureKind = 'auto-approve' | 'allow-all' | 'custom' | 'prompt';
45
+ export type ApprovalPostureKind = 'auto-approve' | 'allow-all' | 'custom' | 'prompt' | 'plan' | 'accept-edits';
33
46
 
34
47
  export interface ApprovalPostureInput {
35
48
  /** behavior.autoApprove, read via the SDK's isAutoApproveEnabled (or an equivalent duck-typed read). */
@@ -48,7 +61,7 @@ export interface ApprovalPosture {
48
61
  /** Which precedence branch produced this posture. */
49
62
  readonly kind: ApprovalPostureKind;
50
63
  /** The raw permissions.mode value, normalized to a known mode string (defaults to 'prompt'). */
51
- readonly mode: 'prompt' | 'allow-all' | 'custom';
64
+ readonly mode: 'prompt' | 'allow-all' | 'custom' | 'plan' | 'accept-edits';
52
65
  /** The raw behavior.autoApprove value that drove this posture. */
53
66
  readonly autoApprove: boolean;
54
67
  /**
@@ -63,8 +76,8 @@ export interface ApprovalPosture {
63
76
  readonly detail: string;
64
77
  }
65
78
 
66
- function normalizeMode(mode: unknown): 'prompt' | 'allow-all' | 'custom' {
67
- if (mode === 'allow-all' || mode === 'custom') return mode;
79
+ function normalizeMode(mode: unknown): 'prompt' | 'allow-all' | 'custom' | 'plan' | 'accept-edits' {
80
+ if (mode === 'allow-all' || mode === 'custom' || mode === 'plan' || mode === 'accept-edits') return mode;
68
81
  return 'prompt';
69
82
  }
70
83
 
@@ -98,6 +111,28 @@ export function computeApprovalPosture(input: ApprovalPostureInput): ApprovalPos
98
111
  };
99
112
  }
100
113
 
114
+ if (mode === 'plan') {
115
+ return {
116
+ kind: 'plan',
117
+ mode,
118
+ autoApprove: false,
119
+ bypassesPrompts: false,
120
+ label: 'Plan mode — read-only, nothing prompts because nothing mutates',
121
+ detail: 'permissions.mode is plan: read-only tool calls are approved automatically; every write, execute, or delegate tool call is refused outright (never asked) so the model presents a plan instead of acting.',
122
+ };
123
+ }
124
+
125
+ if (mode === 'accept-edits') {
126
+ return {
127
+ kind: 'accept-edits',
128
+ mode,
129
+ autoApprove: false,
130
+ bypassesPrompts: false,
131
+ label: 'Accept edits — file writes auto-approve, execute/delegate still ask',
132
+ detail: 'permissions.mode is accept-edits: read and file write/edit tool calls are approved automatically without asking; execute and every other risky class still prompt for approval.',
133
+ };
134
+ }
135
+
101
136
  if (mode === 'custom') {
102
137
  const values = Object.values(input.customTools ?? {});
103
138
  const allAllow = values.length > 0 && values.every((value) => value === 'allow');
@@ -30,10 +30,35 @@ export class PermissionPromptUI {
30
30
  };
31
31
  }
32
32
 
33
+ /**
34
+ * "Who is asking" line for the two attribution kinds (SDK 1.6.1) that have
35
+ * no OTHER rendered channel for it. `background-agent` attribution is
36
+ * deliberately excluded here: it already flows into the fleet
37
+ * ProcessRegistry via `metadata.agentId` (see
38
+ * bootstrap-core.ts's approvalMetadataForRequest), so a spawned agent's
39
+ * pending ask is attributed on its own ProcessNode. An MCP server
40
+ * elicitation or a sandbox host-access escalation has no ProcessNode to
41
+ * attach to — this prompt is the ONLY place a user learns who/what is
42
+ * asking, so those two kinds render an explicit line. Returns null for
43
+ * `background-agent` and for no-attribution (foreground) requests.
44
+ */
45
+ private static attributionLine(request: PermissionPromptRequest): string | null {
46
+ const attribution = request.attribution;
47
+ if (!attribution) return null;
48
+ if (attribution.kind === 'mcp-server') {
49
+ return `MCP server: ${attribution.serverName}`;
50
+ }
51
+ if (attribution.kind === 'sandbox-escalation') {
52
+ return `Sandbox ${attribution.sandbox}: ${attribution.escalations.join(', ')}`;
53
+ }
54
+ return null;
55
+ }
56
+
33
57
  static getPromptHeight(request: PermissionPromptRequest): number {
34
58
  const analysis = this.fallbackAnalysis(request);
35
59
  const reasonLines = Math.min(2, Math.max(1, analysis.reasons.length));
36
- const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0);
60
+ const extraLines = (analysis.host ? 1 : 0) + (analysis.surface ? 1 : 0) + (analysis.sideEffects && analysis.sideEffects.length > 0 ? 1 : 0)
61
+ + (this.attributionLine(request) ? 1 : 0);
37
62
  return 12 + reasonLines + extraLines;
38
63
  }
39
64
 
@@ -89,6 +114,15 @@ export class PermissionPromptUI {
89
114
  const toolLine = ` Tool : ${tool}`;
90
115
  lines.push(UIFactory.stringToLine(toolLine.padEnd(width), width, { fg: TEXT }));
91
116
 
117
+ // Attribution row (mcp-server / sandbox-escalation only — see attributionLine's doc comment)
118
+ const attributionText = this.attributionLine(request);
119
+ if (attributionText) {
120
+ const maxAttrLen = Math.max(10, width - 16);
121
+ const truncatedAttr = attributionText.length > maxAttrLen ? `${attributionText.slice(0, maxAttrLen - 3)}...` : attributionText;
122
+ const attributionRow = ` Asked by : ${truncatedAttr}`;
123
+ lines.push(UIFactory.stringToLine(attributionRow.padEnd(width), width, { fg: DIM }));
124
+ }
125
+
92
126
  // Key argument row - truncate if too long
93
127
  const maxArgLen = Math.max(10, width - 16);
94
128
  const truncatedArg = displayArg.length > maxArgLen
@@ -82,15 +82,18 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
82
82
  behavior: 'Behavior',
83
83
  storage: 'Storage',
84
84
  permissions: 'Permissions',
85
+ diagnostics: 'Diagnostics',
85
86
  helper: 'Helper',
86
87
  tts: 'TTS',
87
88
  automation: 'Automation',
89
+ checkin: 'Check-in',
88
90
  service: 'Service',
89
91
  controlPlane: 'Control Plane',
90
92
  httpListener: 'HTTP Listener',
91
93
  web: 'Web',
92
94
  watchers: 'Watchers',
93
95
  network: 'Network',
96
+ relay: 'Relay',
94
97
  orchestration: 'Orchestration',
95
98
  planner: 'Planner',
96
99
  daemon: 'Daemon',
@@ -106,6 +109,14 @@ export const CATEGORY_LABELS: Record<(typeof SETTINGS_CATEGORIES)[number], strin
106
109
  release: 'Update Channel',
107
110
  tools: 'Tools',
108
111
  flags: 'Feature Controls',
112
+ atRest: 'At-Rest Protection',
113
+ learning: 'Memory Consolidation',
114
+ agents: 'Agent Runtime',
115
+ notifications: 'Notifications',
116
+ policy: 'Policy Bundles',
117
+ fetch: 'Fetch Safety',
118
+ security: 'Token Rotation',
119
+ integrations: 'Delivery Reliability',
109
120
  };
110
121
 
111
122
  export const SETTING_LABELS: Partial<Record<string, string>> = {
@@ -31,15 +31,18 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
31
31
  behavior: 'Day-to-day shell behavior: approval posture, compaction, history, guidance, notifications, stale-context warnings, return context, and Human-in-the-Loop mode.',
32
32
  storage: 'Local storage posture, including secret storage policy and maximum artifact size for Agent Knowledge, artifacts, and document ingestion.',
33
33
  permissions: 'Permission mode and tool-class policy. These settings decide whether the shell prompts before read/write/exec/network/agent actions.',
34
+ diagnostics: 'Post-edit diagnostics behavior: whether a successful file write/edit gets cheap, in-process syntax diagnostics appended to the tool result so the model sees a broken edit immediately. Syntax-level only — not type-checking.',
34
35
  helper: 'Helper model defaults used by helper subsystems when they do not use the main chat route.',
35
36
  tts: 'Text-to-speech provider, voice, and optional spoken-turn LLM overrides.',
36
37
  automation: 'Scheduled and automated run settings, concurrency, timeout, catch-up, cooldown, and retention behavior.',
38
+ checkin: 'Proactive check-in: off by default. When enabled, on a cadence the Agent assembles a compact briefing of current state, asks the model to judge whether anything warrants contacting you, and delivers a message through the configured channel only when the judgment says yes. Every run — delivered, quiet, skipped for quiet hours, or errored — leaves a receipt (checkin.receipts.list) so this automatic behavior stays accountable even when it decides to say nothing.',
37
39
  service: 'GoodVibes daemon service posture and restart/autostart preferences.',
38
40
  controlPlane: 'Control-plane endpoint, stream, remote access, and TLS settings used by daemon-backed operator routes.',
39
41
  httpListener: 'HTTP listener binding, trust proxy, and TLS settings for inbound companion/channel routes.',
40
42
  web: 'Web companion surface settings including host, port, public URL, and static asset path.',
41
43
  watchers: 'Polling watcher and heartbeat behavior for runtime recovery and periodic checks.',
42
44
  network: 'Outbound TLS and remote fetch network policy.',
45
+ relay: 'Outbound zero-knowledge relay reachability for the connected GoodVibes daemon: an end-to-end encrypted tunnel (ECDH P-256 -> HKDF -> AES-256-GCM) that terminates INSIDE the daemon, so the relay operator only ever sees ciphertext plus connection metadata (who paired with whom, byte counts, timing) — never plaintext requests, responses, or the operator token. Default off, and gated by the relay-connect feature flag in addition to relay.enabled. These are the connected daemon\'s own settings (imported here, not live-shared): changing them in Agent does not itself start or stop the daemon\'s relay registration.',
43
46
  orchestration: 'Visible agent orchestration limits such as recursion and active-agent caps.',
44
47
  planner: 'Planning-decomposition agent limits: decomposition strategy, max turns, token ceiling, and wall-clock timeout before falling back to the deterministic heuristic path.',
45
48
  daemon: 'Whether the local session daemon runs, and whether it is embedded in this surface process instead of spawned as a detached background service.',
@@ -55,6 +58,14 @@ const CATEGORY_INFO: Record<SettingsCategory, string> = {
55
58
  release: 'Update-channel preference.',
56
59
  tools: 'Tool LLM and helper model routing. Empty provider/model values inherit the active chat route unless a specific helper/tool route is set.',
57
60
  flags: 'Feature controls for optional behavior that can be enabled or disabled separately from normal configuration.',
61
+ atRest: 'Data-at-rest protection: whether stored content is redacted, and retention limits by age and total size.',
62
+ learning: 'Idle-time memory consolidation: dedupe merges, confidence decay of never-referenced records, and review proposals. Off by default.',
63
+ agents: 'Agent runtime tuning: the context-window fraction that triggers sub-agent conversation compaction, and the token budget, relevance floor, and code-chunk limit for per-turn passive knowledge/code injection.',
64
+ notifications: 'Adaptive notification-burst suppression: the observation window, trip threshold, and cooldown that collapse a rapid run of same-domain notifications to panel-only. Critical/milestone/alert notifications are always exempt.',
65
+ policy: 'Policy-as-code bundle loading: where the policy registry loads its initial bundle from at startup, and the file path when loading from disk. A loaded bundle is a candidate subject to the divergence gate before promotion.',
66
+ fetch: 'Fetch-tool response sanitization: the default sanitize mode, and default trusted/blocked host lists layered under any per-call overrides. The built-in SSRF-risk block applies independently.',
67
+ security: 'Credential rotation-audit defaults: how often tokens should rotate, how much lead time a warning gets, and whether overdue or over-scoped tokens are blocked from use rather than only reported.',
68
+ integrations: 'Integration delivery reliability: retry ceiling and exponential-backoff bounds for Slack/Discord/webhook delivery, dead-letter queue size, and whether dead-letter events log at error level.',
58
69
  };
59
70
 
60
71
  const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
@@ -72,6 +83,16 @@ const ENUM_VALUE_DESCRIPTIONS: Record<string, Record<string, string>> = {
72
83
  prompt: 'Ask before powerful or risky actions according to tool policy.',
73
84
  'allow-all': 'Allow actions without prompting. This is fast but removes an important safety gate.',
74
85
  custom: 'Use per-tool-class permission settings from the rows below.',
86
+ plan: 'Read-only: every write, execute, or delegate tool call is refused outright (never asked) so the model presents a plan instead of acting.',
87
+ 'accept-edits': 'File write/edit tool calls auto-approve without asking; execute and every other risky class still prompt for approval.',
88
+ },
89
+ 'permissions.backgroundAgents': {
90
+ inherit: 'Background/subagent tool calls consult the same session permission mode as the foreground turn — prompt/plan/accept-edits/custom apply their matrices, and any resulting ask still brokers through the normal approval prompt with subagent attribution.',
91
+ 'allow-all': 'Background/subagent tool calls are exempt from the session permission mode and auto-approve regardless of it.',
92
+ },
93
+ 'diagnostics.postEdit': {
94
+ on: 'After a successful file write/edit, append cheap, in-process syntax diagnostics (errors only) to the tool result. Syntax-level only — not type-checking.',
95
+ off: 'Never append post-edit diagnostics to write/edit tool results.',
75
96
  },
76
97
  'storage.secretPolicy': {
77
98
  preferred_secure: 'Use secure secret storage when available, with supported fallback behavior.',
@@ -4,22 +4,75 @@
4
4
  * focus reporting). Extracted as plain constants so callers share one definition
5
5
  * and the entry file stays within the source-file line-count gate.
6
6
  *
7
+ * ALT_SCREEN_ENTER / ALT_SCREEN_EXIT / MOUSE_* / CURSOR_* / KEYBOARD_EXT_* /
8
+ * PASTE_* / CLEAR_VIEWPORT_HOME: re-exported byte-for-byte from
9
+ * @pellux/goodvibes-terminal-shell's TERMINAL_ESCAPES so this app and the TUI
10
+ * cannot drift on the bytes (mechanical swap from a previously-inlined local
11
+ * copy — see main.ts history and the package's terminal-lifecycle module docs).
12
+ *
13
+ * There used to be a local CLEAR_SCREEN constant here that appended ESC[3J
14
+ * (scrollback erase) after ESC[2J. That sequence must never be written by this
15
+ * app: 3J wipes the user's PRIMARY-screen scrollback on several emulators even
16
+ * when issued mid-session or from the alt screen, and main.ts's exit teardown
17
+ * was doing exactly that. CLEAR_VIEWPORT_HOME (2J + cursor-home, no 3J) is the
18
+ * only clear this app ever writes now, for both the entry-time paint and the
19
+ * no-alt-screen restore path.
20
+ *
7
21
  * FOCUS_ENABLE / FOCUS_DISABLE (DECSET ?1004) are the shared home for the OS
8
22
  * window-focus reporting mode consumed by the focus-tracker: the focus-mode
9
23
  * module (shell/terminal-focus-mode.ts) imports FOCUS_ENABLE from here rather
10
- * than redefining it.
24
+ * than redefining it. The terminal-shell package does not define these, so
25
+ * they stay local literals.
11
26
  */
12
27
 
13
- export const ALT_SCREEN_ENTER = '\x1b[?1049h';
14
- export const ALT_SCREEN_EXIT = '\x1b[?1049l';
15
- export const MOUSE_ENABLE = '\x1b[?1000h\x1b[?1002h\x1b[?1006h';
16
- export const MOUSE_DISABLE = '\x1b[?1006l\x1b[?1002l\x1b[?1000l';
17
- export const CURSOR_HIDE = '\x1b[?25l';
18
- export const CURSOR_SHOW = '\x1b[?25h';
19
- export const CLEAR_SCREEN = '\x1b[2J\x1b[3J\x1b[H';
20
- export const KEYBOARD_EXT_ENABLE = '\x1b[>4;2m' + '\x1b[?1u';
21
- export const KEYBOARD_EXT_DISABLE = '\x1b[>4;0m' + '\x1b[?1l';
22
- export const PASTE_ENABLE = '\x1b[?2004h';
23
- export const PASTE_DISABLE = '\x1b[?2004l';
28
+ import { TERMINAL_ESCAPES } from '@pellux/goodvibes-terminal-shell';
29
+
30
+ export const ALT_SCREEN_ENTER = TERMINAL_ESCAPES.ALT_SCREEN_ENTER;
31
+ export const ALT_SCREEN_EXIT = TERMINAL_ESCAPES.ALT_SCREEN_EXIT;
32
+ export const MOUSE_ENABLE = TERMINAL_ESCAPES.MOUSE_ENABLE;
33
+ export const MOUSE_DISABLE = TERMINAL_ESCAPES.MOUSE_DISABLE;
34
+ export const CURSOR_HIDE = TERMINAL_ESCAPES.CURSOR_HIDE;
35
+ export const CURSOR_SHOW = TERMINAL_ESCAPES.CURSOR_SHOW;
36
+ export const KEYBOARD_EXT_ENABLE = TERMINAL_ESCAPES.KEYBOARD_EXT_ENABLE;
37
+ export const KEYBOARD_EXT_DISABLE = TERMINAL_ESCAPES.KEYBOARD_EXT_DISABLE;
38
+ export const PASTE_ENABLE = TERMINAL_ESCAPES.PASTE_ENABLE;
39
+ export const PASTE_DISABLE = TERMINAL_ESCAPES.PASTE_DISABLE;
40
+ /** Clear viewport + home the cursor, WITHOUT ESC[3J. Never wipes scrollback. */
41
+ export const CLEAR_VIEWPORT_HOME = TERMINAL_ESCAPES.CLEAR_VIEWPORT_HOME;
42
+
24
43
  export const FOCUS_ENABLE = '\x1b[?1004h';
25
44
  export const FOCUS_DISABLE = '\x1b[?1004l';
45
+
46
+ /**
47
+ * The bytes main.ts writes to enter terminal mode: alt screen (unless
48
+ * disabled), clear + home (no 3J), cursor hidden, then the input modes.
49
+ */
50
+ export function buildEnterSequence(noAltScreen: boolean): string {
51
+ return (noAltScreen ? '' : ALT_SCREEN_ENTER)
52
+ + CLEAR_VIEWPORT_HOME
53
+ + CURSOR_HIDE
54
+ + MOUSE_ENABLE
55
+ + KEYBOARD_EXT_ENABLE
56
+ + PASTE_ENABLE
57
+ + FOCUS_ENABLE;
58
+ }
59
+
60
+ /**
61
+ * The bytes main.ts writes to restore the terminal on exit: input modes off,
62
+ * then the screen switch, then cursor-show LAST so visibility applies to the
63
+ * screen the shell prompt actually lands on.
64
+ *
65
+ * Alt-screen path: just leave the alt screen — 1049l restores the primary
66
+ * screen and cursor exactly as they were at launch. Clearing first is
67
+ * pointless (the alt screen is discarded) and actively harmful (3J wipes the
68
+ * primary scrollback even when issued from the alt screen).
69
+ *
70
+ * No-alt path: the compositor painted over the primary screen, so clear the
71
+ * viewport and home the cursor — but WITHOUT 3J, the user's scrollback is
72
+ * theirs.
73
+ */
74
+ export function buildExitSequence(noAltScreen: boolean): string {
75
+ const exitScreen = noAltScreen ? CLEAR_VIEWPORT_HOME : ALT_SCREEN_EXIT;
76
+ return PASTE_DISABLE + KEYBOARD_EXT_DISABLE + MOUSE_DISABLE + FOCUS_DISABLE
77
+ + exitScreen + CURSOR_SHOW;
78
+ }
@@ -1,8 +1,11 @@
1
1
  import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
2
2
  import type { ConversationFollowUpItem } from '@pellux/goodvibes-sdk/platform/core';
3
- import type { AgentManager } from '@pellux/goodvibes-sdk/platform/tools';
3
+ import type { AgentManager, ToolRegistry } from '@pellux/goodvibes-sdk/platform/tools';
4
+ import { logger } from '@pellux/goodvibes-sdk/platform/utils';
4
5
  import type {
5
6
  AgentEvent,
7
+ CompactionEvent,
8
+ PermissionEvent,
6
9
  ProviderEvent,
7
10
  RuntimeEventBus,
8
11
  } from '@/runtime/index.ts';
@@ -23,6 +26,7 @@ export interface AgentRuntimeEventBridgeOptions {
23
26
  readonly requestRender: () => void;
24
27
  readonly configManager: ConfigManager;
25
28
  readonly agentManager: AgentManager;
29
+ readonly toolRegistry: Pick<ToolRegistry, 'execute'>;
26
30
  }
27
31
 
28
32
  function withRouter(
@@ -37,6 +41,55 @@ function formatAgentTask(task: string): string {
37
41
  return task.length > 50 ? `${task.slice(0, 50)}...` : task;
38
42
  }
39
43
 
44
+ /** Renders a COMPACTION_RECEIPT payload as a one-line honest summary: strategy, trigger, and outcome — with the fallback reason (`detail`) named when the compaction did not apply. */
45
+ function formatCompactionReceipt(payload: Extract<CompactionEvent, { type: 'COMPACTION_RECEIPT' }>): string {
46
+ const { strategy, trigger, outcome, tokensBefore, tokensAfter, detail } = payload;
47
+ if (outcome === 'applied') {
48
+ const saved = Math.max(0, tokensBefore - tokensAfter);
49
+ return `${strategy} (${trigger}) applied — ${saved.toLocaleString()} tokens reclaimed`;
50
+ }
51
+ const reason = detail ? `: ${detail}` : '';
52
+ return `${strategy} (${trigger}) ${outcome}${reason}`;
53
+ }
54
+
55
+ interface ChildFailureEnvelopeLike {
56
+ readonly phase?: string;
57
+ readonly reason?: { readonly code?: string };
58
+ readonly partialOutputs?: { readonly note?: string; readonly turnsCompleted?: number };
59
+ }
60
+
61
+ /**
62
+ * Fetches the structured child-failure envelope the SDK's own `agent` tool
63
+ * attaches to its `status` action result (SDK 1.6.1, child-failure
64
+ * envelopes on the agent tool's status/get/wait results) and renders it as a
65
+ * compact suffix: `reason: <code>, phase: <phase>[, N turns completed][, note]`.
66
+ * Goes through `toolRegistry.execute` — the SAME tool the model calls —
67
+ * rather than re-deriving the classification locally: `classifyChildFailureReason`
68
+ * and `describeChildPhase` are internal to the SDK's tool module, not part of
69
+ * its public API, so calling the tool is the only faithful way to surface
70
+ * the real classification instead of guessing at it. Read-only, no side
71
+ * effects (see ToolRegistry.execute's own doc comment); returns null on any
72
+ * failure to look up the record or parse the result, so a fetch problem never
73
+ * blocks the (already-informative) base failure message.
74
+ */
75
+ async function fetchChildFailureSummary(toolRegistry: Pick<ToolRegistry, 'execute'>, agentId: string): Promise<string | null> {
76
+ try {
77
+ const result = await toolRegistry.execute('agent-runtime-events:child-failure-envelope', 'agent', { mode: 'status', agentId });
78
+ if (!result.success || !result.output) return null;
79
+ const parsed = JSON.parse(result.output) as { failure?: ChildFailureEnvelopeLike };
80
+ const failure = parsed.failure;
81
+ if (!failure) return null;
82
+ const parts = [`reason: ${failure.reason?.code ?? 'error'}`];
83
+ if (failure.phase) parts.push(`phase: ${failure.phase}`);
84
+ if (failure.partialOutputs?.turnsCompleted !== undefined) parts.push(`${failure.partialOutputs.turnsCompleted} turns completed`);
85
+ if (failure.partialOutputs?.note) parts.push(failure.partialOutputs.note);
86
+ return parts.join(', ');
87
+ } catch (err) {
88
+ logger.debug('agent-runtime-events: child-failure envelope fetch failed (non-fatal)', { agentId, error: String(err) });
89
+ return null;
90
+ }
91
+ }
92
+
40
93
  export function registerAgentRuntimeEvents(options: AgentRuntimeEventBridgeOptions): {
41
94
  unsubs: Array<() => void>;
42
95
  agentStatusIntervalRef: { value: ReturnType<typeof setInterval> | null };
@@ -48,6 +101,7 @@ export function registerAgentRuntimeEvents(options: AgentRuntimeEventBridgeOptio
48
101
  queueConversationFollowUp,
49
102
  requestRender,
50
103
  agentManager,
104
+ toolRegistry,
51
105
  } = options;
52
106
  const unsubs: Array<() => void> = [];
53
107
 
@@ -66,9 +120,46 @@ export function registerAgentRuntimeEvents(options: AgentRuntimeEventBridgeOptio
66
120
  unsubs.push(runtimeBus.onDomain('compaction', (env) => {
67
121
  domainDispatch.dispatchCompactionEvent(env.payload);
68
122
  }));
123
+ // The mandatory post-compaction receipt (SDK 1.6.1, emitted after every
124
+ // automatic and manual compaction path — see the SDK's own doc comment on
125
+ // emitCompactionReceipt) is the one compaction signal a user should never
126
+ // miss: system-message-router.ts's own tiering doc already names
127
+ // "compaction events" as 'high' priority (conversation + activity feed),
128
+ // but nothing routed COMPACTION_RECEIPT there before this — the domain
129
+ // subscription above only ever updated runtime-store session state, not a
130
+ // user-visible message. Surfaces strategy + outcome; when the compaction
131
+ // did not apply (kept-original/failed), `detail` carries the fallback
132
+ // reason so a silent revert is never actually silent.
133
+ unsubs.push(runtimeBus.on<Extract<CompactionEvent, { type: 'COMPACTION_RECEIPT' }>>('COMPACTION_RECEIPT', ({ payload }) => {
134
+ withRouter(getSystemMessageRouter, (router) => {
135
+ router.high(`[Compaction] ${formatCompactionReceipt(payload)}`);
136
+ });
137
+ requestRender();
138
+ }));
69
139
  unsubs.push(runtimeBus.onDomain('transport', (env) => {
70
140
  domainDispatch.dispatchTransportEvent(env.payload);
71
141
  }));
142
+ // Keep the session's stored permission-mode metadata live. The SDK emits
143
+ // PERMISSION_MODE_CHANGED on the 'permissions' domain whenever the
144
+ // permissions.mode config value changes (see emitPermissionModeChanged), but
145
+ // nothing consumed it before: the runtime store's permission domain was seeded
146
+ // once at boot and never advanced, so any surface reading the stored mode went
147
+ // stale until restart. Forwarding the domain event into the store reducer
148
+ // (dispatchPermissionEvent -> updatePermissionState) refreshes that stored
149
+ // metadata live from the wire event, and re-emitting it onto a remote surface's
150
+ // bus keeps cross-surface readers current too.
151
+ unsubs.push(runtimeBus.onDomain('permissions', (env) => {
152
+ domainDispatch.dispatchPermissionEvent(env.payload);
153
+ }));
154
+ // Make the mode change visible in-session the moment it lands, rather than
155
+ // only reflecting it the next time a surface happens to rebuild. The precise
156
+ // previous/next mode values ride the event, so the message states both.
157
+ unsubs.push(runtimeBus.on<Extract<PermissionEvent, { type: 'PERMISSION_MODE_CHANGED' }>>('PERMISSION_MODE_CHANGED', ({ payload }) => {
158
+ withRouter(getSystemMessageRouter, (router) => {
159
+ router.high(`[Permissions] Permission mode changed: ${payload.previousMode} -> ${payload.mode}.`);
160
+ });
161
+ requestRender();
162
+ }));
72
163
 
73
164
  unsubs.push(runtimeBus.on<Extract<ProviderEvent, { type: 'MODEL_FALLBACK' }>>('MODEL_FALLBACK', ({ payload }) => {
74
165
  withRouter(getSystemMessageRouter, (router) => {
@@ -102,8 +193,16 @@ export function registerAgentRuntimeEvents(options: AgentRuntimeEventBridgeOptio
102
193
  if (record && record.status !== 'cancelled') {
103
194
  const durationSeconds = record.completedAt !== undefined ? Math.round((record.completedAt - record.startedAt) / 1000) : 0;
104
195
  const taskSnippet = formatAgentTask(record.task);
105
- withRouter(getSystemMessageRouter, (router) => {
106
- router.low(`[Delegated task] ${record.template} ${payload.agentId.slice(-8)} failed in ${durationSeconds}s ${payload.error.slice(0, 80)}`);
196
+ // Compact child-failure envelope enrichment (SDK 1.6.1) fetched via
197
+ // the SAME 'agent' tool the model calls, then appended to the base
198
+ // message. Deferred (async) rather than blocking this handler; no test
199
+ // or downstream consumer depends on this message landing synchronously
200
+ // with the AGENT_FAILED event.
201
+ void fetchChildFailureSummary(toolRegistry, payload.agentId).then((envelopeSummary) => {
202
+ withRouter(getSystemMessageRouter, (router) => {
203
+ router.low(`[Delegated task] ${record.template} ${payload.agentId.slice(-8)} failed in ${durationSeconds}s ${payload.error.slice(0, 80)}${envelopeSummary ? ` (${envelopeSummary})` : ''}`);
204
+ });
205
+ requestRender();
107
206
  });
108
207
  queueConversationFollowUp?.({
109
208
  key: `agent:${payload.agentId}:failed`,