larkin 0.5.3 → 0.5.4

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.
@@ -424,13 +424,57 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
424
424
  for (const agent of agents)
425
425
  prepareAgentState(agent);
426
426
  const reminder = new host_reminder_orchestrator_js_1.HostReminderOrchestrator({ agents, stateStore, envelopeProjector, deliveryTarget: runtimeHost, log });
427
+ const auditRegistry = (0, missed_outbound_scan_js_1.inboxAuditRegistryFile)(larkinHome);
428
+ const auditRetryJournal = (0, missed_outbound_scan_js_1.inboxAuditRetryFile)(larkinHome);
429
+ let auditRetryTimer = null;
430
+ let auditRetryAttempt = 0;
431
+ const reconcileAuditRetries = () => {
432
+ try {
433
+ return (0, missed_outbound_scan_js_1.reconcileInboxAuditTargetRetries)(auditRegistry, auditRetryJournal).remaining;
434
+ }
435
+ catch (error) {
436
+ log(`inbox audit retry 读取失败: ${(0, inbox_audit_heartbeat_js_1.boundedInboxAuditDiagnostic)(error)}`);
437
+ return 1;
438
+ }
439
+ };
440
+ const scheduleAuditRetry = () => {
441
+ if (auditRetryTimer || auditRetryAttempt >= 5)
442
+ return;
443
+ const delay = [100, 250, 1_000, 3_000, 10_000][auditRetryAttempt++];
444
+ auditRetryTimer = setTimeout(() => {
445
+ auditRetryTimer = null;
446
+ if (reconcileAuditRetries() > 0)
447
+ scheduleAuditRetry();
448
+ else
449
+ auditRetryAttempt = 0;
450
+ }, delay);
451
+ auditRetryTimer.unref?.();
452
+ };
427
453
  const inboxAudit = new inbox_audit_heartbeat_js_1.InboxAuditHeartbeat({
428
454
  agents,
429
455
  stateStore,
430
456
  runtimeHost,
431
457
  log,
458
+ configFile: node_path_1.default.join(larkinHome, "config.json"),
459
+ schedule(agent) {
460
+ try {
461
+ return (0, config_js_1.resolveInboxAuditSchedule)((0, config_js_1.loadConfig)(env).config, agent.agentId);
462
+ }
463
+ catch (error) {
464
+ log(`inbox audit schedule 读取失败 agent=${agent.agentId}: ${errorMessage(error)}`);
465
+ return { enabled: false, intervalMs: inbox_audit_heartbeat_js_1.INBOX_AUDIT_CADENCE_MS };
466
+ }
467
+ },
468
+ shouldDispatch(agent) {
469
+ try {
470
+ return (0, missed_outbound_scan_js_1.hasPendingInboxAuditTargets)(auditRegistry, agent.agentId);
471
+ }
472
+ catch (error) {
473
+ log(`inbox audit pending 读取失败 agent=${agent.agentId}: ${errorMessage(error)}`);
474
+ return false;
475
+ }
476
+ },
432
477
  });
433
- const auditRegistry = (0, missed_outbound_scan_js_1.inboxAuditRegistryFile)(larkinHome);
434
478
  const seenEventIds = new Set();
435
479
  const inFlightEventIds = new Set();
436
480
  const onFeishuMessage = async (agent, event, options) => {
@@ -477,14 +521,23 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
477
521
  // append/dedupe decision is durable. Agent model-seen state is untouched.
478
522
  if (event.event_id)
479
523
  seenEventIds.add(eventKey);
524
+ if (append.status === "duplicate_consumed")
525
+ return null;
526
+ const auditSourceSeq = Number(append.envelope.target_seq);
480
527
  try {
481
- (0, missed_outbound_scan_js_1.observeInboxAuditTarget)(auditRegistry, agent.agentId, event);
528
+ (0, missed_outbound_scan_js_1.observeInboxAuditTarget)(auditRegistry, agent.agentId, { ...event, wake, source_seq: auditSourceSeq });
529
+ (0, missed_outbound_scan_js_1.discardInboxAuditTargetRetry)(auditRetryJournal, agent.agentId, { ...event, source_seq: auditSourceSeq });
482
530
  }
483
531
  catch (error) {
484
- log(`inbox audit target 未持久化: ${error.message}`);
532
+ try {
533
+ (0, missed_outbound_scan_js_1.enqueueInboxAuditTargetRetry)(auditRetryJournal, agent.agentId, { ...event, wake, source_seq: auditSourceSeq });
534
+ scheduleAuditRetry();
535
+ log(`inbox audit target 延后重试: ${(0, inbox_audit_heartbeat_js_1.boundedInboxAuditDiagnostic)(error)}`);
536
+ }
537
+ catch (retryError) {
538
+ log(`inbox audit target 未持久化: ${(0, inbox_audit_heartbeat_js_1.boundedInboxAuditDiagnostic)(retryError)}`);
539
+ }
485
540
  }
486
- if (append.status === "duplicate_consumed")
487
- return null;
488
541
  const inboxEnvelope = append.envelope;
489
542
  if (append.status === "appended")
490
543
  hostState.appendConversation(agent, {
@@ -1452,6 +1505,9 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1452
1505
  eventSourceStartTimer = null;
1453
1506
  await Promise.resolve(eventSourceStop());
1454
1507
  reminder.stopSync();
1508
+ if (auditRetryTimer)
1509
+ clearTimeout(auditRetryTimer);
1510
+ auditRetryTimer = null;
1455
1511
  inboxAudit.stop();
1456
1512
  interaction.stopSync();
1457
1513
  await runtimeHost.shutdown(reason);
@@ -1670,6 +1726,8 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
1670
1726
  previousSession: agentStates.get(agent.agentId)?.state.previousSessions?.[agent.runtime] ?? null,
1671
1727
  })));
1672
1728
  reminder.startSync();
1729
+ if (reconcileAuditRetries() > 0)
1730
+ scheduleAuditRetry();
1673
1731
  inboxAudit.start();
1674
1732
  interaction.startSync();
1675
1733
  eventSourceStartTimer = setTimeout(() => { eventSourceStartTimer = null; startEventSource(); }, eventSourceStartDelayMs);
@@ -11,15 +11,15 @@ import { HostEnvelopeProjector, HostStateProjection, SenderIdentityCache, projec
11
11
  import { ProcessingEyeOrchestrator } from "./host-processing-eye.mjs";
12
12
  import { projectInboxEnvelope, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
13
13
  import { HostReminderOrchestrator } from "../agent/host-reminder-orchestrator.mjs";
14
- import { InboxAuditHeartbeat } from "../agent/inbox-audit-heartbeat.mjs";
15
- import { inboxAuditRegistryFile, observeInboxAuditTarget } from "../agent/missed-outbound-scan.mjs";
14
+ import { boundedInboxAuditDiagnostic, InboxAuditHeartbeat, INBOX_AUDIT_CADENCE_MS } from "../agent/inbox-audit-heartbeat.mjs";
15
+ import { discardInboxAuditTargetRetry, enqueueInboxAuditTargetRetry, hasPendingInboxAuditTargets, inboxAuditRegistryFile, inboxAuditRetryFile, observeInboxAuditTarget, reconcileInboxAuditTargetRetries } from "../agent/missed-outbound-scan.mjs";
16
16
  import { HostChannelBusiness } from "./host-channel-business.mjs";
17
17
  import { HostInteractionOrchestrator } from "./interaction-orchestrator.mjs";
18
18
  import { targetFor } from "./message-policy.mjs";
19
19
  import { authFailureAppliesTo, parsePersistedAuthFailure, providerAuthenticationFailureReadiness, readinessForPersistedAuthFailure, RuntimePrerequisiteError, } from "../runtime/runtime-readiness.mjs";
20
20
  import { safeProviderDiagnostic } from "../runtime/provider-error-classifier.mjs";
21
21
  import { readDocumentCommentSubscription, verifyCallbackProbe } from "../platform/callback-capability.mjs";
22
- import { loadConfig, resolveMentionPolicy } from "../platform/config.mjs";
22
+ import { loadConfig, resolveInboxAuditSchedule, resolveMentionPolicy } from "../platform/config.mjs";
23
23
  import { processCommandToken } from "../app/internal-command.mjs";
24
24
  import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
25
25
  import { isChannelReconnecting, isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
@@ -384,13 +384,57 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
384
384
  for (const agent of agents)
385
385
  prepareAgentState(agent);
386
386
  const reminder = new HostReminderOrchestrator({ agents, stateStore, envelopeProjector, deliveryTarget: runtimeHost, log });
387
+ const auditRegistry = inboxAuditRegistryFile(larkinHome);
388
+ const auditRetryJournal = inboxAuditRetryFile(larkinHome);
389
+ let auditRetryTimer = null;
390
+ let auditRetryAttempt = 0;
391
+ const reconcileAuditRetries = () => {
392
+ try {
393
+ return reconcileInboxAuditTargetRetries(auditRegistry, auditRetryJournal).remaining;
394
+ }
395
+ catch (error) {
396
+ log(`inbox audit retry 读取失败: ${boundedInboxAuditDiagnostic(error)}`);
397
+ return 1;
398
+ }
399
+ };
400
+ const scheduleAuditRetry = () => {
401
+ if (auditRetryTimer || auditRetryAttempt >= 5)
402
+ return;
403
+ const delay = [100, 250, 1_000, 3_000, 10_000][auditRetryAttempt++];
404
+ auditRetryTimer = setTimeout(() => {
405
+ auditRetryTimer = null;
406
+ if (reconcileAuditRetries() > 0)
407
+ scheduleAuditRetry();
408
+ else
409
+ auditRetryAttempt = 0;
410
+ }, delay);
411
+ auditRetryTimer.unref?.();
412
+ };
387
413
  const inboxAudit = new InboxAuditHeartbeat({
388
414
  agents,
389
415
  stateStore,
390
416
  runtimeHost,
391
417
  log,
418
+ configFile: path.join(larkinHome, "config.json"),
419
+ schedule(agent) {
420
+ try {
421
+ return resolveInboxAuditSchedule(loadConfig(env).config, agent.agentId);
422
+ }
423
+ catch (error) {
424
+ log(`inbox audit schedule 读取失败 agent=${agent.agentId}: ${errorMessage(error)}`);
425
+ return { enabled: false, intervalMs: INBOX_AUDIT_CADENCE_MS };
426
+ }
427
+ },
428
+ shouldDispatch(agent) {
429
+ try {
430
+ return hasPendingInboxAuditTargets(auditRegistry, agent.agentId);
431
+ }
432
+ catch (error) {
433
+ log(`inbox audit pending 读取失败 agent=${agent.agentId}: ${errorMessage(error)}`);
434
+ return false;
435
+ }
436
+ },
392
437
  });
393
- const auditRegistry = inboxAuditRegistryFile(larkinHome);
394
438
  const seenEventIds = new Set();
395
439
  const inFlightEventIds = new Set();
396
440
  const onFeishuMessage = async (agent, event, options) => {
@@ -437,14 +481,23 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
437
481
  // append/dedupe decision is durable. Agent model-seen state is untouched.
438
482
  if (event.event_id)
439
483
  seenEventIds.add(eventKey);
484
+ if (append.status === "duplicate_consumed")
485
+ return null;
486
+ const auditSourceSeq = Number(append.envelope.target_seq);
440
487
  try {
441
- observeInboxAuditTarget(auditRegistry, agent.agentId, event);
488
+ observeInboxAuditTarget(auditRegistry, agent.agentId, { ...event, wake, source_seq: auditSourceSeq });
489
+ discardInboxAuditTargetRetry(auditRetryJournal, agent.agentId, { ...event, source_seq: auditSourceSeq });
442
490
  }
443
491
  catch (error) {
444
- log(`inbox audit target 未持久化: ${error.message}`);
492
+ try {
493
+ enqueueInboxAuditTargetRetry(auditRetryJournal, agent.agentId, { ...event, wake, source_seq: auditSourceSeq });
494
+ scheduleAuditRetry();
495
+ log(`inbox audit target 延后重试: ${boundedInboxAuditDiagnostic(error)}`);
496
+ }
497
+ catch (retryError) {
498
+ log(`inbox audit target 未持久化: ${boundedInboxAuditDiagnostic(retryError)}`);
499
+ }
445
500
  }
446
- if (append.status === "duplicate_consumed")
447
- return null;
448
501
  const inboxEnvelope = append.envelope;
449
502
  if (append.status === "appended")
450
503
  hostState.appendConversation(agent, {
@@ -1412,6 +1465,9 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1412
1465
  eventSourceStartTimer = null;
1413
1466
  await Promise.resolve(eventSourceStop());
1414
1467
  reminder.stopSync();
1468
+ if (auditRetryTimer)
1469
+ clearTimeout(auditRetryTimer);
1470
+ auditRetryTimer = null;
1415
1471
  inboxAudit.stop();
1416
1472
  interaction.stopSync();
1417
1473
  await runtimeHost.shutdown(reason);
@@ -1630,6 +1686,8 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
1630
1686
  previousSession: agentStates.get(agent.agentId)?.state.previousSessions?.[agent.runtime] ?? null,
1631
1687
  })));
1632
1688
  reminder.startSync();
1689
+ if (reconcileAuditRetries() > 0)
1690
+ scheduleAuditRetry();
1633
1691
  inboxAudit.start();
1634
1692
  interaction.startSync();
1635
1693
  eventSourceStartTimer = setTimeout(() => { eventSourceStartTimer = null; startEventSource(); }, eventSourceStartDelayMs);
@@ -36,12 +36,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.toUserRuntime = exports.runtimeOptionTarget = exports.runtimeOptionOf = exports.isUserRuntime = exports.isAdapterRuntime = exports.fromUserRuntime = exports.RUNTIME_OPTIONS = void 0;
39
+ exports.MAX_INBOX_AUDIT_INTERVAL_MS = exports.MIN_INBOX_AUDIT_INTERVAL_MS = exports.DEFAULT_INBOX_AUDIT_INTERVAL_MS = exports.toUserRuntime = exports.runtimeOptionTarget = exports.runtimeOptionOf = exports.isUserRuntime = exports.isAdapterRuntime = exports.fromUserRuntime = exports.RUNTIME_OPTIONS = void 0;
40
40
  exports.loadRuntimeModels = loadRuntimeModels;
41
41
  exports.defaultModelFor = defaultModelFor;
42
42
  exports.resolveConfigDir = resolveConfigDir;
43
43
  exports.resolveLarkConfigDir = resolveLarkConfigDir;
44
44
  exports.resolveRuntimeAuthority = resolveRuntimeAuthority;
45
+ exports.assertInboxAuditInterval = assertInboxAuditInterval;
46
+ exports.parseInboxAuditInterval = parseInboxAuditInterval;
47
+ exports.formatInboxAuditInterval = formatInboxAuditInterval;
48
+ exports.inboxAuditMutationFromCli = inboxAuditMutationFromCli;
45
49
  exports.hydrateAgent = hydrateAgent;
46
50
  exports.normalizeConfig = normalizeConfig;
47
51
  exports.assertPrivateConfigMetadata = assertPrivateConfigMetadata;
@@ -50,6 +54,7 @@ exports.selectAgent = selectAgent;
50
54
  exports.toStored = toStored;
51
55
  exports.resolveMentionPolicy = resolveMentionPolicy;
52
56
  exports.resolveAgentGlobalMentionPolicy = resolveAgentGlobalMentionPolicy;
57
+ exports.resolveInboxAuditSchedule = resolveInboxAuditSchedule;
53
58
  exports.runtimeConfigSignature = runtimeConfigSignature;
54
59
  exports.configApplyState = configApplyState;
55
60
  exports.markConfigApplied = markConfigApplied;
@@ -76,11 +81,16 @@ Object.defineProperty(exports, "isUserRuntime", { enumerable: true, get: functio
76
81
  Object.defineProperty(exports, "runtimeOptionOf", { enumerable: true, get: function () { return user_runtime_js_2.runtimeOptionOf; } });
77
82
  Object.defineProperty(exports, "runtimeOptionTarget", { enumerable: true, get: function () { return user_runtime_js_2.runtimeOptionTarget; } });
78
83
  Object.defineProperty(exports, "toUserRuntime", { enumerable: true, get: function () { return user_runtime_js_2.toUserRuntime; } });
84
+ exports.DEFAULT_INBOX_AUDIT_INTERVAL_MS = 15 * 60_000;
85
+ exports.MIN_INBOX_AUDIT_INTERVAL_MS = 60_000;
86
+ exports.MAX_INBOX_AUDIT_INTERVAL_MS = 24 * 60 * 60_000;
79
87
  const TOP_FIELDS_V3 = new Set(["version", "serverId", "activeAgent", "agents"]);
80
88
  const TOP_FIELDS_V4 = new Set(["version", "serverId", "mentionPolicy", "activeAgent", "agents"]);
89
+ const OPTIONAL_TOP_FIELDS_V4 = new Set(["inboxAudit"]);
81
90
  const AGENT_FIELDS_V3 = new Set(["runtime", "model", "effort", "noMentionChats", "createdAt"]);
82
- const AGENT_FIELDS_V4 = new Set(["runtime", "model", "effort", "mentionPolicy", "chatMentionPolicies", "createdAt"]);
91
+ const AGENT_FIELDS_V4 = new Set(["runtime", "model", "effort", "mentionPolicy", "chatMentionPolicies", "inboxAudit", "createdAt"]);
83
92
  const LEGACY_AGENT_FIELDS = new Set(["piDistribution", "runtimeOption"]);
93
+ const INBOX_AUDIT_FIELDS = new Set(["enabled", "intervalMs"]);
84
94
  const APP_ID = /^cli_[A-Za-z0-9]+$/;
85
95
  const CHAT_ID = /^oc_[A-Za-z0-9_-]+$/;
86
96
  const PI_EFFORTS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
@@ -144,6 +154,85 @@ function assertPolicy(value, label) {
144
154
  if (value !== "require" && value !== "free")
145
155
  throw new Error(`${label} 只允许 require/free`);
146
156
  }
157
+ function assertInboxAuditInterval(value, label = "inbox-audit interval") {
158
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < exports.MIN_INBOX_AUDIT_INTERVAL_MS || value > exports.MAX_INBOX_AUDIT_INTERVAL_MS) {
159
+ throw new Error(`${label} 必须是 ${exports.MIN_INBOX_AUDIT_INTERVAL_MS}–${exports.MAX_INBOX_AUDIT_INTERVAL_MS} 毫秒`);
160
+ }
161
+ }
162
+ function parseInboxAuditInterval(value) {
163
+ if (typeof value !== "string" || !value)
164
+ throw new Error("inbox-audit interval 不能为空");
165
+ let milliseconds;
166
+ if (/^[1-9]\d*$/.test(value))
167
+ milliseconds = Number(value);
168
+ else {
169
+ const match = /^([1-9]\d*)(m|h)$/.exec(value);
170
+ if (!match)
171
+ throw new Error("inbox-audit interval 只允许 15m/1h 或 60000–86400000 毫秒");
172
+ milliseconds = Number(match[1]) * (match[2] === "h" ? 3_600_000 : 60_000);
173
+ }
174
+ assertInboxAuditInterval(milliseconds);
175
+ return milliseconds;
176
+ }
177
+ function formatInboxAuditInterval(intervalMs) {
178
+ if (intervalMs % 3_600_000 === 0)
179
+ return `${intervalMs / 3_600_000}h`;
180
+ if (intervalMs % 60_000 === 0)
181
+ return `${intervalMs / 60_000}m`;
182
+ return `${intervalMs}ms`;
183
+ }
184
+ function parseStoredInboxAudit(value, label) {
185
+ if (!isPlainObject(value))
186
+ throw new Error(`${label} 必须是普通 object`);
187
+ assertAllowedFields(value, INBOX_AUDIT_FIELDS, label);
188
+ const settings = {};
189
+ if (Object.hasOwn(value, "enabled")) {
190
+ if (typeof value.enabled !== "boolean")
191
+ throw new Error(`${label}.enabled 必须是 boolean`);
192
+ settings.enabled = value.enabled;
193
+ }
194
+ if (Object.hasOwn(value, "intervalMs")) {
195
+ assertInboxAuditInterval(value.intervalMs, `${label}.intervalMs`);
196
+ settings.intervalMs = value.intervalMs;
197
+ }
198
+ return settings;
199
+ }
200
+ function resolvedInboxAudit(settings) {
201
+ return {
202
+ enabled: settings?.enabled ?? false,
203
+ intervalMs: settings?.intervalMs ?? exports.DEFAULT_INBOX_AUDIT_INTERVAL_MS,
204
+ };
205
+ }
206
+ function inboxAuditMutationFromCli(input) {
207
+ if (input.scope === "global") {
208
+ if (input.enabled !== "on" && input.enabled !== "off") {
209
+ throw new Error("用法: larkin config inbox-audit global <on|off> [--interval <15m|1h>]");
210
+ }
211
+ const mutation = {
212
+ kind: "set-global-inbox-audit", enabled: input.enabled === "on",
213
+ };
214
+ if (input.interval !== undefined) {
215
+ if (input.interval === "inherit")
216
+ throw new Error("全局 inbox-audit 不支持 interval=inherit");
217
+ mutation.intervalMs = parseInboxAuditInterval(input.interval);
218
+ }
219
+ return mutation;
220
+ }
221
+ if (input.scope === "agent") {
222
+ if (input.enabled !== "on" && input.enabled !== "off" && input.enabled !== "inherit") {
223
+ throw new Error("用法: larkin config inbox-audit agent <inherit|on|off> [--agent <App ID>] [--interval <15m|inherit>]");
224
+ }
225
+ const mutation = {
226
+ kind: "set-agent-inbox-audit", agentId: input.agentId,
227
+ enabled: input.enabled === "inherit" ? "inherit" : input.enabled === "on",
228
+ };
229
+ if (input.interval !== undefined) {
230
+ mutation.intervalMs = input.interval === "inherit" ? "inherit" : parseInboxAuditInterval(input.interval);
231
+ }
232
+ return mutation;
233
+ }
234
+ throw new Error("inbox-audit scope 只支持 global/agent");
235
+ }
147
236
  function assertModel(runtime, model) {
148
237
  if (model === "default")
149
238
  return;
@@ -211,6 +300,8 @@ function validateStoredAgent(key, agent, version) {
211
300
  }
212
301
  if (version === 4 && Object.hasOwn(agent, "mentionPolicy"))
213
302
  assertPolicy(agent.mentionPolicy, `Agent ${key}.mentionPolicy`);
303
+ if (version === 4 && Object.hasOwn(agent, "inboxAudit"))
304
+ parseStoredInboxAudit(agent.inboxAudit, `Agent ${key}.inboxAudit`);
214
305
  if (version === 4 && Object.hasOwn(agent, "chatMentionPolicies")) {
215
306
  if (!isPlainObject(agent.chatMentionPolicies))
216
307
  throw new Error(`Agent ${key}.chatMentionPolicies 必须是普通 object`);
@@ -239,6 +330,7 @@ function hydratedStoredAgent(key, agent, version) {
239
330
  model: agent.model,
240
331
  ...(typeof agent.effort === "string" ? { effort: agent.effort } : {}),
241
332
  ...(version === 4 && (agent.mentionPolicy === "require" || agent.mentionPolicy === "free") ? { mentionPolicy: agent.mentionPolicy } : {}),
333
+ ...(version === 4 && Object.hasOwn(agent, "inboxAudit") ? { inboxAudit: parseStoredInboxAudit(agent.inboxAudit, `Agent ${key}.inboxAudit`) } : {}),
242
334
  ...(Object.keys(chatMentionPolicies).length ? { chatMentionPolicies, noMentionChats: Object.entries(chatMentionPolicies).filter(([, policy]) => policy === "free").map(([chatId]) => chatId) } : {}),
243
335
  ...(typeof agent.createdAt === "string" ? { createdAt: agent.createdAt } : {}),
244
336
  };
@@ -252,6 +344,7 @@ function hydrateAgent(key, agent, configDir) {
252
344
  larkConfigDir: path.join(layout.agentStateDir(key), "lark-cli-config"),
253
345
  ...(agent.effort ? { effort: agent.effort } : {}),
254
346
  ...(agent.mentionPolicy ? { mentionPolicy: agent.mentionPolicy } : {}),
347
+ ...(agent.inboxAudit ? { inboxAudit: { ...agent.inboxAudit } } : {}),
255
348
  ...(agent.chatMentionPolicies ? { chatMentionPolicies: { ...agent.chatMentionPolicies } } : {}),
256
349
  ...(agent.noMentionChats ? { noMentionChats: [...agent.noMentionChats] } : {}),
257
350
  ...(agent.createdAt ? { createdAt: agent.createdAt } : {}),
@@ -270,7 +363,7 @@ function normalizeConfig(raw, configDir, { mint } = {}) {
270
363
  if (version !== 3 && version !== 4)
271
364
  throw new Error("不支持该配置格式:larkin 只接受 version=3/4");
272
365
  const topFields = version === 3 ? TOP_FIELDS_V3 : TOP_FIELDS_V4;
273
- assertAllowedFields(raw, topFields, "config");
366
+ assertAllowedFields(raw, version === 4 ? new Set([...TOP_FIELDS_V4, ...OPTIONAL_TOP_FIELDS_V4]) : topFields, "config");
274
367
  for (const field of topFields)
275
368
  if (!Object.hasOwn(raw, field))
276
369
  throw new Error(`config 缺少必需字段 ${field}`);
@@ -289,8 +382,12 @@ function normalizeConfig(raw, configDir, { mint } = {}) {
289
382
  const agents = {};
290
383
  for (const [key, agent] of Object.entries(raw.agents))
291
384
  agents[key] = hydrateAgent(key, hydratedStoredAgent(key, agent, version), layout.root);
385
+ const inboxAudit = version === 4 && Object.hasOwn(raw, "inboxAudit")
386
+ ? resolvedInboxAudit(parseStoredInboxAudit(raw.inboxAudit, "config.inboxAudit"))
387
+ : undefined;
292
388
  return {
293
389
  version: 4, serverId: raw.serverId, mentionPolicy: version === 4 ? raw.mentionPolicy : "require",
390
+ ...(inboxAudit ? { inboxAudit } : {}),
294
391
  configDir: layout.configDir, larkinHome: layout.larkinHome, larkConfigDir: resolveLarkConfigDir({}, layout.root), activeAgent: raw.activeAgent, agents,
295
392
  };
296
393
  }
@@ -516,7 +613,11 @@ function selectAgent(config, env = process.env) {
516
613
  return agents[config.activeAgent];
517
614
  }
518
615
  function toStored(config) {
519
- const out = { version: 4, serverId: config.serverId, mentionPolicy: config.mentionPolicy, activeAgent: config.activeAgent, agents: {} };
616
+ const out = {
617
+ version: 4, serverId: config.serverId, mentionPolicy: config.mentionPolicy, activeAgent: config.activeAgent, agents: {},
618
+ };
619
+ if (config.inboxAudit)
620
+ out.inboxAudit = { enabled: config.inboxAudit.enabled, intervalMs: config.inboxAudit.intervalMs };
520
621
  for (const [key, agent] of Object.entries(config.agents || {})) {
521
622
  const stored = { runtime: agent.runtime, model: agent.model };
522
623
  if (typeof agent.effort === "string" && agent.effort)
@@ -525,6 +626,9 @@ function toStored(config) {
525
626
  stored.mentionPolicy = agent.mentionPolicy;
526
627
  if (agent.chatMentionPolicies && Object.keys(agent.chatMentionPolicies).length)
527
628
  stored.chatMentionPolicies = { ...agent.chatMentionPolicies };
629
+ if (agent.inboxAudit && (agent.inboxAudit.enabled !== undefined || agent.inboxAudit.intervalMs !== undefined)) {
630
+ stored.inboxAudit = { ...agent.inboxAudit };
631
+ }
528
632
  if (typeof agent.createdAt === "string" && agent.createdAt)
529
633
  stored.createdAt = agent.createdAt;
530
634
  out.agents[key] = stored;
@@ -551,6 +655,20 @@ function resolveAgentGlobalMentionPolicy(config, agentId) {
551
655
  return { effective: agent.mentionPolicy, source: "agent" };
552
656
  return { effective: config.mentionPolicy, source: "global" };
553
657
  }
658
+ function resolveInboxAuditSchedule(config, agentId) {
659
+ const agent = config.agents[agentId];
660
+ if (!agent)
661
+ throw new Error(`Agent 不存在:${agentId}`);
662
+ const enabled = agent.inboxAudit?.enabled ?? config.inboxAudit?.enabled ?? false;
663
+ const intervalMs = agent.inboxAudit?.intervalMs ?? config.inboxAudit?.intervalMs ?? exports.DEFAULT_INBOX_AUDIT_INTERVAL_MS;
664
+ assertInboxAuditInterval(intervalMs);
665
+ return {
666
+ enabled,
667
+ intervalMs,
668
+ enabledSource: agent.inboxAudit?.enabled !== undefined ? "agent" : config.inboxAudit ? "global" : "default",
669
+ intervalSource: agent.inboxAudit?.intervalMs !== undefined ? "agent" : config.inboxAudit?.intervalMs !== undefined ? "global" : "default",
670
+ };
671
+ }
554
672
  function revision(bytes) {
555
673
  return `sha256:${crypto.createHash("sha256").update(bytes).digest("hex")}`;
556
674
  }
@@ -563,6 +681,7 @@ function agentSignature(config, agentId) {
563
681
  schema: 2,
564
682
  runtime: agent.runtime, model: agent.model, effort: agent.effort ?? null,
565
683
  globalMentionPolicy: config.mentionPolicy, agentMentionPolicy: agent.mentionPolicy ?? null, chatMentionPolicies: chats,
684
+ globalInboxAudit: config.inboxAudit ?? null, agentInboxAudit: agent.inboxAudit ?? null,
566
685
  }));
567
686
  }
568
687
  function runtimeConfigSignature(config, agentId) {
@@ -700,6 +819,19 @@ function applyMutation(config, mutation) {
700
819
  config.mentionPolicy = mutation.value;
701
820
  return { scope: "global", runtimeChange: false, affectedAgentIds: Object.keys(config.agents) };
702
821
  }
822
+ if (mutation.kind === "set-global-inbox-audit") {
823
+ if (mutation.enabled === undefined && mutation.intervalMs === undefined)
824
+ throw new Error("inbox-audit 全局变更必须包含 enabled 或 interval");
825
+ const current = resolvedInboxAudit(config.inboxAudit);
826
+ if (mutation.enabled !== undefined)
827
+ current.enabled = mutation.enabled;
828
+ if (mutation.intervalMs !== undefined) {
829
+ assertInboxAuditInterval(mutation.intervalMs, "全局 inbox-audit interval");
830
+ current.intervalMs = mutation.intervalMs;
831
+ }
832
+ config.inboxAudit = current;
833
+ return { scope: "global", runtimeChange: false, affectedAgentIds: Object.keys(config.agents) };
834
+ }
703
835
  if (!APP_ID.test(mutation.agentId) || !config.agents[mutation.agentId])
704
836
  throw new Error(`Agent 不存在:${mutation.agentId}`);
705
837
  const agent = config.agents[mutation.agentId];
@@ -712,6 +844,26 @@ function applyMutation(config, mutation) {
712
844
  }
713
845
  return { scope: "agent", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
714
846
  }
847
+ if (mutation.kind === "set-agent-inbox-audit") {
848
+ if (mutation.enabled === undefined && mutation.intervalMs === undefined)
849
+ throw new Error("inbox-audit Agent 变更必须包含 enabled 或 interval");
850
+ const current = { ...(agent.inboxAudit || {}) };
851
+ if (mutation.enabled === "inherit")
852
+ delete current.enabled;
853
+ else if (mutation.enabled !== undefined)
854
+ current.enabled = mutation.enabled;
855
+ if (mutation.intervalMs === "inherit")
856
+ delete current.intervalMs;
857
+ else if (mutation.intervalMs !== undefined) {
858
+ assertInboxAuditInterval(mutation.intervalMs, "Agent inbox-audit interval");
859
+ current.intervalMs = mutation.intervalMs;
860
+ }
861
+ if (current.enabled === undefined && current.intervalMs === undefined)
862
+ delete agent.inboxAudit;
863
+ else
864
+ agent.inboxAudit = current;
865
+ return { scope: "agent", agentId: mutation.agentId, runtimeChange: false, affectedAgentIds: [mutation.agentId] };
866
+ }
715
867
  if (mutation.kind === "set-chat-mention") {
716
868
  if (!CHAT_ID.test(mutation.chatId))
717
869
  throw new Error(`群 ID 格式无效:${mutation.chatId}`);
@@ -1198,8 +1350,24 @@ function safeConfigView(config, onlyAgentId, chatId, applyState) {
1198
1350
  source: agent.mentionPolicy ? "agent" : "global",
1199
1351
  ...(chatId ? { chat: { chatId, override: agent.chatMentionPolicies?.[chatId] ?? "inherit", ...resolveMentionPolicy(config, agentId, chatId) } } : {}),
1200
1352
  },
1353
+ inboxAudit: (() => {
1354
+ const resolved = resolveInboxAuditSchedule(config, agentId);
1355
+ return {
1356
+ override: {
1357
+ enabled: agent.inboxAudit?.enabled === undefined ? "inherit" : agent.inboxAudit.enabled ? "on" : "off",
1358
+ intervalMs: agent.inboxAudit?.intervalMs ?? "inherit",
1359
+ },
1360
+ effective: { enabled: resolved.enabled, intervalMs: resolved.intervalMs },
1361
+ source: { enabled: resolved.enabledSource, intervalMs: resolved.intervalSource },
1362
+ };
1363
+ })(),
1201
1364
  chatMentionPolicies: { ...(agent.chatMentionPolicies || {}) },
1202
1365
  apply: applyState?.agents?.[agentId] ?? { applyState: "unknown" },
1203
1366
  }));
1204
- return { version: 4, mentionPolicy: config.mentionPolicy, persistedRevision: applyState?.persistedRevision ?? "unknown", agents };
1367
+ const inboxAudit = resolvedInboxAudit(config.inboxAudit);
1368
+ return {
1369
+ version: 4, mentionPolicy: config.mentionPolicy,
1370
+ inboxAudit: { enabled: inboxAudit.enabled, intervalMs: inboxAudit.intervalMs },
1371
+ persistedRevision: applyState?.persistedRevision ?? "unknown", agents,
1372
+ };
1205
1373
  }