@makerbi/remodex 3.2.0 → 3.3.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/package.json +1 -1
- package/src/bridge.js +160 -30
- package/src/codex-desktop-refresher.js +8 -8
- package/src/codex-runtime-settings.js +113 -0
- package/src/desktop-ipc-action-follower.js +269 -92
- package/src/desktop-ipc-conversation-adapter.js +56 -51
- package/src/desktop-ipc-conversation-projector.js +1 -0
- package/src/desktop-ipc-live-owner.js +189 -164
- package/src/desktop-ipc-shared.js +35 -1
- package/src/git-handler.js +20 -17
- package/src/rollout-live-mirror.js +3 -4
- package/src/runtime-provider-router.js +10 -2
- package/src/thread-activity-projector.js +694 -0
- package/src/thread-activity-store.js +325 -0
- package/src/thread-runtime-settings-store.js +60 -74
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// FILE: thread-activity-store.js
|
|
2
|
+
// Purpose: Owns Activity snapshots, revisions, opt-in delivery, and bounded retention.
|
|
3
|
+
// Layer: CLI helper
|
|
4
|
+
// Exports: createThreadActivityStore
|
|
5
|
+
// Depends on: crypto
|
|
6
|
+
|
|
7
|
+
const { randomUUID } = require("crypto");
|
|
8
|
+
|
|
9
|
+
const ACTIVITY_SCHEMA_VERSION = 1;
|
|
10
|
+
const ACTIVITY_SUBSCRIBE_METHOD = "remodex/activity/subscribe";
|
|
11
|
+
const ACTIVITY_UNSUBSCRIBE_METHOD = "remodex/activity/unsubscribe";
|
|
12
|
+
const ACTIVITY_UPDATED_METHOD = "remodex/activity/updated";
|
|
13
|
+
const DEFAULT_MAX_INACTIVE_ENTRIES = 200;
|
|
14
|
+
const DEFAULT_COALESCE_MS = 225;
|
|
15
|
+
|
|
16
|
+
function createThreadActivityStore({
|
|
17
|
+
sendApplicationResponse,
|
|
18
|
+
createEpoch = randomUUID,
|
|
19
|
+
maxInactiveEntries = DEFAULT_MAX_INACTIVE_ENTRIES,
|
|
20
|
+
coalesceMs = DEFAULT_COALESCE_MS,
|
|
21
|
+
setTimeoutFn = setTimeout,
|
|
22
|
+
clearTimeoutFn = clearTimeout,
|
|
23
|
+
onEvict = () => {},
|
|
24
|
+
} = {}) {
|
|
25
|
+
const epoch = createEpoch();
|
|
26
|
+
const entriesByThreadId = new Map();
|
|
27
|
+
const serializedEntriesByThreadId = new Map();
|
|
28
|
+
const pendingUpsertsByThreadId = new Map();
|
|
29
|
+
const pendingRemovedThreadIds = new Set();
|
|
30
|
+
let revision = 0;
|
|
31
|
+
let deliveredRevision = 0;
|
|
32
|
+
let subscribed = false;
|
|
33
|
+
let flushTimer = null;
|
|
34
|
+
let disposed = false;
|
|
35
|
+
|
|
36
|
+
function handleRequest(message) {
|
|
37
|
+
if (disposed) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (message?.method === ACTIVITY_SUBSCRIBE_METHOD) {
|
|
41
|
+
handleSubscribe(message);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (message?.method === ACTIVITY_UNSUBSCRIBE_METHOD) {
|
|
45
|
+
handleUnsubscribe(message);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function handleSubscribe(message) {
|
|
52
|
+
if (!validRequest(message)) {
|
|
53
|
+
sendInvalidParams(message?.id);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
clearPendingDelivery();
|
|
57
|
+
sendResponse(message.id, snapshot());
|
|
58
|
+
deliveredRevision = revision;
|
|
59
|
+
subscribed = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleUnsubscribe(message) {
|
|
63
|
+
if (!validRequest(message)) {
|
|
64
|
+
sendInvalidParams(message?.id);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
resetSubscriber();
|
|
68
|
+
sendResponse(message.id, { schemaVersion: ACTIVITY_SCHEMA_VERSION });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function upsert(entry) {
|
|
72
|
+
if (disposed || !validEntry(entry)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const previous = entriesByThreadId.get(entry.threadId) || null;
|
|
76
|
+
const serialized = JSON.stringify(entry);
|
|
77
|
+
if (serializedEntriesByThreadId.get(entry.threadId) === serialized) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
entriesByThreadId.delete(entry.threadId);
|
|
82
|
+
entriesByThreadId.set(entry.threadId, entry);
|
|
83
|
+
serializedEntriesByThreadId.set(entry.threadId, serialized);
|
|
84
|
+
const removedThreadIds = evictInactiveEntries();
|
|
85
|
+
commitChanges({
|
|
86
|
+
upserts: [entry],
|
|
87
|
+
removedThreadIds,
|
|
88
|
+
urgent: isUrgentChange(previous, entry) || removedThreadIds.length > 0,
|
|
89
|
+
});
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function remove(threadId, { source = "" } = {}) {
|
|
94
|
+
const entry = entriesByThreadId.get(threadId);
|
|
95
|
+
if (!entry || (source && entry.source !== source)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
entriesByThreadId.delete(threadId);
|
|
99
|
+
serializedEntriesByThreadId.delete(threadId);
|
|
100
|
+
commitChanges({ removedThreadIds: [threadId], urgent: true });
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function markSourceStale(source, sourceGeneration = null) {
|
|
105
|
+
const staleEntries = [];
|
|
106
|
+
for (const entry of entriesByThreadId.values()) {
|
|
107
|
+
if (entry.source !== source
|
|
108
|
+
|| (sourceGeneration != null && entry.sourceGeneration !== sourceGeneration)
|
|
109
|
+
|| entry.freshness === "stale") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
staleEntries.push({ ...entry, freshness: "stale" });
|
|
113
|
+
}
|
|
114
|
+
if (staleEntries.length === 0) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
for (const entry of staleEntries) {
|
|
118
|
+
entriesByThreadId.set(entry.threadId, entry);
|
|
119
|
+
serializedEntriesByThreadId.set(entry.threadId, JSON.stringify(entry));
|
|
120
|
+
}
|
|
121
|
+
commitChanges({ upserts: staleEntries, urgent: true });
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resetSubscriber() {
|
|
126
|
+
subscribed = false;
|
|
127
|
+
clearPendingDelivery();
|
|
128
|
+
deliveredRevision = revision;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function dispose() {
|
|
132
|
+
disposed = true;
|
|
133
|
+
resetSubscriber();
|
|
134
|
+
entriesByThreadId.clear();
|
|
135
|
+
serializedEntriesByThreadId.clear();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function snapshot() {
|
|
139
|
+
return {
|
|
140
|
+
schemaVersion: ACTIVITY_SCHEMA_VERSION,
|
|
141
|
+
epoch,
|
|
142
|
+
revision,
|
|
143
|
+
coverage: "observedThreads",
|
|
144
|
+
entries: [...entriesByThreadId.values()]
|
|
145
|
+
.slice()
|
|
146
|
+
.sort((left, right) => left.threadId.localeCompare(right.threadId)),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function evictInactiveEntries() {
|
|
151
|
+
let inactiveCount = [...entriesByThreadId.values()].filter(isInactiveEntry).length;
|
|
152
|
+
const removedThreadIds = [];
|
|
153
|
+
if (inactiveCount <= maxInactiveEntries) {
|
|
154
|
+
return removedThreadIds;
|
|
155
|
+
}
|
|
156
|
+
for (const [threadId, entry] of entriesByThreadId) {
|
|
157
|
+
if (!isInactiveEntry(entry)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
entriesByThreadId.delete(threadId);
|
|
161
|
+
serializedEntriesByThreadId.delete(threadId);
|
|
162
|
+
removedThreadIds.push(threadId);
|
|
163
|
+
onEvict(threadId, entry.source);
|
|
164
|
+
inactiveCount -= 1;
|
|
165
|
+
if (inactiveCount <= maxInactiveEntries) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return removedThreadIds;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function commitChanges({ upserts = [], removedThreadIds = [], urgent = false }) {
|
|
173
|
+
revision += 1;
|
|
174
|
+
if (!subscribed) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const entry of upserts) {
|
|
178
|
+
pendingRemovedThreadIds.delete(entry.threadId);
|
|
179
|
+
pendingUpsertsByThreadId.set(entry.threadId, entry);
|
|
180
|
+
}
|
|
181
|
+
for (const threadId of removedThreadIds) {
|
|
182
|
+
pendingUpsertsByThreadId.delete(threadId);
|
|
183
|
+
pendingRemovedThreadIds.add(threadId);
|
|
184
|
+
}
|
|
185
|
+
if (urgent) {
|
|
186
|
+
flushPendingDelivery();
|
|
187
|
+
} else {
|
|
188
|
+
scheduleFlush();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function scheduleFlush() {
|
|
193
|
+
if (flushTimer) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
flushTimer = setTimeoutFn(flushPendingDelivery, coalesceMs);
|
|
197
|
+
flushTimer?.unref?.();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function flushPendingDelivery() {
|
|
201
|
+
clearFlushTimer();
|
|
202
|
+
if (!subscribed || !hasPendingDelivery()) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const notification = {
|
|
206
|
+
method: ACTIVITY_UPDATED_METHOD,
|
|
207
|
+
params: {
|
|
208
|
+
schemaVersion: ACTIVITY_SCHEMA_VERSION,
|
|
209
|
+
epoch,
|
|
210
|
+
baseRevision: deliveredRevision,
|
|
211
|
+
revision,
|
|
212
|
+
upserts: [...pendingUpsertsByThreadId.values()]
|
|
213
|
+
.sort((left, right) => left.threadId.localeCompare(right.threadId)),
|
|
214
|
+
removedThreadIds: [...pendingRemovedThreadIds].sort(),
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
clearPendingCollections();
|
|
218
|
+
deliveredRevision = revision;
|
|
219
|
+
sendApplicationResponse(JSON.stringify(notification));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clearPendingDelivery() {
|
|
223
|
+
clearFlushTimer();
|
|
224
|
+
clearPendingCollections();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function clearPendingCollections() {
|
|
228
|
+
pendingUpsertsByThreadId.clear();
|
|
229
|
+
pendingRemovedThreadIds.clear();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function clearFlushTimer() {
|
|
233
|
+
if (!flushTimer) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
clearTimeoutFn(flushTimer);
|
|
237
|
+
flushTimer = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function hasPendingDelivery() {
|
|
241
|
+
return pendingUpsertsByThreadId.size > 0 || pendingRemovedThreadIds.size > 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function sendResponse(id, result) {
|
|
245
|
+
if (id == null) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
sendApplicationResponse(JSON.stringify({ id, result }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function sendInvalidParams(id) {
|
|
252
|
+
if (id == null) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
sendApplicationResponse(JSON.stringify({
|
|
256
|
+
id,
|
|
257
|
+
error: {
|
|
258
|
+
code: -32602,
|
|
259
|
+
message: "Activity requests support schemaVersion 1.",
|
|
260
|
+
},
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
get(threadId) { return entriesByThreadId.get(threadId) || null; },
|
|
266
|
+
dispose,
|
|
267
|
+
flush: flushPendingDelivery,
|
|
268
|
+
handleRequest,
|
|
269
|
+
markSourceStale,
|
|
270
|
+
remove,
|
|
271
|
+
resetSubscriber,
|
|
272
|
+
snapshot,
|
|
273
|
+
upsert,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function validRequest(message) {
|
|
278
|
+
if (message?.id == null) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const params = message.params;
|
|
282
|
+
if (params == null) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
return typeof params === "object"
|
|
286
|
+
&& !Array.isArray(params)
|
|
287
|
+
&& (params.schemaVersion == null || params.schemaVersion === ACTIVITY_SCHEMA_VERSION);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function validEntry(entry) {
|
|
291
|
+
return entry
|
|
292
|
+
&& typeof entry === "object"
|
|
293
|
+
&& typeof entry.threadId === "string"
|
|
294
|
+
&& entry.threadId.length > 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isInactiveEntry(entry) {
|
|
298
|
+
return entry.runtime !== "active"
|
|
299
|
+
&& entry.runningWithoutTurnId !== true
|
|
300
|
+
&& entry.approvalRequired !== true
|
|
301
|
+
&& entry.userInputRequired !== true;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function isUrgentChange(previous, next) {
|
|
305
|
+
if (!previous || previous.source !== next.source) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return previous.runtime !== next.runtime
|
|
309
|
+
|| previous.runningWithoutTurnId !== next.runningWithoutTurnId
|
|
310
|
+
|| previous.approvalRequestCount !== next.approvalRequestCount
|
|
311
|
+
|| previous.approvalRequired !== next.approvalRequired
|
|
312
|
+
|| previous.userInputRequestCount !== next.userInputRequestCount
|
|
313
|
+
|| previous.userInputRequired !== next.userInputRequired
|
|
314
|
+
|| previous.freshness !== next.freshness
|
|
315
|
+
|| JSON.stringify(previous.activeTurnIds) !== JSON.stringify(next.activeTurnIds)
|
|
316
|
+
|| JSON.stringify(previous.lastOutcome) !== JSON.stringify(next.lastOutcome);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
ACTIVITY_SCHEMA_VERSION,
|
|
321
|
+
ACTIVITY_SUBSCRIBE_METHOD,
|
|
322
|
+
ACTIVITY_UNSUBSCRIBE_METHOD,
|
|
323
|
+
ACTIVITY_UPDATED_METHOD,
|
|
324
|
+
createThreadActivityStore,
|
|
325
|
+
};
|
|
@@ -9,7 +9,10 @@ const os = require("os");
|
|
|
9
9
|
const path = require("path");
|
|
10
10
|
const { forEachThreadRowInResponse } = require("./thread-row-enrichment");
|
|
11
11
|
|
|
12
|
-
const
|
|
12
|
+
const { randomUUID } = require("crypto");
|
|
13
|
+
const { runtimeSettingsPatch, runtimeSettingsFromConversation } = require("./codex-runtime-settings");
|
|
14
|
+
|
|
15
|
+
const STORE_VERSION = 2;
|
|
13
16
|
const DEFAULT_MAX_THREADS = 500;
|
|
14
17
|
const DEFAULT_MAX_AGE_MS = 180 * 24 * 60 * 60 * 1_000;
|
|
15
18
|
const DEFAULT_STORE_DIR = path.join(os.homedir(), ".remodex");
|
|
@@ -21,6 +24,8 @@ function createThreadRuntimeSettingsStore({
|
|
|
21
24
|
now = () => Date.now(),
|
|
22
25
|
maxThreads = DEFAULT_MAX_THREADS,
|
|
23
26
|
maxAgeMs = DEFAULT_MAX_AGE_MS,
|
|
27
|
+
onChange = () => {},
|
|
28
|
+
onError = (error) => console.warn(`[remodex] runtime settings persistence failed: ${error.message}`),
|
|
24
29
|
} = {}) {
|
|
25
30
|
let state = readState({ storeFile, fsImpl });
|
|
26
31
|
|
|
@@ -29,26 +34,24 @@ function createThreadRuntimeSettingsStore({
|
|
|
29
34
|
if (!normalizedThreadId) {
|
|
30
35
|
return null;
|
|
31
36
|
}
|
|
32
|
-
|
|
37
|
+
const settings = state.threads[normalizedThreadId];
|
|
38
|
+
return settings?.confirmed ? cloneSettings(settings) : null;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
function commit(threadId, turnParams, { source = "unknown", turnId = "" } = {}) {
|
|
36
42
|
const normalizedThreadId = normalizeString(threadId);
|
|
37
43
|
const nextSource = normalizeString(source) || "unknown";
|
|
38
|
-
|
|
39
|
-
// runtime that executes its turn, while Desktop choices stay local.
|
|
40
|
-
if (!normalizedThreadId || nextSource !== "phone") {
|
|
44
|
+
if (!normalizedThreadId || !["phone", "desktop", "runtime"].includes(nextSource)) {
|
|
41
45
|
return null;
|
|
42
46
|
}
|
|
43
|
-
const previous =
|
|
44
|
-
const nextValues = runtimeSettingsFromTurnParams(turnParams, previous);
|
|
45
|
-
if (
|
|
47
|
+
const previous = get(normalizedThreadId);
|
|
48
|
+
const nextValues = runtimeSettingsFromTurnParams(turnParams, previous, { authoritative: source === "runtime" });
|
|
49
|
+
if (Object.keys(nextValues).length === 0) {
|
|
46
50
|
return null;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
const normalizedTurnId = normalizeString(turnId);
|
|
50
54
|
if (previous
|
|
51
|
-
&& previous.turnId === normalizedTurnId
|
|
52
55
|
&& previous.model === nextValues.model
|
|
53
56
|
&& previous.reasoningEffort === nextValues.reasoningEffort
|
|
54
57
|
&& previous.serviceTier === nextValues.serviceTier) {
|
|
@@ -56,20 +59,33 @@ function createThreadRuntimeSettingsStore({
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
const next = {
|
|
59
|
-
|
|
60
|
-
reasoningEffort: nextValues.reasoningEffort || null,
|
|
61
|
-
serviceTier: nextValues.serviceTier,
|
|
62
|
+
...nextValues,
|
|
62
63
|
revision: Math.max(0, Number(previous?.revision) || 0) + 1,
|
|
63
|
-
updatedAt: now(),
|
|
64
|
+
updatedAt: Math.max(now(), (previous?.updatedAt || 0) + 1),
|
|
65
|
+
epoch: previous?.epoch || randomUUID(),
|
|
66
|
+
confirmed: true,
|
|
64
67
|
source: nextSource,
|
|
65
68
|
turnId: normalizedTurnId || null,
|
|
66
69
|
};
|
|
67
|
-
state.threads[normalizedThreadId]
|
|
68
|
-
pruneState(
|
|
69
|
-
writeState(
|
|
70
|
+
const nextState = { ...state, threads: { ...state.threads, [normalizedThreadId]: next } };
|
|
71
|
+
pruneState(nextState, { now: now(), maxThreads, maxAgeMs });
|
|
72
|
+
writeState(nextState, { storeFile, fsImpl });
|
|
73
|
+
state = nextState;
|
|
74
|
+
onChange(normalizedThreadId, cloneSettings(next));
|
|
70
75
|
return cloneSettings(next);
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
// Unsolicited owner notifications must not interrupt the live event stream
|
|
79
|
+
// when the local settings cache cannot be written. A later snapshot retries.
|
|
80
|
+
function observe(threadId, settings, source = "runtime") {
|
|
81
|
+
try {
|
|
82
|
+
return commit(threadId, settings, { source });
|
|
83
|
+
} catch (error) {
|
|
84
|
+
onError(error);
|
|
85
|
+
return get(threadId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
function attachToConversation(threadId, conversation) {
|
|
74
90
|
if (!conversation || typeof conversation !== "object") {
|
|
75
91
|
return conversation;
|
|
@@ -79,34 +95,6 @@ function createThreadRuntimeSettingsStore({
|
|
|
79
95
|
return conversation;
|
|
80
96
|
}
|
|
81
97
|
conversation.remodexRuntimeSettings = settings;
|
|
82
|
-
if (settings.model) {
|
|
83
|
-
conversation.latestModel = settings.model;
|
|
84
|
-
}
|
|
85
|
-
if (settings.reasoningEffort) {
|
|
86
|
-
conversation.latestReasoningEffort = settings.reasoningEffort;
|
|
87
|
-
}
|
|
88
|
-
conversation.latestServiceTier = settings.serviceTier;
|
|
89
|
-
conversation.latestThreadSettings = {
|
|
90
|
-
...(conversation.latestThreadSettings && typeof conversation.latestThreadSettings === "object"
|
|
91
|
-
? conversation.latestThreadSettings
|
|
92
|
-
: {}),
|
|
93
|
-
model: settings.model,
|
|
94
|
-
effort: settings.reasoningEffort,
|
|
95
|
-
serviceTier: settings.serviceTier,
|
|
96
|
-
};
|
|
97
|
-
const collaborationSettings = conversation.latestCollaborationMode?.settings;
|
|
98
|
-
conversation.latestCollaborationMode = {
|
|
99
|
-
mode: conversation.latestCollaborationMode?.mode || "default",
|
|
100
|
-
settings: {
|
|
101
|
-
...(collaborationSettings && typeof collaborationSettings === "object"
|
|
102
|
-
? collaborationSettings
|
|
103
|
-
: { developer_instructions: null }),
|
|
104
|
-
model: settings.model || collaborationSettings?.model || "",
|
|
105
|
-
reasoning_effort: settings.reasoningEffort
|
|
106
|
-
|| collaborationSettings?.reasoning_effort
|
|
107
|
-
|| null,
|
|
108
|
-
},
|
|
109
|
-
};
|
|
110
98
|
return conversation;
|
|
111
99
|
}
|
|
112
100
|
|
|
@@ -120,9 +108,12 @@ function createThreadRuntimeSettingsStore({
|
|
|
120
108
|
if (!settings || !thread || typeof thread !== "object") {
|
|
121
109
|
return thread;
|
|
122
110
|
}
|
|
123
|
-
thread.
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
thread.runtimeSettings = settings;
|
|
112
|
+
// Legacy phone fields remain readable, while the v2 object explicitly
|
|
113
|
+
// represents next-turn choices rather than an executing turn's metadata.
|
|
114
|
+
thread.model ||= settings.model;
|
|
115
|
+
if (Object.hasOwn(settings, "reasoningEffort")) thread.reasoningEffort = settings.reasoningEffort;
|
|
116
|
+
if (Object.hasOwn(settings, "serviceTier")) thread.serviceTier = settings.serviceTier === "priority" ? "fast" : settings.serviceTier;
|
|
126
117
|
thread.runtimeSettingsRevision = settings.revision;
|
|
127
118
|
thread.runtimeSettingsUpdatedAt = settings.updatedAt;
|
|
128
119
|
thread.runtimeSettingsSource = settings.source;
|
|
@@ -132,31 +123,25 @@ function createThreadRuntimeSettingsStore({
|
|
|
132
123
|
return {
|
|
133
124
|
get,
|
|
134
125
|
commit,
|
|
126
|
+
observe,
|
|
135
127
|
attachToConversation,
|
|
136
128
|
attachToThread,
|
|
137
129
|
enrichResponse,
|
|
130
|
+
observeConversation(threadId, conversation) {
|
|
131
|
+
const patch = runtimeSettingsFromConversation(conversation);
|
|
132
|
+
if (!patch.model) return get(threadId);
|
|
133
|
+
const settings = observe(threadId, patch, "desktop");
|
|
134
|
+
attachToConversation(threadId, conversation);
|
|
135
|
+
return settings;
|
|
136
|
+
},
|
|
138
137
|
};
|
|
139
138
|
}
|
|
140
139
|
|
|
141
|
-
function runtimeSettingsFromTurnParams(turnParams, previous = null) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const model = normalizeString(params.model)
|
|
147
|
-
|| normalizeString(collaborationSettings.model)
|
|
148
|
-
|| normalizeString(previous?.model)
|
|
149
|
-
|| null;
|
|
150
|
-
const reasoningEffort = normalizeString(params.effort)
|
|
151
|
-
|| normalizeString(params.reasoningEffort)
|
|
152
|
-
|| normalizeString(collaborationSettings.reasoning_effort)
|
|
153
|
-
|| normalizeString(collaborationSettings.reasoningEffort)
|
|
154
|
-
|| normalizeString(previous?.reasoningEffort)
|
|
155
|
-
|| null;
|
|
156
|
-
const serviceTier = normalizeString(params.serviceTier)
|
|
157
|
-
|| normalizeString(params.service_tier)
|
|
158
|
-
|| null;
|
|
159
|
-
return { model, reasoningEffort, serviceTier };
|
|
140
|
+
function runtimeSettingsFromTurnParams(turnParams, previous = null, options = {}) {
|
|
141
|
+
return {
|
|
142
|
+
...runtimeSettingsPatch(previous || {}),
|
|
143
|
+
...runtimeSettingsPatch(turnParams, options),
|
|
144
|
+
};
|
|
160
145
|
}
|
|
161
146
|
|
|
162
147
|
function readState({ storeFile, fsImpl }) {
|
|
@@ -164,7 +149,7 @@ function readState({ storeFile, fsImpl }) {
|
|
|
164
149
|
const parsed = JSON.parse(fsImpl.readFileSync(storeFile, "utf8"));
|
|
165
150
|
return normalizeState(parsed);
|
|
166
151
|
} catch {
|
|
167
|
-
return { version: STORE_VERSION, threads: {} };
|
|
152
|
+
return { version: STORE_VERSION, epoch: randomUUID(), threads: {} };
|
|
168
153
|
}
|
|
169
154
|
}
|
|
170
155
|
|
|
@@ -173,25 +158,26 @@ function normalizeState(rawState) {
|
|
|
173
158
|
? rawState.threads
|
|
174
159
|
: {};
|
|
175
160
|
const threads = {};
|
|
161
|
+
const epoch = normalizeString(rawState?.epoch) || randomUUID();
|
|
176
162
|
for (const [threadId, rawSettings] of Object.entries(rawThreads)) {
|
|
177
163
|
const normalizedThreadId = normalizeString(threadId);
|
|
178
164
|
const source = normalizeString(rawSettings?.source) || "unknown";
|
|
179
|
-
|
|
180
|
-
// cannot be replayed to the phone after upgrading.
|
|
181
|
-
if (!normalizedThreadId || !rawSettings || typeof rawSettings !== "object" || source !== "phone") {
|
|
165
|
+
if (!normalizedThreadId || !rawSettings || typeof rawSettings !== "object") {
|
|
182
166
|
continue;
|
|
183
167
|
}
|
|
184
168
|
threads[normalizedThreadId] = {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
169
|
+
...runtimeSettingsFromTurnParams(rawSettings),
|
|
170
|
+
epoch: normalizeString(rawSettings.epoch) || epoch,
|
|
171
|
+
// Preserve old preferences on disk, but require fresh owner evidence
|
|
172
|
+
// before exposing them as confirmed runtime state.
|
|
173
|
+
confirmed: rawState.version === STORE_VERSION && rawSettings.confirmed === true,
|
|
188
174
|
revision: Math.max(0, Number(rawSettings.revision) || 0),
|
|
189
175
|
updatedAt: Math.max(0, Number(rawSettings.updatedAt) || 0),
|
|
190
176
|
source,
|
|
191
177
|
turnId: normalizeString(rawSettings.turnId) || null,
|
|
192
178
|
};
|
|
193
179
|
}
|
|
194
|
-
return { version: STORE_VERSION, threads };
|
|
180
|
+
return { version: STORE_VERSION, epoch, threads };
|
|
195
181
|
}
|
|
196
182
|
|
|
197
183
|
function pruneState(storeState, { now, maxThreads, maxAgeMs }) {
|