@epoch-agent/core 0.15.0 → 0.16.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/dist/index.d.ts CHANGED
@@ -11791,6 +11791,14 @@ declare const EpochConfigSchema: z.ZodObject<{
11791
11791
  label: z.ZodString;
11792
11792
  icon: z.ZodOptional<z.ZodString>;
11793
11793
  }, z.core.$strip>>;
11794
+ settingsHidden: z.ZodOptional<z.ZodArray<z.ZodString>>;
11795
+ settingsPanes: z.ZodOptional<z.ZodArray<z.ZodObject<{
11796
+ id: z.ZodString;
11797
+ label: z.ZodString;
11798
+ icon: z.ZodOptional<z.ZodString>;
11799
+ url: z.ZodString;
11800
+ before: z.ZodOptional<z.ZodString>;
11801
+ }, z.core.$strip>>>;
11794
11802
  }, z.core.$strip>;
11795
11803
  /**
11796
11804
  * 全默认配置 —— 从 schema 派生,不是第二份字面量。
@@ -16964,8 +16972,8 @@ type BackendOutcome = {
16964
16972
  /**
16965
16973
  * OS 侧那条任务此刻的样子。`null` = 那儿没有这条任务。
16966
16974
  *
16967
- * 两个字段都是**给 `doctor` 比对用的字符串**,不是结构 —— 两个平台能拿到的
16968
- * 东西形状完全不同(Windows 是 `schtasks /Query` 的一行文本,macOS 是我们
16975
+ * `commandLine` / `spec` 都是**给 `doctor` 比对用的字符串**,不是结构 —— 两个平台
16976
+ * 能拿到的东西形状完全不同(Windows 是 `schtasks /Query` 的一行文本,macOS 是我们
16969
16977
  * 自己写的那份 plist),硬拗成同一个结构只会让两边各塞一半空字段。
16970
16978
  * 比对的另一半由 `expectedSnapshot()` 用**同一套算法**从库里那行算出来。
16971
16979
  */
@@ -16973,6 +16981,20 @@ interface OsTaskSnapshot {
16973
16981
  osTaskId: string;
16974
16982
  /** 那条命令行 */
16975
16983
  commandLine: string;
16984
+ /**
16985
+ * 同一条命令行**切成段**。
16986
+ *
16987
+ * ⚠️ **它不是 `commandLine` 的便利副本,是一件 `commandLine` 答不了的事。**
16988
+ * `doctor` 判一条孤儿归不归自己管,靠的是命令行里 `--home` 后面那一段
16989
+ * (规矩见 [argv.ts](./argv.ts) 第 2 条:数据目录固化进命令行)。而从拼好的
16990
+ * 那一串里把它切回来,两个平台都不可靠:launchd 那侧 `commandLine` 是
16991
+ * `argv.join(' ')`,home 路径带空格就切不开;Windows 那侧每段带引号,
16992
+ * 按空白切又会把 `"C:\Program Files\..."` 劈成两半。
16993
+ *
16994
+ * 所以由**后端**交出来 —— 它手上有没拼之前的那份:launchd 本来就是数组,
16995
+ * schtasks 用 `taskRunArgv()` 做 `quoteTaskRun()` 的逆。
16996
+ */
16997
+ argv: string[];
16976
16998
  /** 触发规格 */
16977
16999
  spec: string;
16978
17000
  /** 停用了没有 */
@@ -17095,6 +17117,15 @@ type RegisterOutcome = {
17095
17117
  declare class ScheduleRegistrar {
17096
17118
  private readonly opts;
17097
17119
  constructor(opts: RegistrarOptions);
17120
+ /**
17121
+ * 固化进命令行的那个数据目录。
17122
+ *
17123
+ * **公开出来是给 `doctor` 用的**(同下面 `toRegisterInput` 那一处的理由):
17124
+ * 它要判「OS 侧这条没主的任务是不是我们这个 home 的」,而那个比较的另一头
17125
+ * 必须和注册时真的写进命令行的那个值**同源**。让调用方自己再传一个 home 进来
17126
+ * 的话,两者对不上时不会有任何地方报错 —— 只会安静地把别人的任务判成孤儿。
17127
+ */
17128
+ get homeDir(): string;
17098
17129
  /** 这条任务在 OS 侧叫什么。没有后端时是空串 */
17099
17130
  osTaskIdFor(taskId: string): string;
17100
17131
  /**
@@ -17177,6 +17208,11 @@ declare class ScheduleRegistrar {
17177
17208
  * 第三类的判据要写清楚:**DB 是真源。** 反过来(以 OS 为准)意味着我们要把
17178
17209
  * `schtasks` 的时间语法反解析回三档,那是一条只输不赢的路。
17179
17210
  *
17211
+ * ⚠️ **第二类先过一道 home 闸**:OS 侧的枚举是刻意跨 home 的,所以「库里没有」
17212
+ * 不等于「没人认领」—— 同一台机器上另一个数据目录(嵌入宿主 / 另一个
17213
+ * `EPOCH_PROFILE`)的活任务也在同一个前缀底下。判据全文在 {@link ownedByThisHome},
17214
+ * 那儿记着它拦下的那次真实误删。
17215
+ *
17180
17216
  * ⚠️ **第三类在两个平台上都是逐字比的,而 Windows 那侧曾经不是**(2026-08-18
17181
17217
  * 真机那一轮改的)。两头各自归一到同一个串(`DAILY|09:00` / `EVERY|15` /
17182
17218
  * `ONCE|2026-12-31T23:00`…):一头从 `/Query /XML` 算(`specFromXml`),
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { join, dirname, resolve, relative, isAbsolute, sep, extname, basename }
4
4
  import { execFile, execSync, execFileSync, spawn } from 'child_process';
5
5
  import { createLogger, dbPath, resolveHomeDir, DEFAULT_MAX_TURNS, PROJECT_DIR_NAME, guardSpawn, t, suggestKey, misspelledKeyIssues, openDatabase, migrate, releaseDatabase, parseLenient, issueDetails, splitWords, unknownKeyIssues, checkpointsDir, pluginsDir, projectHooksPath, projectClaudeSettingsPath, projectClaudeLocalSettingsPath, projectSettingsPath, projectLocalSettingsPath, projectPoliciesDir, trustedImportsPath, canProbeGit, budgetStatePath, artifactsDir, UNLIMITED_TURNS, turnLimitOf, configPath, managedSettingsPath, getSecretValues, listStoredSecretNames, maskApiKey, setSectionField, countCodePoints, headCodePoints, attachmentsDir, isInWorkspace, workspacesPath, samePath, lastSessionsPath, automationLogsDir, projectComplianceDir, projectSkillsDir, trustPath, projectCommandsDir, detectBackend, EXEC_PATH_AS_NODE_ENV, isolate, createStreamDecoder, parseShellCommand, writeArtifact, envPath, checkDangerousCommand, checkObfuscation, keepHeadAndTail, normalizeForMatch, isReadOnlyCommand, CODE_EXEC_ROOTS, tailCodePoints } from '@epoch-agent/infra';
6
6
  export { IS_WINDOWS, WINDOWS_HIDE_FLAGS, buildBwrapArgs, buildProfile, checkDangerousCommand, checkObfuscation, createLogger, detectBackend, getDefaultShell, getPythonCommand, isInWorkspace, isReadOnlyCommand, isolate, maskApiKey, probeBubblewrap, probeSeatbelt, complianceDir as resolveComplianceDir, policiesDir as resolvePoliciesDir, setLogLevel } from '@epoch-agent/infra';
7
- import { PROVIDER_TYPES, OPERATION_TYPES as OPERATION_TYPES$1, COMPLIANCE_CATEGORIES, COMPLIANCE_ACTIONS, SEARCH_PROVIDER_TYPES, CURRENCIES, LANGS, PERMISSION_LEVELS, SHELL_KINDS, ToolExposure, MAX_OPTIONS, MIN_OPTIONS, MAX_HEADER_CHARS, MAX_QUESTIONS, SCHEDULE_INTERVAL_MINUTES, isPermissionLevel, SCHEDULE_DRAFT_TOOL, isValidAgentRoleName, DEFAULT_AGENT_ROLE, isValidCommandName, RESERVED_COMMAND_NAMES, parseModelRef, isWirePluginArchiveName, systemNote, formatMoney, totalUsageTokens, NOOP_TELEMETRY, METRIC, contentToText, GEN_AI, isPromptOversize, promptTokens, apiKeyEnvVar, PROVIDER_INFOS, isProviderType, API_KEY_ENV_VARS, KEY_ACTIONS, tableOf, perAction, isKeyAction, parseSequence, RESERVED_CHORDS, chordId, sequenceId, ACTION_CONTEXTS, SEQUENCE_CONTEXTS, assertNeverPart, MAX_ATTACH_FILES, MAX_ATTACH_TOTAL_BYTES, MAX_SESSION_REFS, utf8ByteLength, truncateToBudget, attachSessionSurface, isContextNearlyFull, isOperationType, normalizeApproval, imagePartOmittedSummary, MAX_ATTACH_BYTES, SIMULTANEOUS_CONTEXTS, SESSION_REFERENCE_ERROR_CODES, filePartSummary, sessionPartSummary } from '@epoch-agent/protocol';
7
+ import { PROVIDER_TYPES, OPERATION_TYPES as OPERATION_TYPES$1, COMPLIANCE_CATEGORIES, COMPLIANCE_ACTIONS, SEARCH_PROVIDER_TYPES, CURRENCIES, LANGS, isSettingsPaneId, SETTINGS_PANE_IDS, PERMISSION_LEVELS, SHELL_KINDS, ToolExposure, MAX_OPTIONS, MIN_OPTIONS, MAX_HEADER_CHARS, MAX_QUESTIONS, SCHEDULE_INTERVAL_MINUTES, isPermissionLevel, SCHEDULE_DRAFT_TOOL, isValidAgentRoleName, DEFAULT_AGENT_ROLE, isValidCommandName, RESERVED_COMMAND_NAMES, parseModelRef, isWirePluginArchiveName, systemNote, formatMoney, totalUsageTokens, NOOP_TELEMETRY, METRIC, contentToText, GEN_AI, isPromptOversize, promptTokens, apiKeyEnvVar, PROVIDER_INFOS, isProviderType, API_KEY_ENV_VARS, KEY_ACTIONS, tableOf, perAction, isKeyAction, parseSequence, RESERVED_CHORDS, chordId, sequenceId, ACTION_CONTEXTS, SEQUENCE_CONTEXTS, assertNeverPart, MAX_ATTACH_FILES, MAX_ATTACH_TOTAL_BYTES, MAX_SESSION_REFS, utf8ByteLength, truncateToBudget, attachSessionSurface, isContextNearlyFull, isOperationType, normalizeApproval, imagePartOmittedSummary, MAX_ATTACH_BYTES, SIMULTANEOUS_CONTEXTS, SESSION_REFERENCE_ERROR_CODES, filePartSummary, sessionPartSummary } from '@epoch-agent/protocol';
8
8
  export { API_KEY_ENV_VARS, COMPLIANCE_ACTIONS, COMPLIANCE_CATEGORIES, MAX_ATTACH_BYTES, MAX_ATTACH_FILES, MAX_ATTACH_TOTAL_BYTES, MAX_SESSION_ATTACH_BYTES, MAX_SESSION_REFS, OPERATION_TYPES, PERMISSION_LEVELS, PLAN_OUTCOMES, PROVIDER_INFOS, PROVIDER_TYPES, ToolExposure, apiKeyEnvVar, extractAllMentions, extractMentions, extractSessionMentions, getProviderInfo, isOperationType, isPermissionLevel, isPlanOutcome, isProviderType, normalizeApproval } from '@epoch-agent/protocol';
9
9
  import { createRequire } from 'module';
10
10
  import { randomUUID, createHash, randomBytes } from 'crypto';
@@ -34,7 +34,8 @@ __export(schtasks_exports, {
34
34
  quoteTaskRun: () => quoteTaskRun,
35
35
  scheduleArgs: () => scheduleArgs,
36
36
  specFromTrigger: () => specFromTrigger,
37
- specFromXml: () => specFromXml
37
+ specFromXml: () => specFromXml,
38
+ taskRunArgv: () => taskRunArgv
38
39
  });
39
40
  function createSchtasksBackend(opts = {}) {
40
41
  const run = opts.run ?? runCommand2;
@@ -99,9 +100,11 @@ function createSchtasksBackend(opts = {}) {
99
100
  async inspect(osTaskId) {
100
101
  const res = await run("schtasks", ["/Query", "/TN", osTaskId, "/XML"]);
101
102
  if (res.code !== 0) return null;
103
+ const commandLine = commandLineFromXml(res.stdout);
102
104
  return {
103
105
  osTaskId,
104
- commandLine: commandLineFromXml(res.stdout),
106
+ commandLine,
107
+ argv: taskRunArgv(commandLine),
105
108
  spec: specFromXml(res.stdout),
106
109
  enabled: !/<Enabled>\s*false\s*<\/Enabled>/i.test(res.stdout)
107
110
  };
@@ -118,6 +121,13 @@ function createSchtasksBackend(opts = {}) {
118
121
  function quoteTaskRun(argv) {
119
122
  return argv.map((part) => `"${part.replace(/"/g, "")}"`).join(" ");
120
123
  }
124
+ function taskRunArgv(commandLine) {
125
+ const out = [];
126
+ const re = /"([^"]*)"|(\S+)/g;
127
+ let m;
128
+ while ((m = re.exec(commandLine)) !== null) out.push(m[1] ?? m[2] ?? "");
129
+ return out;
130
+ }
121
131
  function scheduleArgs(trigger) {
122
132
  if (trigger.kind === "interval") {
123
133
  const slots = intervalSlots(trigger.everyMinutes);
@@ -331,9 +341,11 @@ function createLaunchdBackend(opts = {}) {
331
341
  return null;
332
342
  }
333
343
  const disabled = await isDisabled(run, label);
344
+ const argv = extractProgramArguments(xml2);
334
345
  return {
335
346
  osTaskId: label,
336
- commandLine: extractProgramArguments(xml2).join(" "),
347
+ commandLine: argv.join(" "),
348
+ argv,
337
349
  spec: extractCalendarSpec(xml2),
338
350
  enabled: !disabled
339
351
  };
@@ -12421,8 +12433,48 @@ var EpochConfigSchema = z.object({
12421
12433
  message: "blankCards: id must be unique"
12422
12434
  }).optional(),
12423
12435
  blankGreeting: BlankGreetingSchema.optional(),
12424
- user: HostUserSchema.optional()
12436
+ user: HostUserSchema.optional(),
12437
+ settingsHidden: z.array(z.string().min(1)).min(1).optional(),
12438
+ settingsPanes: z.array(
12439
+ z.object({
12440
+ id: z.string().min(1).refine((id) => !isSettingsPaneId(id), {
12441
+ message: `id must not collide with a built-in pane (${SETTINGS_PANE_IDS.join(", ")})`
12442
+ }),
12443
+ label: z.string().min(1),
12444
+ icon: z.string().min(1).optional(),
12445
+ url: z.string().min(1).refine(
12446
+ (url) => {
12447
+ try {
12448
+ const scheme = new URL(url).protocol;
12449
+ return scheme === "http:" || scheme === "https:";
12450
+ } catch {
12451
+ return false;
12452
+ }
12453
+ },
12454
+ { message: "url must be an absolute http(s) URL" }
12455
+ ),
12456
+ before: z.string().min(1).optional()
12457
+ })
12458
+ ).min(1).refine((items) => new Set(items.map((item) => item.id)).size === items.length, {
12459
+ message: "settingsPanes: id must be unique"
12460
+ }).optional()
12425
12461
  });
12462
+ function settingsHiddenIssues(config) {
12463
+ const hidden = config.settingsHidden;
12464
+ if (hidden === void 0) return [];
12465
+ return hidden.filter((id) => !isSettingsPaneId(id)).map((id) => ({
12466
+ path: "settingsHidden",
12467
+ message: `unknown settings pane id: ${id} (known: ${SETTINGS_PANE_IDS.join(", ")})`
12468
+ }));
12469
+ }
12470
+ function settingsPaneBeforeIssues(config) {
12471
+ const panes = config.settingsPanes;
12472
+ if (panes === void 0) return [];
12473
+ return panes.filter((pane) => pane.before !== void 0 && !isSettingsPaneId(pane.before)).map((pane) => ({
12474
+ path: `settingsPanes.${pane.id}.before`,
12475
+ message: `unknown settings pane id: ${String(pane.before)} (known: ${SETTINGS_PANE_IDS.join(", ")})`
12476
+ }));
12477
+ }
12426
12478
  function defaultConfig() {
12427
12479
  return EpochConfigSchema.parse({});
12428
12480
  }
@@ -12893,6 +12945,8 @@ function parseRawConfig(raw, issues, track) {
12893
12945
  EpochConfigSchema.parse({})
12894
12946
  );
12895
12947
  issues.push(...outcome.issues);
12948
+ issues.push(...settingsHiddenIssues(outcome.value));
12949
+ issues.push(...settingsPaneBeforeIssues(outcome.value));
12896
12950
  if (outcome.issues.length === 0) collectKeyPaths(migrated.raw, track.file);
12897
12951
  return outcome.value;
12898
12952
  }
@@ -19524,6 +19578,9 @@ var ScheduleRegistrar = class {
19524
19578
  this.opts = opts;
19525
19579
  }
19526
19580
  opts;
19581
+ get homeDir() {
19582
+ return this.opts.homeDir;
19583
+ }
19527
19584
  osTaskIdFor(taskId) {
19528
19585
  const backend = this.opts.backend;
19529
19586
  if (!backend) return "";
@@ -19594,7 +19651,6 @@ var ScheduleRegistrar = class {
19594
19651
  if (this.opts.backend && def.osTaskId) await this.opts.backend.retire(def.osTaskId);
19595
19652
  }
19596
19653
  };
19597
- // src/schedule/doctor.ts
19598
19654
  async function diagnoseSchedules(opts) {
19599
19655
  const { store, backend, registrar } = opts;
19600
19656
  if (!backend) return { checked: 0, drifts: [], unsupported: true };
@@ -19631,8 +19687,18 @@ async function diagnoseSchedules(opts) {
19631
19687
  });
19632
19688
  }
19633
19689
  }
19690
+ let foreign = 0;
19634
19691
  for (const osTaskId of osTaskIds) {
19635
19692
  if (seen.has(osTaskId)) continue;
19693
+ const snapshot = await backend.inspect(osTaskId);
19694
+ if (!snapshot) {
19695
+ foreign++;
19696
+ continue;
19697
+ }
19698
+ if (!ownedByThisHome(snapshot.argv, registrar.homeDir)) {
19699
+ foreign++;
19700
+ continue;
19701
+ }
19636
19702
  const taskId = backend.taskIdFrom(osTaskId);
19637
19703
  drifts.push({
19638
19704
  kind: "orphan-in-os",
@@ -19640,7 +19706,17 @@ async function diagnoseSchedules(opts) {
19640
19706
  ...taskId ? { scheduleId: taskId } : {}
19641
19707
  });
19642
19708
  }
19643
- return { checked: defs.length + osTaskIds.length - seen.size, drifts, unsupported: false };
19709
+ return {
19710
+ checked: defs.length + osTaskIds.length - seen.size - foreign,
19711
+ drifts,
19712
+ unsupported: false
19713
+ };
19714
+ }
19715
+ function ownedByThisHome(argv, homeDir) {
19716
+ const at = argv.indexOf("--home");
19717
+ const home = at >= 0 ? argv[at + 1] : void 0;
19718
+ if (home === void 0 || home.length === 0) return true;
19719
+ return samePath(home, homeDir);
19644
19720
  }
19645
19721
  function compare2(def, snapshot, backend, registrar) {
19646
19722
  const expected = backend.expectedSnapshot(registrar.toRegisterInput(def, def.osCommand));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@epoch-agent/core",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "epoch-agent 核心引擎:ReAct 循环、Provider 路由、工具调度、记忆管理",
6
6
  "repository": {
@@ -27,8 +27,8 @@
27
27
  "gpt-tokenizer": "^3.4.0",
28
28
  "js-yaml": "^5.2.3",
29
29
  "zod": "^4.4.3",
30
- "@epoch-agent/infra": "0.15.0",
31
- "@epoch-agent/protocol": "0.15.0"
30
+ "@epoch-agent/infra": "0.16.0",
31
+ "@epoch-agent/protocol": "0.16.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@ai-sdk/amazon-bedrock": "^5.0.40",