@qearlyao/familiar 0.1.2 → 0.2.1

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.
@@ -0,0 +1,289 @@
1
+ import { loadConfigOverrides } from "./config-overrides.js";
2
+ import { isAllowedModel, parseModelRef } from "./models.js";
3
+ function requireBoolean(value, key) {
4
+ if (typeof value !== "boolean")
5
+ throw new Error(`${key} must be a boolean`);
6
+ return value;
7
+ }
8
+ function requirePositiveInt(value, key) {
9
+ const n = typeof value === "number" ? value : Number(value);
10
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
11
+ throw new Error(`${key} must be a positive integer`);
12
+ }
13
+ return n;
14
+ }
15
+ function requireInt(value, key, min) {
16
+ const n = typeof value === "number" ? value : Number(value);
17
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < min) {
18
+ throw new Error(`${key} must be an integer >= ${min}`);
19
+ }
20
+ return n;
21
+ }
22
+ function requireNumberInRange(value, key, min, max) {
23
+ const n = typeof value === "number" ? value : Number(value);
24
+ if (!Number.isFinite(n) || n < min || n > max) {
25
+ throw new Error(`${key} must be a number between ${min} and ${max}`);
26
+ }
27
+ return n;
28
+ }
29
+ function requireNonNegativeInt(value, key) {
30
+ const n = typeof value === "number" ? value : Number(value);
31
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
32
+ throw new Error(`${key} must be a non-negative integer`);
33
+ }
34
+ return n;
35
+ }
36
+ function requireNonNegativeNumber(value, key) {
37
+ const n = typeof value === "number" ? value : Number(value);
38
+ if (!Number.isFinite(n) || n < 0) {
39
+ throw new Error(`${key} must be a number >= 0`);
40
+ }
41
+ return n;
42
+ }
43
+ function resolveProviderSetting(records, provider, modelId) {
44
+ return records[`${provider}/${modelId}`] ?? records[provider];
45
+ }
46
+ export const CONFIG_REGISTRY = {
47
+ "heartbeat.enabled": {
48
+ read: (config) => config.heartbeat.enabled,
49
+ validate: (value) => requireBoolean(value, "heartbeat.enabled"),
50
+ write: (config, value) => {
51
+ config.heartbeat.enabled = value;
52
+ },
53
+ apply: ({ discordDaemon }) => {
54
+ discordDaemon.rearmHeartbeat();
55
+ },
56
+ },
57
+ "heartbeat.idleThresholdMs": {
58
+ read: (config) => config.heartbeat.idleThresholdMs,
59
+ validate: (value) => requirePositiveInt(value, "heartbeat.idleThresholdMs"),
60
+ write: (config, value) => {
61
+ config.heartbeat.idleThresholdMs = value;
62
+ },
63
+ apply: ({ discordDaemon }) => {
64
+ discordDaemon.rearmHeartbeat();
65
+ },
66
+ },
67
+ "heartbeat.intervalMs": {
68
+ read: (config) => config.heartbeat.intervalMs,
69
+ validate: (value) => requirePositiveInt(value, "heartbeat.intervalMs"),
70
+ write: (config, value) => {
71
+ config.heartbeat.intervalMs = value;
72
+ },
73
+ apply: ({ discordDaemon }) => {
74
+ discordDaemon.rearmHeartbeat();
75
+ },
76
+ },
77
+ "image_gen.enabled": {
78
+ read: (config) => config.imageGen.enabled,
79
+ validate: (value) => requireBoolean(value, "image_gen.enabled"),
80
+ write: (config, value) => {
81
+ config.imageGen.enabled = value;
82
+ },
83
+ },
84
+ "image_gen.model": {
85
+ read: (config) => config.imageGen.model,
86
+ validate: (value) => {
87
+ if (typeof value !== "string")
88
+ throw new Error("image_gen.model must be a string");
89
+ const ref = parseModelRef(value);
90
+ if (!ref)
91
+ throw new Error("image_gen.model: format must be provider/model-id");
92
+ return ref.key;
93
+ },
94
+ write: (config, value) => {
95
+ const ref = parseModelRef(value);
96
+ if (!ref)
97
+ return;
98
+ config.imageGen.model = ref.key;
99
+ },
100
+ },
101
+ "image_gen.fallback_model": {
102
+ read: (config) => config.imageGen.fallbackModel ?? "",
103
+ validate: (value) => {
104
+ if (value == null || value === "")
105
+ return "";
106
+ if (typeof value !== "string")
107
+ throw new Error("image_gen.fallback_model must be a string");
108
+ const ref = parseModelRef(value);
109
+ if (!ref)
110
+ throw new Error("image_gen.fallback_model: format must be provider/model-id");
111
+ return ref.key;
112
+ },
113
+ write: (config, value) => {
114
+ if (value == null || value === "") {
115
+ config.imageGen.fallbackModel = undefined;
116
+ return;
117
+ }
118
+ const ref = parseModelRef(value);
119
+ if (!ref)
120
+ return;
121
+ config.imageGen.fallbackModel = ref.key;
122
+ },
123
+ },
124
+ "memory.lcm.enabled": {
125
+ read: (config) => config.memory.lcm.enabled,
126
+ validate: (value) => requireBoolean(value, "memory.lcm.enabled"),
127
+ write: (config, value) => {
128
+ config.memory.lcm.enabled = value;
129
+ },
130
+ },
131
+ "memory.lcm.model": {
132
+ read: (config) => config.memory.lcm.model,
133
+ validate: (value, config) => {
134
+ if (typeof value !== "string")
135
+ throw new Error("memory.lcm.model must be a string");
136
+ const ref = parseModelRef(value);
137
+ if (!ref)
138
+ throw new Error("memory.lcm.model: format must be provider/model-id");
139
+ if (!isAllowedModel(config, ref))
140
+ throw new Error(`memory.lcm.model is not allowlisted: ${ref.key}`);
141
+ return ref.key;
142
+ },
143
+ write: (config, value) => {
144
+ const ref = parseModelRef(value);
145
+ if (!ref)
146
+ return;
147
+ config.memory.lcm.model = ref.key;
148
+ config.memory.lcm.provider = ref.provider;
149
+ config.memory.lcm.modelId = ref.id;
150
+ config.memory.lcm.baseUrl = resolveProviderSetting(config.models.baseUrls, ref.provider, ref.id);
151
+ config.memory.lcm.apiKeyEnv = resolveProviderSetting(config.models.apiKeyEnvs, ref.provider, ref.id);
152
+ },
153
+ },
154
+ "memory.lcm.contextThreshold": {
155
+ read: (config) => config.memory.lcm.contextThreshold,
156
+ validate: (value) => requireNumberInRange(value, "memory.lcm.contextThreshold", 0, 1),
157
+ write: (config, value) => {
158
+ config.memory.lcm.contextThreshold = value;
159
+ },
160
+ },
161
+ "memory.lcm.freshTailCount": {
162
+ read: (config) => config.memory.lcm.freshTailCount,
163
+ validate: (value) => requirePositiveInt(value, "memory.lcm.freshTailCount"),
164
+ write: (config, value) => {
165
+ config.memory.lcm.freshTailCount = value;
166
+ },
167
+ },
168
+ "memory.lcm.leafChunkTokens": {
169
+ read: (config) => config.memory.lcm.leafChunkTokens,
170
+ validate: (value) => requirePositiveInt(value, "memory.lcm.leafChunkTokens"),
171
+ write: (config, value) => {
172
+ config.memory.lcm.leafChunkTokens = value;
173
+ },
174
+ },
175
+ "memory.lcm.leafTargetTokens": {
176
+ read: (config) => config.memory.lcm.leafTargetTokens,
177
+ validate: (value) => requirePositiveInt(value, "memory.lcm.leafTargetTokens"),
178
+ write: (config, value) => {
179
+ config.memory.lcm.leafTargetTokens = value;
180
+ },
181
+ },
182
+ "memory.lcm.condenseGroupSize": {
183
+ read: (config) => config.memory.lcm.condenseGroupSize,
184
+ validate: (value) => requirePositiveInt(value, "memory.lcm.condenseGroupSize"),
185
+ write: (config, value) => {
186
+ config.memory.lcm.condenseGroupSize = value;
187
+ },
188
+ },
189
+ "memory.lcm.maxSummaryDepth": {
190
+ read: (config) => config.memory.lcm.maxSummaryDepth,
191
+ validate: (value) => requirePositiveInt(value, "memory.lcm.maxSummaryDepth"),
192
+ write: (config, value) => {
193
+ config.memory.lcm.maxSummaryDepth = value;
194
+ },
195
+ },
196
+ "memory.lcm.newSessionRetainDepth": {
197
+ read: (config) => config.memory.lcm.newSessionRetainDepth,
198
+ validate: (value) => requireInt(value, "memory.lcm.newSessionRetainDepth", -1),
199
+ write: (config, value) => {
200
+ config.memory.lcm.newSessionRetainDepth = value;
201
+ },
202
+ },
203
+ "memory.ambient.enabled": {
204
+ read: (config) => config.memory.ambient.enabled,
205
+ validate: (value) => requireBoolean(value, "memory.ambient.enabled"),
206
+ write: (config, value) => {
207
+ config.memory.ambient.enabled = value;
208
+ },
209
+ },
210
+ "memory.ambient.topK": {
211
+ read: (config) => config.memory.ambient.topK,
212
+ validate: (value) => requirePositiveInt(value, "memory.ambient.topK"),
213
+ write: (config, value) => {
214
+ config.memory.ambient.topK = value;
215
+ },
216
+ },
217
+ "memory.ambient.minQueryLength": {
218
+ read: (config) => config.memory.ambient.minQueryLength,
219
+ validate: (value) => requireNonNegativeInt(value, "memory.ambient.minQueryLength"),
220
+ write: (config, value) => {
221
+ config.memory.ambient.minQueryLength = value;
222
+ },
223
+ },
224
+ "memory.ambient.throttleSeconds": {
225
+ read: (config) => config.memory.ambient.throttleSeconds,
226
+ validate: (value) => requireNonNegativeInt(value, "memory.ambient.throttleSeconds"),
227
+ write: (config, value) => {
228
+ config.memory.ambient.throttleSeconds = value;
229
+ },
230
+ },
231
+ "memory.ambient.weightSimilarity": {
232
+ read: (config) => config.memory.ambient.weightSimilarity,
233
+ validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightSimilarity"),
234
+ write: (config, value) => {
235
+ config.memory.ambient.weightSimilarity = value;
236
+ },
237
+ },
238
+ "memory.ambient.weightValence": {
239
+ read: (config) => config.memory.ambient.weightValence,
240
+ validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightValence"),
241
+ write: (config, value) => {
242
+ config.memory.ambient.weightValence = value;
243
+ },
244
+ },
245
+ "memory.ambient.weightRecency": {
246
+ read: (config) => config.memory.ambient.weightRecency,
247
+ validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightRecency"),
248
+ write: (config, value) => {
249
+ config.memory.ambient.weightRecency = value;
250
+ },
251
+ },
252
+ "memory.ambient.weightIntensity": {
253
+ read: (config) => config.memory.ambient.weightIntensity,
254
+ validate: (value) => requireNonNegativeNumber(value, "memory.ambient.weightIntensity"),
255
+ write: (config, value) => {
256
+ config.memory.ambient.weightIntensity = value;
257
+ },
258
+ },
259
+ };
260
+ export const CONFIG_KEYS = Object.keys(CONFIG_REGISTRY);
261
+ export function isConfigKey(value) {
262
+ return typeof value === "string" && value in CONFIG_REGISTRY;
263
+ }
264
+ const defaultSnapshot = new Map();
265
+ export function snapshotConfigDefaults(config) {
266
+ defaultSnapshot.clear();
267
+ for (const key of CONFIG_KEYS) {
268
+ defaultSnapshot.set(key, CONFIG_REGISTRY[key].read(config));
269
+ }
270
+ }
271
+ export function getConfigDefault(key) {
272
+ return defaultSnapshot.get(key);
273
+ }
274
+ export function applyConfigOverridesToConfig(config) {
275
+ snapshotConfigDefaults(config);
276
+ const overrides = loadConfigOverrides();
277
+ for (const [key, value] of Object.entries(overrides)) {
278
+ if (!isConfigKey(key))
279
+ continue;
280
+ const entry = CONFIG_REGISTRY[key];
281
+ try {
282
+ const validated = entry.validate(value, config);
283
+ entry.write(config, validated);
284
+ }
285
+ catch (error) {
286
+ console.warn(`Skipping invalid config override ${key}:`, error);
287
+ }
288
+ }
289
+ }
package/dist/config.js CHANGED
@@ -340,140 +340,17 @@ function readCronJobs(cron) {
340
340
  };
341
341
  });
342
342
  }
343
- function readBrowserAllowedSites(browser) {
344
- const rawSites = browser.sites;
345
- if (rawSites === undefined)
346
- return defaultBrowserAllowedSites();
347
- if (!rawSites || typeof rawSites !== "object" || Array.isArray(rawSites)) {
348
- throw new Error("Config value browser.sites must be a table");
349
- }
350
- const sites = {};
351
- for (const [siteName, rawSite] of Object.entries(rawSites)) {
352
- if (!rawSite || typeof rawSite !== "object" || Array.isArray(rawSite)) {
353
- throw new Error(`Config value browser.sites.${siteName} must be a table`);
354
- }
355
- const site = rawSite;
356
- assertKnownKeys(site, `browser.sites.${siteName}`, ["read", "write"]);
357
- const read = readStringArray(site.read, `browser.sites.${siteName}.read`);
358
- const write = readStringArray(site.write, `browser.sites.${siteName}.write`);
359
- sites[siteName] = { read, write };
360
- }
361
- return sites;
362
- }
363
343
  function defaultBrowserAllowedSites() {
364
344
  return {
365
- twitter: {
366
- read: [
367
- "article",
368
- "bookmark-folders",
369
- "bookmarks",
370
- "following",
371
- "likes",
372
- "list-tweets",
373
- "lists",
374
- "notifications",
375
- "profile",
376
- "search",
377
- "thread",
378
- "timeline",
379
- "trending",
380
- "tweets",
381
- ],
382
- write: [],
383
- },
384
- xiaohongshu: {
385
- read: [
386
- "comments",
387
- "creator-note-detail",
388
- "creator-notes",
389
- "creator-notes-summary",
390
- "creator-profile",
391
- "creator-stats",
392
- "feed",
393
- "note",
394
- "notifications",
395
- "search",
396
- "user",
397
- ],
398
- write: [],
399
- },
400
- rednote: {
401
- read: ["comments", "feed", "note", "notifications", "search", "user"],
402
- write: [],
403
- },
404
- reddit: {
405
- read: [
406
- "frontpage",
407
- "home",
408
- "hot",
409
- "popular",
410
- "read",
411
- "saved",
412
- "search",
413
- "subreddit",
414
- "subreddit-info",
415
- "upvoted",
416
- "user",
417
- "user-comments",
418
- "user-posts",
419
- "whoami",
420
- ],
421
- write: [],
422
- },
423
- bilibili: {
424
- read: [
425
- "comments",
426
- "dynamic",
427
- "feed",
428
- "following",
429
- "history",
430
- "hot",
431
- "me",
432
- "ranking",
433
- "search",
434
- "subtitle",
435
- "user-videos",
436
- "video",
437
- ],
438
- write: [],
439
- },
440
- youtube: {
441
- read: [
442
- "channel",
443
- "comments",
444
- "feed",
445
- "history",
446
- "playlist",
447
- "search",
448
- "subscriptions",
449
- "transcript",
450
- "video",
451
- "watch-later",
452
- ],
453
- write: [],
454
- },
455
- tiktok: {
456
- read: ["explore", "friends", "live", "notifications", "profile", "search", "user"],
457
- write: [],
458
- },
459
- douyin: {
460
- read: [
461
- "activities",
462
- "collections",
463
- "drafts",
464
- "hashtag",
465
- "location",
466
- "profile",
467
- "stats",
468
- "user-videos",
469
- "videos",
470
- ],
471
- write: [],
472
- },
473
- spotify: {
474
- read: ["search", "status"],
475
- write: [],
476
- },
345
+ twitter: true,
346
+ xiaohongshu: true,
347
+ rednote: true,
348
+ reddit: true,
349
+ bilibili: true,
350
+ youtube: true,
351
+ tiktok: true,
352
+ douyin: true,
353
+ spotify: true,
477
354
  };
478
355
  }
479
356
  export async function loadConfig(workspacePathInput) {
@@ -506,6 +383,18 @@ export async function loadConfig(workspacePathInput) {
506
383
  const persona = (parsed.persona ?? {});
507
384
  const workspace = (parsed.workspace ?? {});
508
385
  const memory = (parsed.memory ?? {});
386
+ assertKnownKeys(browser, "browser", [
387
+ "enabled",
388
+ "backend",
389
+ "opencli_command",
390
+ "harness_command",
391
+ "session",
392
+ "profile",
393
+ "window",
394
+ "timeout_ms",
395
+ "max_output_chars",
396
+ "read_write",
397
+ ]);
509
398
  const memoryEmbedding = (memory.embedding ?? {});
510
399
  const memoryAmbient = (memory.ambient ?? {});
511
400
  const memoryLcm = (memory.lcm ?? {});
@@ -644,7 +533,7 @@ export async function loadConfig(workspacePathInput) {
644
533
  timeoutMs: readInteger(browser.timeout_ms, 60_000, "browser.timeout_ms", 1),
645
534
  maxOutputChars: readInteger(browser.max_output_chars, 12_000, "browser.max_output_chars", 1000),
646
535
  readWrite: readBoolean(browser.read_write, false, "browser.read_write"),
647
- allowedSites: readBrowserAllowedSites(browser),
536
+ allowedSites: defaultBrowserAllowedSites(),
648
537
  },
649
538
  agent: {
650
539
  model: agentModel,
@@ -710,6 +599,7 @@ export async function loadConfig(workspacePathInput) {
710
599
  persona: {
711
600
  soul: resolveWorkspacePath(workspacePath, readOptionalString(persona.soul, "SOUL.md")),
712
601
  user: resolveWorkspacePath(workspacePath, readOptionalString(persona.user, "USER.md")),
602
+ contact: resolveWorkspacePath(workspacePath, readOptionalString(persona.contact, "CONTACT.md")),
713
603
  memory: resolveWorkspacePath(workspacePath, readOptionalString(persona.memory, "MEMORY.md")),
714
604
  inner: resolveWorkspacePath(workspacePath, readOptionalString(persona.inner, "INNER.md")),
715
605
  },
@@ -0,0 +1,41 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ let contactNotePath = resolve(process.cwd(), "CONTACT.md");
4
+ let cachedNickname = null;
5
+ function isMissingFile(error) {
6
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
7
+ }
8
+ export function setContactNotePath(path) {
9
+ contactNotePath = path;
10
+ cachedNickname = null;
11
+ }
12
+ export async function loadContactNote() {
13
+ try {
14
+ return await readFile(contactNotePath, "utf8");
15
+ }
16
+ catch (error) {
17
+ if (isMissingFile(error))
18
+ return null;
19
+ throw error;
20
+ }
21
+ }
22
+ export function parseContactNickname(raw, fallback) {
23
+ let remaining = raw?.trim() ?? "";
24
+ while (remaining.startsWith("<!--")) {
25
+ const end = remaining.indexOf("-->");
26
+ if (end === -1)
27
+ return fallback;
28
+ remaining = remaining.slice(end + 3).trim();
29
+ }
30
+ const firstLine = remaining
31
+ .split(/\r?\n/)
32
+ .map((line) => line.trim())
33
+ .find((line) => line && !line.startsWith("<!--"));
34
+ return firstLine ?? fallback;
35
+ }
36
+ export async function refreshContactNote() {
37
+ cachedNickname = parseContactNickname(await loadContactNote(), "");
38
+ }
39
+ export function getContactNickname(fallback) {
40
+ return cachedNickname || fallback;
41
+ }
package/dist/discord.js CHANGED
@@ -625,7 +625,7 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
625
625
  let heartbeatQueued = false;
626
626
  let cronRunning = false;
627
627
  let schedulerState = { cron: {} };
628
- const promptForRuntime = async (runtime, jobId, prompt, attachments = [], onEvent) => {
628
+ const promptForRuntime = async (runtime, jobId, prompt, attachments = [], onEvent, onTurnEnd) => {
629
629
  const run = agentWorkQueue.then(async () => {
630
630
  if (!runtime.hasActiveJob(jobId))
631
631
  throw canceledJobError();
@@ -635,6 +635,7 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
635
635
  const input = [prompt, promptImages.promptSuffix].filter(Boolean).join("\n");
636
636
  const reply = await familiarAgent.prompt(runtime.channelKey, input, promptImages.images, onEvent, {
637
637
  referenceAttachments: attachments,
638
+ onTurnEnd,
638
639
  });
639
640
  if (!runtime.hasActiveJob(jobId))
640
641
  throw canceledJobError();
@@ -1170,12 +1171,21 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
1170
1171
  console.warn("Discord websocket closed; discord.js will reconnect when possible", event);
1171
1172
  });
1172
1173
  schedulerState = await loadSchedulerState(config.workspace.dataDir);
1174
+ const tickHeartbeat = () => {
1175
+ void runHeartbeat().catch((error) => console.error("Heartbeat tick failed", error));
1176
+ };
1177
+ const rearmHeartbeat = () => {
1178
+ if (heartbeatTimer) {
1179
+ clearInterval(heartbeatTimer);
1180
+ heartbeatTimer = undefined;
1181
+ }
1182
+ if (config.heartbeat.enabled) {
1183
+ heartbeatTimer = setInterval(tickHeartbeat, Math.min(config.heartbeat.intervalMs, 60_000));
1184
+ }
1185
+ };
1173
1186
  if (config.heartbeat.enabled) {
1174
1187
  await initializeHeartbeatState((await getOwnerDmSession()).runtime);
1175
- const tickHeartbeat = () => {
1176
- void runHeartbeat().catch((error) => console.error("Heartbeat tick failed", error));
1177
- };
1178
- heartbeatTimer = setInterval(tickHeartbeat, Math.min(config.heartbeat.intervalMs, 60_000));
1188
+ rearmHeartbeat();
1179
1189
  tickHeartbeat();
1180
1190
  }
1181
1191
  if (config.cron.enabled && config.cron.jobs.some((job) => job.enabled)) {
@@ -1191,12 +1201,12 @@ export async function startDiscordDaemon(config, familiarAgent, settings, memory
1191
1201
  getRuntimeForWebChannel,
1192
1202
  runPromptForWeb: promptForRuntime,
1193
1203
  abortWebRuntime(runtime) {
1194
- if (runtime.hasActiveJob() && activeAgentOwner === runtime.channelKey)
1195
- familiarAgent.abort(runtime.channelKey);
1204
+ familiarAgent.requestSoftStop(runtime.channelKey);
1196
1205
  },
1197
1206
  getActiveRuntimeKey() {
1198
1207
  return activeAgentOwner;
1199
1208
  },
1209
+ rearmHeartbeat,
1200
1210
  async stop() {
1201
1211
  client.off(Events.MessageCreate, onMessageCreate);
1202
1212
  client.off(Events.InteractionCreate, onInteractionCreate);
@@ -1,7 +1,17 @@
1
1
  import { watch } from "node:fs";
2
2
  import { readdir } from "node:fs/promises";
3
3
  import { basename, relative, resolve, sep } from "node:path";
4
- const ROOT_FILES = new Set(["config.toml", ".env", "SOUL.md", "USER.md", "MEMORY.md", "INNER.md", "HEARTBEAT.md"]);
4
+ import { refreshContactNote } from "./contact-note.js";
5
+ const ROOT_FILES = new Set([
6
+ "config.toml",
7
+ ".env",
8
+ "SOUL.md",
9
+ "USER.md",
10
+ "CONTACT.md",
11
+ "MEMORY.md",
12
+ "INNER.md",
13
+ "HEARTBEAT.md",
14
+ ]);
5
15
  const SKILLS_DIR = "skills";
6
16
  function isEnoent(error) {
7
17
  return !!error && typeof error === "object" && error.code === "ENOENT";
@@ -94,7 +104,21 @@ export function startWorkspaceHotReload(options) {
94
104
  if (!filename ||
95
105
  shouldReloadForPath(workspacePath, changedPath) ||
96
106
  shouldReloadForPath(workspacePath, dirPath)) {
97
- scheduleReload(relative(workspacePath, changedPath) || relative(workspacePath, dirPath) || ".");
107
+ const reason = relative(workspacePath, changedPath) || relative(workspacePath, dirPath) || ".";
108
+ if (reason === "CONTACT.md") {
109
+ reloadQueue = reloadQueue.then(async () => {
110
+ try {
111
+ await refreshContactNote();
112
+ logger.info(`contact note refresh complete after ${reason}`);
113
+ }
114
+ catch (error) {
115
+ logger.error("contact note refresh failed", error);
116
+ }
117
+ });
118
+ }
119
+ else {
120
+ scheduleReload(reason);
121
+ }
98
122
  }
99
123
  });
100
124
  watcher.on("error", (error) => {
@@ -599,8 +599,13 @@ function lcmRecordPartsFromAgentMessage(message) {
599
599
  return [];
600
600
  const parts = [];
601
601
  for (const item of content) {
602
- if (item.type === "text")
603
- parts.push({ kind: "text", text: item.text });
602
+ if (item.type === "text") {
603
+ parts.push({
604
+ kind: "text",
605
+ text: item.text,
606
+ ...(item.textSignature ? { signature: item.textSignature } : {}),
607
+ });
608
+ }
604
609
  else if (item.type === "thinking") {
605
610
  parts.push({
606
611
  kind: "thinking",
@@ -609,7 +614,13 @@ function lcmRecordPartsFromAgentMessage(message) {
609
614
  });
610
615
  }
611
616
  else if (item.type === "toolCall") {
612
- parts.push({ kind: "tool_call", toolCallId: item.id, toolName: item.name, arguments: item.arguments });
617
+ parts.push({
618
+ kind: "tool_call",
619
+ toolCallId: item.id,
620
+ toolName: item.name,
621
+ arguments: item.arguments,
622
+ ...(item.thoughtSignature ? { signature: item.thoughtSignature } : {}),
623
+ });
613
624
  }
614
625
  else if (item.type === "image") {
615
626
  parts.push({ kind: "text", text: `[image: ${item.mimeType}]` });