@pellux/goodvibes-tui 1.21.0 → 1.22.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 +20 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +714 -177
- package/docs/foundation-artifacts/peer-contract.json +8 -4
- package/package.json +9 -7
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/service-posture.ts +6 -2
- package/src/config/run-daemon-config-migration.ts +46 -0
- package/src/core/conversation-compaction-render.ts +88 -0
- package/src/core/conversation-line-cache.ts +120 -92
- package/src/core/conversation-rendering.ts +290 -233
- package/src/core/conversation-turn-structure.ts +464 -0
- package/src/core/conversation-types.ts +8 -9
- package/src/daemon/cli.ts +71 -5
- package/src/input/commands/local-runtime.ts +14 -14
- package/src/input/commands/operator-rpc.ts +15 -4
- package/src/input/daemon-owned-settings-descriptions.ts +53 -0
- package/src/input/handler-content-actions.ts +4 -2
- package/src/input/handler-onboarding-daemon-adopt.ts +10 -0
- package/src/input/onboarding/onboarding-feature-units.ts +38 -0
- package/src/input/onboarding/onboarding-wizard-types.ts +1 -0
- package/src/input/search.ts +1 -1
- package/src/input/settings-modal-data.ts +15 -85
- package/src/input/settings-modal-search.ts +99 -0
- package/src/input/settings-modal-types.ts +3 -1
- package/src/renderer/conversation-surface.ts +37 -11
- package/src/renderer/conversation-tree.ts +179 -0
- package/src/renderer/settings-modal-helpers.ts +56 -2
- package/src/renderer/settings-modal.ts +9 -50
- package/src/renderer/tool-call.ts +38 -5
- package/src/runtime/cloudflare-control-plane.ts +4 -1
- package/src/runtime/feature-settings.ts +7 -0
- package/src/runtime/index.ts +22 -1
- package/src/runtime/legacy-daemon-migration.ts +2 -0
- package/src/verification/verification-ledger.ts +115 -2
- package/src/version.ts +1 -1
- package/src/core/conversation-tool-groups.ts +0 -152
|
@@ -79,6 +79,116 @@ export const FEATURE_KNOB_LOCAL_SETTINGS = [
|
|
|
79
79
|
'runtime.toolBudget.maxCostUsd',
|
|
80
80
|
] as const;
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Config keys added by the paired-device and trigger-family domains in SDK
|
|
84
|
+
* 1.14.0. Like FEATURE_KNOB_LOCAL_SETTINGS above, these arrived as new
|
|
85
|
+
* CONFIG_SCHEMA entries that raised the settings inventory `total` with no
|
|
86
|
+
* matching behavior coverage, dropping `localBehaviorPercent` below its floor.
|
|
87
|
+
*
|
|
88
|
+
* THIS CONSTANT IS NOT A DIAL. Each key below is counted because a test that
|
|
89
|
+
* exercises it was actually written:
|
|
90
|
+
* `src/test/verification/device-and-trigger-settings-persistence.test.ts` runs,
|
|
91
|
+
* for every key in this list, the same four-part persistence contract the
|
|
92
|
+
* feature-knob keys are counted for — schema default exposure, `set()` through
|
|
93
|
+
* the key's own validator to disk, reload into a fresh ConfigManager with
|
|
94
|
+
* read-back equality, and reset-to-default that also survives reload. That test
|
|
95
|
+
* additionally asserts this list is exactly the non-enablement key set of both
|
|
96
|
+
* domains and overlaps neither of the other counted sets, so nothing here is
|
|
97
|
+
* double-counted and nothing drifts in uncounted.
|
|
98
|
+
*
|
|
99
|
+
* PER-KEY EVIDENCE — what is verified, and how live the key is TODAY.
|
|
100
|
+
* "persistence" below means the four-part contract above; the second column
|
|
101
|
+
* records whether anything in this tree (TUI or the pinned SDK) reads the key,
|
|
102
|
+
* audited by tracing each key to its consumers.
|
|
103
|
+
*
|
|
104
|
+
* watchers.triggers.backoffLadderMs persistence · live (trigger supervisor retry ladder)
|
|
105
|
+
* watchers.triggers.breakerStrikes persistence · live (strike breaker)
|
|
106
|
+
* watchers.triggers.defaultCheckIntervalMs persistence · live (condition-check scheduler)
|
|
107
|
+
* watchers.triggers.probeTimeoutMs persistence · live (probe execution)
|
|
108
|
+
* watchers.triggers.maxConcurrentChecks persistence · live (check concurrency cap)
|
|
109
|
+
* watchers.triggers.observationRingSize persistence · live (observation ring)
|
|
110
|
+
* watchers.triggers.runHistoryLimit persistence · live (run history retention)
|
|
111
|
+
* watchers.triggers.runHistoryTtlHours persistence · live (run history sweep)
|
|
112
|
+
* watchers.triggers.eventLogLimit persistence · live (event log retention)
|
|
113
|
+
* watchers.triggers.eventLogTtlHours persistence · live (event log sweep)
|
|
114
|
+
* watchers.triggers.sweepIntervalMs persistence · live (housekeeping cadence)
|
|
115
|
+
* watchers.triggers.supervisionTickMs persistence · live (supervision loop tick)
|
|
116
|
+
* watchers.triggers.streamQueueLimit persistence · live (stream watcher queue)
|
|
117
|
+
* watchers.triggers.streamBatchLines persistence · live (stream batching)
|
|
118
|
+
* watchers.triggers.streamBatchIntervalMs persistence · live (stream batching)
|
|
119
|
+
* watchers.triggers.onExitMaxDurationMs persistence · live (on-exit trigger duration cap)
|
|
120
|
+
* watchers.triggers.onExitStdin persistence · live (on-exit stdin posture)
|
|
121
|
+
* watchers.triggers.outputTailBytes persistence · live (captured output tail)
|
|
122
|
+
* device.capabilities.allowAlwaysOffer persistence · settings surface only in this build
|
|
123
|
+
* device.capabilities.requestTimeoutSeconds persistence · settings surface only in this build
|
|
124
|
+
* device.location.precision persistence · settings surface only in this build
|
|
125
|
+
* device.clipboard.readMode persistence · settings surface only in this build
|
|
126
|
+
* device.capture.retentionHours persistence · settings surface only in this build
|
|
127
|
+
* device.capture.maxArtifacts persistence · settings surface only in this build
|
|
128
|
+
* device.capture.sweepIntervalMinutes persistence · settings surface only in this build
|
|
129
|
+
* device.grants.expiryDays persistence · settings surface only in this build
|
|
130
|
+
* device.grants.maxPerNode persistence · settings surface only in this build
|
|
131
|
+
* device.grants.auditRetentionDays persistence · settings surface only in this build
|
|
132
|
+
* device.nodes.maxPaired persistence · live (enforced at the pairing path)
|
|
133
|
+
*
|
|
134
|
+
* On `device.nodes.maxPaired`: the SDK enforces it where a device pairs —
|
|
135
|
+
* PairingTokenManager.mint refuses a NEW node at the cap with
|
|
136
|
+
* PairingLimitReachedError (code DEVICE_NODES_MAX_PAIRED, carrying the setting
|
|
137
|
+
* name, the cap and the live count), mapped to HTTP 409 by the pairing and
|
|
138
|
+
* pairing-handoff routes. An already-paired node supersedes its own record
|
|
139
|
+
* instead of being refused, migration mints off the legacy shared token are
|
|
140
|
+
* deliberately exempt, lowering the cap unpairs nobody, and a non-positive or
|
|
141
|
+
* non-finite value reads as no cap so a broken setting cannot lock the owner
|
|
142
|
+
* out. The cap is read per mint, so a change applies without a restart. What is
|
|
143
|
+
* counted HERE is still only the persistence contract this repo's test
|
|
144
|
+
* exercises — the enforcement itself is SDK-side and SDK-tested, and is recorded
|
|
145
|
+
* in this column so the "how live is it" audit stays accurate, not to claim
|
|
146
|
+
* coverage this repo did not write.
|
|
147
|
+
*
|
|
148
|
+
* On the other ten `device.*` rows: DeviceCapabilityService carries its own
|
|
149
|
+
* policy struct whose defaults match those keys one for one, and names them in
|
|
150
|
+
* its field comments and refusal messages, but no code path in either tree maps
|
|
151
|
+
* configuration into that struct. Persistence is therefore the whole of what is
|
|
152
|
+
* verified for them, and the settings-workspace description for the `device`
|
|
153
|
+
* category says exactly that to the user rather than implying live knobs.
|
|
154
|
+
*
|
|
155
|
+
* DELIBERATELY NOT COUNTED: the 24 non-enablement `voice.wake.*` keys from the
|
|
156
|
+
* same release. `wake-word-detection` is declared `notOperable` — no surface
|
|
157
|
+
* captures audio — so its rows stay in `total` and out of the numerator. That is
|
|
158
|
+
* the honest reading, and it is why this list raises coverage by 29 and not 53.
|
|
159
|
+
*/
|
|
160
|
+
export const DEVICE_AND_TRIGGER_LOCAL_SETTINGS = [
|
|
161
|
+
'watchers.triggers.backoffLadderMs',
|
|
162
|
+
'watchers.triggers.breakerStrikes',
|
|
163
|
+
'watchers.triggers.defaultCheckIntervalMs',
|
|
164
|
+
'watchers.triggers.probeTimeoutMs',
|
|
165
|
+
'watchers.triggers.maxConcurrentChecks',
|
|
166
|
+
'watchers.triggers.observationRingSize',
|
|
167
|
+
'watchers.triggers.runHistoryLimit',
|
|
168
|
+
'watchers.triggers.runHistoryTtlHours',
|
|
169
|
+
'watchers.triggers.eventLogLimit',
|
|
170
|
+
'watchers.triggers.eventLogTtlHours',
|
|
171
|
+
'watchers.triggers.sweepIntervalMs',
|
|
172
|
+
'watchers.triggers.supervisionTickMs',
|
|
173
|
+
'watchers.triggers.streamQueueLimit',
|
|
174
|
+
'watchers.triggers.streamBatchLines',
|
|
175
|
+
'watchers.triggers.streamBatchIntervalMs',
|
|
176
|
+
'watchers.triggers.onExitMaxDurationMs',
|
|
177
|
+
'watchers.triggers.onExitStdin',
|
|
178
|
+
'watchers.triggers.outputTailBytes',
|
|
179
|
+
'device.capabilities.allowAlwaysOffer',
|
|
180
|
+
'device.capabilities.requestTimeoutSeconds',
|
|
181
|
+
'device.location.precision',
|
|
182
|
+
'device.clipboard.readMode',
|
|
183
|
+
'device.capture.retentionHours',
|
|
184
|
+
'device.capture.maxArtifacts',
|
|
185
|
+
'device.capture.sweepIntervalMinutes',
|
|
186
|
+
'device.grants.expiryDays',
|
|
187
|
+
'device.grants.maxPerNode',
|
|
188
|
+
'device.grants.auditRetentionDays',
|
|
189
|
+
'device.nodes.maxPaired',
|
|
190
|
+
] as const;
|
|
191
|
+
|
|
82
192
|
/**
|
|
83
193
|
* Every feature's enablement settings key whose on/off writes round-trip
|
|
84
194
|
* through a REAL on-disk ConfigManager in
|
|
@@ -95,8 +205,11 @@ const ENABLEMENT_KEYS_BEHAVIOR_VERIFIED = new Set(
|
|
|
95
205
|
.filter((key) => !(FEATURE_KNOB_LOCAL_SETTINGS as readonly string[]).includes(key)),
|
|
96
206
|
).size;
|
|
97
207
|
|
|
98
|
-
/** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob and enablement keys. */
|
|
99
|
-
const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
|
|
208
|
+
/** Settings with real local behavior verification: the authored baseline plus the persistence-tested feature-knob, device/trigger, and enablement keys. */
|
|
209
|
+
const SETTINGS_BEHAVIOR_VERIFIED = SETTINGS_BEHAVIOR_BASELINE
|
|
210
|
+
+ FEATURE_KNOB_LOCAL_SETTINGS.length
|
|
211
|
+
+ DEVICE_AND_TRIGGER_LOCAL_SETTINGS.length
|
|
212
|
+
+ ENABLEMENT_KEYS_BEHAVIOR_VERIFIED;
|
|
100
213
|
|
|
101
214
|
const EXTERNAL_SLASH_COMMANDS = new Set([
|
|
102
215
|
'auth',
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '1.
|
|
9
|
+
let _version = '1.22.0';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
// Only trust a version read from OUR OWN package.json. A Bun single-file
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* conversation-tool-groups.ts — detects runs of consecutive tool-result
|
|
3
|
-
* messages that belong to one assistant turn, so the transcript can fold them
|
|
4
|
-
* under a single collapsible group header instead of rendering one
|
|
5
|
-
* "tool result" header per call. Before this module, an assistant turn with
|
|
6
|
-
* N tool calls produced N independent header+body blocks in a row — visual
|
|
7
|
-
* spam once N is more than one or two.
|
|
8
|
-
*
|
|
9
|
-
* A run only counts as a group when it has >= 2 matched tool-result messages:
|
|
10
|
-
* a single result renders exactly as it always has (see
|
|
11
|
-
* renderConversationToolMessage in conversation-rendering.ts, which only
|
|
12
|
-
* consults this module's output when a membership entry exists).
|
|
13
|
-
*
|
|
14
|
-
* Pure function of the message slice — no rendering, no width, no side
|
|
15
|
-
* effects. Called once per render pass (appendConversationMessages /
|
|
16
|
-
* MessageLineCache.renderInto) over the currently-visible message slice, with
|
|
17
|
-
* the absolute index of the slice's first message so group keys and member
|
|
18
|
-
* indexes line up with the same collapseKey / blockRegistry indices a cold
|
|
19
|
-
* render would produce (messages may be a post-clearDisplay slice, not the
|
|
20
|
-
* full snapshot — see appendConversationMessages's msgIndexOffset doc).
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
|
|
24
|
-
import { renderExpandedToolResultLines } from '../renderer/tool-result-expanded-lines.ts';
|
|
25
|
-
|
|
26
|
-
type Message = ConversationMessageSnapshot;
|
|
27
|
-
|
|
28
|
-
/** One consolidated run of tool-result messages sharing one assistant turn. */
|
|
29
|
-
export interface ToolGroup {
|
|
30
|
-
/** Absolute index of the assistant message that owns this group. */
|
|
31
|
-
readonly assistantIdx: number;
|
|
32
|
-
/** Absolute indexes of the matched tool-result messages, in order. */
|
|
33
|
-
readonly toolMessageIndexes: readonly number[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Per-tool-message membership, keyed by the ABSOLUTE index of the tool-result
|
|
38
|
-
* message (the owning assistant message is never a key — only its tool-result
|
|
39
|
-
* children are). `isFirst` marks the member whose render site owns the
|
|
40
|
-
* group's header line and BlockMeta; every other member renders nothing while
|
|
41
|
-
* the group is collapsed (see renderConversationToolMessage).
|
|
42
|
-
*/
|
|
43
|
-
export interface ToolGroupMembership {
|
|
44
|
-
readonly groupKey: string;
|
|
45
|
-
readonly isFirst: boolean;
|
|
46
|
-
/** Matched tool-result messages in the group (the header's "N tools"). */
|
|
47
|
-
readonly toolCount: number;
|
|
48
|
-
/** Sum of each member's EXPANDED render line count (the header's "N lines") —
|
|
49
|
-
* the same post-render count each member's own "N lines" badge shows, so
|
|
50
|
-
* the group total matches what expanding every member actually reveals,
|
|
51
|
-
* not a raw-content estimate (a folded JSON result can pretty-print to far
|
|
52
|
-
* more lines than its raw content has). */
|
|
53
|
-
readonly totalLines: number;
|
|
54
|
-
/** Each member's tool name, in order (falls back to 'tool' when a result
|
|
55
|
-
* carries no name) — the header dedupes these into e.g. "read×3 exec". */
|
|
56
|
-
readonly toolNames: readonly string[];
|
|
57
|
-
/**
|
|
58
|
-
* Absolute indexes of every tool-result message in the group, in order —
|
|
59
|
-
* the same list for every member. Carried onto the group's BlockMeta so a
|
|
60
|
-
* single expand pass can also open each member's own collapse key
|
|
61
|
-
* (`msg_<idx>`), since a folded member pushes no BlockMeta of its own to
|
|
62
|
-
* toggle individually (see local-runtime.ts's toggleBlocks).
|
|
63
|
-
*/
|
|
64
|
-
readonly memberIndexes: readonly number[];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Scan `messages` for assistant-message runs of >=2 consecutive matching
|
|
69
|
-
* tool-result messages. A run breaks on the first message that isn't a
|
|
70
|
-
* matching tool result: any non-tool message (interleaved user/system/
|
|
71
|
-
* assistant content), or a tool message whose callId isn't one of the
|
|
72
|
-
* assistant's own toolCalls ids (a result belonging to a different turn never
|
|
73
|
-
* gets folded into a group it doesn't belong to).
|
|
74
|
-
*/
|
|
75
|
-
export function detectToolGroups(
|
|
76
|
-
messages: readonly Message[],
|
|
77
|
-
indexOffset = 0,
|
|
78
|
-
): ToolGroup[] {
|
|
79
|
-
const groups: ToolGroup[] = [];
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < messages.length; i++) {
|
|
82
|
-
const message = messages[i];
|
|
83
|
-
if (message.role !== 'assistant' || !message.toolCalls || message.toolCalls.length === 0) continue;
|
|
84
|
-
const callIds = new Set(
|
|
85
|
-
message.toolCalls
|
|
86
|
-
.map((tc) => tc.id)
|
|
87
|
-
.filter((id): id is string => id !== undefined),
|
|
88
|
-
);
|
|
89
|
-
if (callIds.size === 0) continue;
|
|
90
|
-
|
|
91
|
-
const toolMessageIndexes: number[] = [];
|
|
92
|
-
let j = i + 1;
|
|
93
|
-
while (j < messages.length) {
|
|
94
|
-
const next = messages[j];
|
|
95
|
-
if (next.role !== 'tool' || !next.callId || !callIds.has(next.callId)) break;
|
|
96
|
-
toolMessageIndexes.push(indexOffset + j);
|
|
97
|
-
j++;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (toolMessageIndexes.length >= 2) {
|
|
101
|
-
groups.push({ assistantIdx: indexOffset + i, toolMessageIndexes });
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return groups;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Build the per-tool-message membership lookup used by the renderer/cache.
|
|
109
|
-
* `width` must match the width the transcript is actually rendered at — it
|
|
110
|
-
* feeds the same expanded-render line count each member's own badge uses
|
|
111
|
-
* (see renderExpandedToolResultLines), so the group total is never computed
|
|
112
|
-
* against a different width than what the member badges show. */
|
|
113
|
-
export function buildToolGroupMembership(
|
|
114
|
-
groups: readonly ToolGroup[],
|
|
115
|
-
messages: readonly Message[],
|
|
116
|
-
indexOffset: number,
|
|
117
|
-
width: number,
|
|
118
|
-
): ReadonlyMap<number, ToolGroupMembership> {
|
|
119
|
-
const membership = new Map<number, ToolGroupMembership>();
|
|
120
|
-
for (const group of groups) {
|
|
121
|
-
const groupKey = `group_${group.assistantIdx}`;
|
|
122
|
-
let totalLines = 0;
|
|
123
|
-
const toolNames: string[] = [];
|
|
124
|
-
for (const absIdx of group.toolMessageIndexes) {
|
|
125
|
-
const message = messages[absIdx - indexOffset];
|
|
126
|
-
if (message && message.role === 'tool') {
|
|
127
|
-
totalLines += renderExpandedToolResultLines(message.content, width).length;
|
|
128
|
-
toolNames.push(message.toolName ?? 'tool');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
group.toolMessageIndexes.forEach((absIdx, idx) => {
|
|
132
|
-
membership.set(absIdx, {
|
|
133
|
-
groupKey,
|
|
134
|
-
isFirst: idx === 0,
|
|
135
|
-
toolCount: group.toolMessageIndexes.length,
|
|
136
|
-
totalLines,
|
|
137
|
-
toolNames,
|
|
138
|
-
memberIndexes: group.toolMessageIndexes,
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
return membership;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** Convenience wrapper: detect groups, then build the membership map in one call. */
|
|
146
|
-
export function computeToolGroupMembership(
|
|
147
|
-
messages: readonly Message[],
|
|
148
|
-
indexOffset = 0,
|
|
149
|
-
width = 80,
|
|
150
|
-
): ReadonlyMap<number, ToolGroupMembership> {
|
|
151
|
-
return buildToolGroupMembership(detectToolGroups(messages, indexOffset), messages, indexOffset, width);
|
|
152
|
-
}
|