@xdxer/dingtalk-agent 0.1.5-beta.8 → 0.1.5-beta.9

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.en.md +1 -0
  3. package/README.md +1 -0
  4. package/dist/bin/dingtalk-agent.js +115 -14
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-audit.js +108 -9
  7. package/dist/src/agent-audit.js.map +1 -1
  8. package/dist/src/development-workspace.js +37 -1
  9. package/dist/src/development-workspace.js.map +1 -1
  10. package/dist/src/dws.js +9 -0
  11. package/dist/src/dws.js.map +1 -1
  12. package/dist/src/multica-deploy.js +347 -105
  13. package/dist/src/multica-deploy.js.map +1 -1
  14. package/dist/src/multica-provider.js +151 -27
  15. package/dist/src/multica-provider.js.map +1 -1
  16. package/dist/src/multica-runtime-vocabulary.js +110 -0
  17. package/dist/src/multica-runtime-vocabulary.js.map +1 -0
  18. package/dist/src/promotion.js +2 -1
  19. package/dist/src/promotion.js.map +1 -1
  20. package/dist/src/schedule-plan.js +192 -24
  21. package/dist/src/schedule-plan.js.map +1 -1
  22. package/docs/AGENT-IN-PRODUCTION.md +255 -0
  23. package/docs/ARCHITECTURE.md +5 -1
  24. package/docs/PLATFORM-GUARDRAILS.md +188 -0
  25. package/docs/schemas/multica-deployment-plan.schema.json +3 -1
  26. package/docs/schemas/multica-deployment-receipt.schema.json +15 -1
  27. package/docs/schemas/multica-deployment-status.schema.json +6 -2
  28. package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
  29. package/docs/schemas/multica-workspace-status.schema.json +2 -0
  30. package/docs/schemas/workspace-scaffold.schema.json +1 -0
  31. package/lab/project-workspace/fake-multica-provider.mjs +97 -7
  32. package/lab/project-workspace/multica-deploy.fixture.json +2 -2
  33. package/lab/project-workspace/multica-readonly.fixture.json +2 -2
  34. package/lab/project-workspace/project.fixture.json +1 -1
  35. package/lab/robot-eval/suite.json +1 -1
  36. package/package.json +5 -2
  37. package/skills/core/dta-agent-compose/SKILL.md +1 -1
  38. package/skills/core/dta-agent-compose/references/drive-and-schedules.md +22 -2
  39. package/skills/core/dta-basic-behavior/SKILL.md +1 -1
  40. package/skills/core/dta-basic-behavior/references/memory-and-evolution.md +1 -1
  41. package/skills/core/dta-people-group-memory/SKILL.md +4 -4
  42. package/skills/core/dta-people-group-memory/references/model.md +1 -1
  43. package/skills/platforms/multica-dingtalk/PLATFORM.md +2 -2
  44. package/skills/platforms/multica-dingtalk/dta-deploy-multica/SKILL.md +6 -5
  45. package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +14 -4
  46. package/skills/platforms/multica-dingtalk/dta-ops-multica/SKILL.md +33 -7
  47. package/skills/platforms/multica-dingtalk/dta-ops-multica/scripts/multica_ext.py +118 -12
@@ -9,12 +9,162 @@
9
9
  *
10
10
  * Pure: no network, no side effects — safe to run in CI and fully testable.
11
11
  */
12
- import { inspectAgentProject } from './development-workspace.js';
12
+ import { inspectAgentProject, multicaAgentIdSource, } from './development-workspace.js';
13
+ // manifest 级的三项静态体检,show 和 plan 共用一份,避免两处逻辑漂移。
14
+ export function analyzeSchedules(declared) {
15
+ const collidingPairs = [];
16
+ const unverifiedPairs = [];
17
+ for (let i = 0; i < declared.length; i++) {
18
+ for (let j = i + 1; j < declared.length; j++) {
19
+ const pair = `${declared[i].name}×${declared[j].name}`;
20
+ // 跨时区:本地无 tz 换算 → 判不准是否同刻 → unverified(不静默当 distinct)。
21
+ if (declared[i].timezone !== declared[j].timezone) {
22
+ unverifiedPairs.push(pair);
23
+ continue;
24
+ }
25
+ const r = firesCollide(declared[i].cron, declared[j].cron);
26
+ if (r === 'collide')
27
+ collidingPairs.push(pair);
28
+ else if (r === 'unverified')
29
+ unverifiedPairs.push(pair);
30
+ }
31
+ }
32
+ const missingCompletion = declared.filter((s) => !s.completion?.trim()).map((s) => s.name);
33
+ return { collidingPairs, unverifiedPairs, missingCompletion };
34
+ }
35
+ /**
36
+ * 阻断部署的问题清单(空 = 可部署)。三类都算阻断,**包括 unverified**。
37
+ *
38
+ * unverified 曾经只告警,理由是"本地证明不了 ≠ 有问题"。这条理由错在赌的方向:
39
+ * `0 9 * * *`@Asia/Shanghai 与 `0 1 * * *`@UTC 是**同一时刻**,静态比对看不出来,
40
+ * 于是两条各建一个 autopilot、每天把同一条消息重复发给第三方——正是本守卫要防的那个伤害。
41
+ * 证不出不撞车就当撞车(fail closed):挡住是一次响亮且可恢复的配置改动,放过是一次静默且发出去收不回的重复外发。
42
+ *
43
+ * 出路有两条,都不需要改这里:把两条节律写成同一个 timezone(对齐后走真实 occurrence 比对,
44
+ * 判得出不撞就自动放行),或换掉本地读不懂的 cron 扩展语法。
45
+ *
46
+ * 判据只此一份:CLI 的 show 和 plan 都调它。此前两处各自手写 exit 条件,结果都漏了 unverified——
47
+ * 抽出 analyzeSchedules 是为了防判据分叉,退出判据自己却分叉了。
48
+ */
49
+ export function scheduleBlockers(a) {
50
+ return [...a.collidingPairs, ...a.unverifiedPairs, ...a.missingCompletion];
51
+ }
13
52
  // ephemeral = run-only (no work item); work-item = one work item per fire.
14
53
  const MULTICA_MODE = {
15
54
  ephemeral: 'run_only',
16
55
  'work-item': 'create_issue',
17
56
  };
57
+ // 只有 work-item 每次 fire 建 issue、平台才有 token 用量可查。
58
+ const isMetered = (mode) => mode === 'work-item';
59
+ // 五个字段的上界(分 时 日 月 周)。
60
+ const CRON_FIELD_MAX = [59, 23, 31, 12, 7];
61
+ // 名字字面量 → 数字(robfig/cron v3 支持,Multica 实测用 MON-FRI;不支持就会漏掉真双发)。
62
+ const DOW_NAMES = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 };
63
+ const MON_NAMES = {
64
+ JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,
65
+ };
66
+ /**
67
+ * 把一个 cron 字段展开成它命中的整数集合。支持 `*`、列表、范围、步长,以及 dow/月的名字字面量。
68
+ * 解析不出(`L`/`W`/`#`/`@` 等扩展语法)时返回 null —— 调用方据此判 unverified,不假装读懂了。
69
+ */
70
+ function expandCronField(field, max, names) {
71
+ const out = new Set();
72
+ const num = (t) => {
73
+ const up = t.toUpperCase();
74
+ if (names && up in names)
75
+ return names[up];
76
+ return /^\d+$/.test(t) ? parseInt(t, 10) : NaN;
77
+ };
78
+ for (const part of field.split(',')) {
79
+ const [range, stepRaw] = part.split('/');
80
+ const step = stepRaw ? parseInt(stepRaw, 10) : 1;
81
+ let lo = 0;
82
+ let hi = max;
83
+ if (range !== '*') {
84
+ const [a, b] = range.split('-');
85
+ lo = num(a);
86
+ // robfig getRange 的特判:**单个数字 + 步长时上界拉到 max**——`5/15` 等价 `5-59/15` = {5,20,35,50},
87
+ // 不是只有 {5}。当成 {5} 会漏掉 20/35/50,把真撞车判成 distinct → 正是本守卫要防的重复外发。
88
+ hi = b === undefined ? (stepRaw ? max : lo) : num(b);
89
+ }
90
+ if (Number.isNaN(lo) || Number.isNaN(hi) || Number.isNaN(step) || step < 1)
91
+ return null;
92
+ for (let v = lo; v <= hi && v <= max; v += step)
93
+ out.add(v === 7 && max === 7 ? 0 : v);
94
+ }
95
+ // 空集合 = 越界(`70`)或倒序区间(`5-2`),是【没读懂】不是【永不 fire】。
96
+ // 返回空集会让 setsIntersect 判 false → distinct(fail-open);返回 null → unverified,交给人核。
97
+ return out.size === 0 ? null : out;
98
+ }
99
+ function setsIntersect(a, b) {
100
+ for (const v of a)
101
+ if (b.has(v))
102
+ return true;
103
+ return false;
104
+ }
105
+ function parseCron(cron) {
106
+ const f = cron.trim().split(/\s+/);
107
+ if (f.length !== 5)
108
+ return null;
109
+ const min = expandCronField(f[0], 59);
110
+ const hour = expandCronField(f[1], 23);
111
+ const dom = expandCronField(f[2], 31);
112
+ const mon = expandCronField(f[3], 12, MON_NAMES);
113
+ const dow = expandCronField(f[4], 7, DOW_NAMES);
114
+ if (!min || !hour || !dom || !mon || !dow)
115
+ return null;
116
+ return { min, hour, dom, mon, dow, domStar: f[2] === '*', dowStar: f[4] === '*' };
117
+ }
118
+ // robfig/cron v3 的日匹配:dom 和 dow 都受限时用 OR,否则各自 AND(* 视为全通过)。
119
+ function dayMatch(p, dom, dowV) {
120
+ if (!p.domStar && !p.dowStar)
121
+ return p.dom.has(dom) || p.dow.has(dowV);
122
+ return (p.domStar || p.dom.has(dom)) && (p.dowStar || p.dow.has(dowV));
123
+ }
124
+ const isLeap = (y) => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0;
125
+ const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
126
+ function daysInMonth(y, m) {
127
+ return m === 2 && isLeap(y) ? 29 : DAYS_IN_MONTH[m - 1];
128
+ }
129
+ /**
130
+ * 两条节律会不会在【同一时刻】fire。返回 collide / distinct / unverified。
131
+ * 判据【按真实 occurrence】算,不做简化字段交集——robfig 的 dom/dow 是 OR 语义、支持名字字面量,
132
+ * 简化交集会误判(Multica 用 robfig/cron v3,`0 9 * * MON-FRI` 必须能识别)。
133
+ * - 分/时/月任一集合不相交 → 一定不同刻 → distinct(便宜的早退)。
134
+ * - 否则枚举【完整的 400 年格里高利周期】找同一天两条都 active。为什么必须整 400 年而不是几年窗口:
135
+ * 像 `0 9 29 2 1` 与 `0 9 30 2 3` 的首个共同 fire 落在 2040-02-29,短窗口会漏判成 distinct(假阴性→漏真双发)。
136
+ * 公历 (月,日,星期) 组合每 400 年完整重复,所以枚举一整轮 = 完备,"整轮无碰撞"才敢判 distinct。
137
+ * - 任一字段解析不出(扩展语法)→ unverified:**绝不当 distinct**,交给人核。
138
+ * export:给 scripts/check-schedule.mjs 做回归断言。
139
+ */
140
+ export function firesCollide(cronA, cronB) {
141
+ const a = parseCron(cronA);
142
+ const b = parseCron(cronB);
143
+ if (!a || !b)
144
+ return 'unverified';
145
+ if (!setsIntersect(a.min, b.min) || !setsIntersect(a.hour, b.hour) || !setsIntersect(a.mon, b.mon)) {
146
+ return 'distinct';
147
+ }
148
+ // 整 400 年周期手工走(不 new Date,避免 14 万次分配)。2000-01-01 是周六(dow=6)。
149
+ let dow = 6;
150
+ for (let y = 2000; y < 2400; y++) {
151
+ for (let m = 1; m <= 12; m++) {
152
+ const monA = a.mon.has(m);
153
+ const monB = b.mon.has(m);
154
+ const dim = daysInMonth(y, m);
155
+ if (!(monA && monB)) {
156
+ dow = (dow + dim) % 7;
157
+ continue;
158
+ } // 该月两条不同时活跃,跳过但推进 dow
159
+ for (let dom = 1; dom <= dim; dom++) {
160
+ if (dayMatch(a, dom, dow) && dayMatch(b, dom, dow))
161
+ return 'collide';
162
+ dow = (dow + 1) % 7;
163
+ }
164
+ }
165
+ }
166
+ return 'distinct';
167
+ }
18
168
  function shellQuote(value) {
19
169
  return `"${value.replace(/(["\\$`])/g, '\\$1')}"`;
20
170
  }
@@ -22,11 +172,11 @@ function envVar(ref) {
22
172
  // "env:MULTICA_FDE_AGENT_ID" -> "$MULTICA_FDE_AGENT_ID"
23
173
  return ref.startsWith('env:') ? `$${ref.slice(4)}` : ref;
24
174
  }
25
- function multicaApplyCommand(schedule, profile, agentIdFrom) {
175
+ function multicaApplyCommand(schedule, profile, agentRef) {
26
176
  const parts = [
27
177
  'python3 "$MULTICA_EXT" autopilot-create',
28
178
  `--profile ${profile}`,
29
- `--agent ${envVar(agentIdFrom)}`,
179
+ `--agent ${agentRef}`,
30
180
  `--title ${shellQuote(schedule.name)}`,
31
181
  `--mode ${MULTICA_MODE[schedule.mode]}`,
32
182
  `--cron ${shellQuote(schedule.cron)}`,
@@ -46,21 +196,39 @@ export function buildSchedulePlan(info, workspaceName) {
46
196
  throw new Error(`Project 未声明 Workspace: ${workspaceName}`);
47
197
  const declared = info.project.schedules;
48
198
  const provider = workspace.provider.kind;
199
+ const analysis = analyzeSchedules(declared); // 撞车/未验证/缺判据——manifest 级,与 provider 无关
49
200
  if (provider === 'multica') {
50
201
  const profile = workspace.provider.profile || '';
51
- const agentIdFrom = workspace.provider.agentIdFrom || '';
52
- if (!profile || !agentIdFrom) {
53
- throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile/agentIdFrom`);
202
+ if (!profile) {
203
+ throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile`);
204
+ }
205
+ // Agent 绑定来源与 deploy 同一份回退链(multicaAgentIdSource):黄金路径下
206
+ // scaffold 故意省略 agentIdFrom、首次 deploy 把真实 ID 写进 state——schedule
207
+ // 此前不读 state,导致部署完的 Agent 声明节律时还被要求手工接线(E-03)。
208
+ const source = multicaAgentIdSource(workspace);
209
+ if (source.kind === 'none') {
210
+ throw new Error(`Workspace ${workspaceName} 缺 Agent 绑定:manifest 未声明 provider.agentIdFrom,` +
211
+ `且尚无部署 state(.dingtalk-agent/state/workspaces/${workspaceName}.json)。` +
212
+ `先 dta deploy --workspace ${workspaceName} 创建 Agent(ID 会写入 state 自动复用),` +
213
+ '或在 manifest 显式声明 provider.agentIdFrom');
54
214
  }
215
+ const agentRef = source.kind === 'declared' ? envVar(source.ref) : source.agentId;
216
+ const agentIdSource = source.kind === 'declared' ? 'declared-env' : 'deploy-state';
55
217
  const schedules = declared.map((schedule) => ({
56
218
  ...schedule,
57
- applyCommand: multicaApplyCommand(schedule, profile, agentIdFrom),
219
+ applyCommand: multicaApplyCommand(schedule, profile, agentRef),
220
+ metered: isMetered(schedule.mode),
58
221
  }));
59
222
  return {
60
- $schema: 'dingtalk-agent/schedule-plan@1',
61
- workspace: workspaceName, provider, supported: true, count: schedules.length, schedules,
223
+ $schema: 'dingtalk-agent/schedule-plan@2',
224
+ workspace: workspaceName, provider, supported: true, count: schedules.length,
225
+ agentIdSource, schedules,
226
+ ...analysis,
62
227
  reconcile: '由 dta-ops-multica 技能执行:先 autopilot-list 读线上,按 title 匹配,' +
63
- '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py,$…_AGENT_ID 由受信环境注入。' +
228
+ '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py' +
229
+ (agentIdSource === 'declared-env'
230
+ ? ',$…_AGENT_ID 由受信环境注入。'
231
+ : ';Agent ID 来自本项目部署 state(首次 deploy 写入)。') +
64
232
  'autopilot-create 非幂等,务必先 list 再建,别盲建(会造重复→重复外发)。',
65
233
  sideEffect: false, dingtalkSideEffect: false,
66
234
  };
@@ -70,35 +238,35 @@ export function buildSchedulePlan(info, workspaceName) {
70
238
  ? 'opencode 是本地开发运行时,不承载常驻节律;节律需部署到托管平台'
71
239
  : `provider ${provider} 尚未提供 schedule 能力`;
72
240
  return {
73
- $schema: 'dingtalk-agent/schedule-plan@1',
241
+ $schema: 'dingtalk-agent/schedule-plan@2',
74
242
  workspace: workspaceName, provider, supported: false, reason,
75
243
  count: declared.length, schedules: [],
244
+ // provider 不支持,也照样报 manifest 自身的问题(撞车/未验证/缺判据),和平台支不支持无关。
245
+ ...analysis,
76
246
  reconcile: `记为缺口:声明了 ${declared.length} 条节律,但目标 provider 不支持——` +
77
247
  '别静默降级成"那就不定时了"。',
78
248
  sideEffect: false, dingtalkSideEffect: false,
79
249
  };
80
250
  }
81
251
  /**
82
- * Declared schedules + the one platform-independent invariant dta can check
83
- * statically: ticks must not share a minute. Outward-effect ticks have no lock on
84
- * their delivery receipt, so two firing in the same minute double-send to third
85
- * parties. Reports any minute field claimed by more than one schedule.
252
+ * Declared schedules + 两条与平台无关、能静态检的不变量:
253
+ * 1) 两拍不得在【同一时刻】并发 fire。按【真实 occurrence】判(见 firesCollide:robfig dom/dow OR、
254
+ * 名字字面量、闰年窗口枚举),不是简化字段交集。判不准的(跨时区/扩展语法)进 unverifiedPairs,不当 distinct。
255
+ * 2) 每条节律都要有数据派生的完成判据(completion),否则只能靠"那一拍跑没跑"判完成,漏拍就永久丢一期。
256
+ * ⚠️ 这两条都只看【本地声明的 manifest】,不查平台实际生效状态——线上真相要走 provider 回读。
86
257
  */
87
258
  export function buildScheduleShow(start, cliVersion = '') {
88
259
  const info = inspectAgentProject(start, cliVersion);
89
260
  const declared = info.project.schedules;
90
- const seen = new Map();
91
- for (const schedule of declared) {
92
- const minute = schedule.cron.trim().split(/\s+/)[0];
93
- seen.set(minute, (seen.get(minute) || 0) + 1);
94
- }
95
- const collidingMinutes = [...seen.entries()].filter(([, n]) => n > 1).map(([m]) => m);
261
+ const { collidingPairs, unverifiedPairs, missingCompletion } = analyzeSchedules(declared);
96
262
  return {
97
- $schema: 'dingtalk-agent/schedule-show@1',
263
+ $schema: 'dingtalk-agent/schedule-show@2',
98
264
  count: declared.length,
99
265
  schedules: declared,
100
- minutesDistinct: collidingMinutes.length === 0,
101
- collidingMinutes,
266
+ firesDistinct: collidingPairs.length === 0 && unverifiedPairs.length === 0,
267
+ collidingPairs,
268
+ unverifiedPairs,
269
+ missingCompletion,
102
270
  sideEffect: false, dingtalkSideEffect: false,
103
271
  };
104
272
  }
@@ -1 +1 @@
1
- {"version":3,"file":"schedule-plan.js","sourceRoot":"","sources":["../../src/schedule-plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schedule reconcile plan — dta's half of the driving contract.\n *\n * dta owns: read the declared schedules (dingtalk-agent.json#schedules), validate\n * them, and map them to a provider-specific reconcile plan. dta does NOT fire or\n * live-write triggers — that stays with the platform ops path (see ARCHITECTURE\n * §3.1, \"触发器不属于 dingtalk-agent 核心执行\"). The plan is the authoritative,\n * provider-mapped desired state the ops skill reconciles against live.\n *\n * Pure: no network, no side effects — safe to run in CI and fully testable.\n */\nimport { inspectAgentProject, type AgentProjectInfo, type DeclaredSchedule } from './development-workspace.js'\n\nexport interface SchedulePlanEntry extends DeclaredSchedule {\n applyCommand: string\n}\n\nexport interface SchedulePlan {\n $schema: 'dingtalk-agent/schedule-plan@1'\n workspace: string\n provider: string\n supported: boolean\n reason?: string\n count: number\n schedules: SchedulePlanEntry[]\n reconcile: string\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n// ephemeral = run-only (no work item); work-item = one work item per fire.\nconst MULTICA_MODE: Record<DeclaredSchedule['mode'], string> = {\n ephemeral: 'run_only',\n 'work-item': 'create_issue',\n}\n\nfunction shellQuote(value: string): string {\n return `\"${value.replace(/([\"\\\\$`])/g, '\\\\$1')}\"`\n}\n\nfunction envVar(ref: string): string {\n // \"env:MULTICA_FDE_AGENT_ID\" -> \"$MULTICA_FDE_AGENT_ID\"\n return ref.startsWith('env:') ? `$${ref.slice(4)}` : ref\n}\n\nfunction multicaApplyCommand(\n schedule: DeclaredSchedule, profile: string, agentIdFrom: string,\n): string {\n const parts = [\n 'python3 \"$MULTICA_EXT\" autopilot-create',\n `--profile ${profile}`,\n `--agent ${envVar(agentIdFrom)}`,\n `--title ${shellQuote(schedule.name)}`,\n `--mode ${MULTICA_MODE[schedule.mode]}`,\n `--cron ${shellQuote(schedule.cron)}`,\n `--timezone ${schedule.timezone}`,\n `--description ${shellQuote(schedule.wake || schedule.name)}`,\n ]\n return parts.join(' ')\n}\n\n/**\n * Build the reconcile plan for one workspace's provider. Returns supported:false\n * (with a reason) when the target platform does not offer schedules yet — the\n * declared rhythms are then a recorded gap, never a silent downgrade.\n */\nexport function buildSchedulePlan(info: AgentProjectInfo, workspaceName: string): SchedulePlan {\n const workspace = info.workspaces.find((item) => item.name === workspaceName)\n if (!workspace) throw new Error(`Project 未声明 Workspace: ${workspaceName}`)\n const declared = info.project.schedules\n const provider = workspace.provider.kind\n\n if (provider === 'multica') {\n const profile = workspace.provider.profile || ''\n const agentIdFrom = workspace.provider.agentIdFrom || ''\n if (!profile || !agentIdFrom) {\n throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile/agentIdFrom`)\n }\n const schedules = declared.map((schedule) => ({\n ...schedule,\n applyCommand: multicaApplyCommand(schedule, profile, agentIdFrom),\n }))\n return {\n $schema: 'dingtalk-agent/schedule-plan@1',\n workspace: workspaceName, provider, supported: true, count: schedules.length, schedules,\n reconcile: '由 dta-ops-multica 技能执行:先 autopilot-list 读线上,按 title 匹配,' +\n '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py,$…_AGENT_ID 由受信环境注入。' +\n 'autopilot-create 非幂等,务必先 list 再建,别盲建(会造重复→重复外发)。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n }\n\n // opencode / local dev / not-yet-supported platforms: no schedule capability.\n const reason = provider === 'opencode'\n ? 'opencode 是本地开发运行时,不承载常驻节律;节律需部署到托管平台'\n : `provider ${provider} 尚未提供 schedule 能力`\n return {\n $schema: 'dingtalk-agent/schedule-plan@1',\n workspace: workspaceName, provider, supported: false, reason,\n count: declared.length, schedules: [],\n reconcile: `记为缺口:声明了 ${declared.length} 条节律,但目标 provider 不支持——` +\n '别静默降级成\"那就不定时了\"。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n\nexport interface ScheduleShow {\n $schema: 'dingtalk-agent/schedule-show@1'\n count: number\n schedules: DeclaredSchedule[]\n minutesDistinct: boolean\n collidingMinutes: string[]\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n/**\n * Declared schedules + the one platform-independent invariant dta can check\n * statically: ticks must not share a minute. Outward-effect ticks have no lock on\n * their delivery receipt, so two firing in the same minute double-send to third\n * parties. Reports any minute field claimed by more than one schedule.\n */\nexport function buildScheduleShow(start: string, cliVersion = ''): ScheduleShow {\n const info = inspectAgentProject(start, cliVersion)\n const declared = info.project.schedules\n const seen = new Map<string, number>()\n for (const schedule of declared) {\n const minute = schedule.cron.trim().split(/\\s+/)[0]\n seen.set(minute, (seen.get(minute) || 0) + 1)\n }\n const collidingMinutes = [...seen.entries()].filter(([, n]) => n > 1).map(([m]) => m)\n return {\n $schema: 'dingtalk-agent/schedule-show@1',\n count: declared.length,\n schedules: declared,\n minutesDistinct: collidingMinutes.length === 0,\n collidingMinutes,\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n"]}
1
+ {"version":3,"file":"schedule-plan.js","sourceRoot":"","sources":["../../src/schedule-plan.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Schedule reconcile plan — dta's half of the driving contract.\n *\n * dta owns: read the declared schedules (dingtalk-agent.json#schedules), validate\n * them, and map them to a provider-specific reconcile plan. dta does NOT fire or\n * live-write triggers — that stays with the platform ops path (see ARCHITECTURE\n * §3.1, \"触发器不属于 dingtalk-agent 核心执行\"). The plan is the authoritative,\n * provider-mapped desired state the ops skill reconciles against live.\n *\n * Pure: no network, no side effects — safe to run in CI and fully testable.\n */\nimport {\n inspectAgentProject, multicaAgentIdSource,\n type AgentProjectInfo, type DeclaredSchedule,\n} from './development-workspace.js'\n\nexport interface SchedulePlanEntry extends DeclaredSchedule {\n applyCommand: string\n // 平台是否为这一拍记账。work-item 每次 fire 建 issue → 有 token 用量可查;\n // ephemeral(run_only) 不建 issue → 平台不记账,这一拍的成本【看不见】。\n // 改 mode 就是在改计量口径,这里显式标出来,别让它变成隐形盲区。\n metered: boolean\n}\n\nexport interface SchedulePlan {\n $schema: 'dingtalk-agent/schedule-plan@2'\n workspace: string\n provider: string\n supported: boolean\n reason?: string\n count: number\n /**\n * Agent 绑定来源:manifest 声明的 env 引用(declared-env),或首次 deploy 写入\n * workspace state 的真实 ID(deploy-state)。黄金路径(scaffold 故意省略\n * agentIdFrom、由 deploy 创建 Agent 存 ID)走的是后者。\n */\n agentIdSource?: 'declared-env' | 'deploy-state'\n schedules: SchedulePlanEntry[]\n // 声明了 cron 却没写 completion(数据派生的完成判据)的节律名。\n // 没有完成判据 = 只能靠\"那一拍跑了没有\"来判完成,而平台会漏拍/跳过迟到的 fire,\n // 一次抖动就永久丢一期。这些拍记成缺口,不静默放过。\n missingCompletion: string[]\n // 会同一时刻并发 fire 的节律对 / 无法静态确认是否同刻的对。\n // ⚠️ plan 是 operator 真正照着部署的路径,所以撞车必须在这里也拦——\n // 否则 show 报了撞车、plan 照样吐出 apply 命令,两条都建成 autopilot → 重复外发。\n collidingPairs: string[]\n unverifiedPairs: string[]\n reconcile: string\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n// manifest 级的三项静态体检,show 和 plan 共用一份,避免两处逻辑漂移。\nexport function analyzeSchedules(declared: DeclaredSchedule[]): {\n collidingPairs: string[]; unverifiedPairs: string[]; missingCompletion: string[]\n} {\n const collidingPairs: string[] = []\n const unverifiedPairs: string[] = []\n for (let i = 0; i < declared.length; i++) {\n for (let j = i + 1; j < declared.length; j++) {\n const pair = `${declared[i].name}×${declared[j].name}`\n // 跨时区:本地无 tz 换算 → 判不准是否同刻 → unverified(不静默当 distinct)。\n if (declared[i].timezone !== declared[j].timezone) { unverifiedPairs.push(pair); continue }\n const r = firesCollide(declared[i].cron, declared[j].cron)\n if (r === 'collide') collidingPairs.push(pair)\n else if (r === 'unverified') unverifiedPairs.push(pair)\n }\n }\n const missingCompletion = declared.filter((s) => !s.completion?.trim()).map((s) => s.name)\n return { collidingPairs, unverifiedPairs, missingCompletion }\n}\n\n/**\n * 阻断部署的问题清单(空 = 可部署)。三类都算阻断,**包括 unverified**。\n *\n * unverified 曾经只告警,理由是\"本地证明不了 ≠ 有问题\"。这条理由错在赌的方向:\n * `0 9 * * *`@Asia/Shanghai 与 `0 1 * * *`@UTC 是**同一时刻**,静态比对看不出来,\n * 于是两条各建一个 autopilot、每天把同一条消息重复发给第三方——正是本守卫要防的那个伤害。\n * 证不出不撞车就当撞车(fail closed):挡住是一次响亮且可恢复的配置改动,放过是一次静默且发出去收不回的重复外发。\n *\n * 出路有两条,都不需要改这里:把两条节律写成同一个 timezone(对齐后走真实 occurrence 比对,\n * 判得出不撞就自动放行),或换掉本地读不懂的 cron 扩展语法。\n *\n * 判据只此一份:CLI 的 show 和 plan 都调它。此前两处各自手写 exit 条件,结果都漏了 unverified——\n * 抽出 analyzeSchedules 是为了防判据分叉,退出判据自己却分叉了。\n */\nexport function scheduleBlockers(a: {\n collidingPairs: string[]; unverifiedPairs: string[]; missingCompletion: string[]\n}): string[] {\n return [...a.collidingPairs, ...a.unverifiedPairs, ...a.missingCompletion]\n}\n\n// ephemeral = run-only (no work item); work-item = one work item per fire.\nconst MULTICA_MODE: Record<DeclaredSchedule['mode'], string> = {\n ephemeral: 'run_only',\n 'work-item': 'create_issue',\n}\n\n// 只有 work-item 每次 fire 建 issue、平台才有 token 用量可查。\nconst isMetered = (mode: DeclaredSchedule['mode']): boolean => mode === 'work-item'\n\n// 五个字段的上界(分 时 日 月 周)。\nconst CRON_FIELD_MAX = [59, 23, 31, 12, 7]\n// 名字字面量 → 数字(robfig/cron v3 支持,Multica 实测用 MON-FRI;不支持就会漏掉真双发)。\nconst DOW_NAMES: Record<string, number> = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 }\nconst MON_NAMES: Record<string, number> = {\n JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12,\n}\n\n/**\n * 把一个 cron 字段展开成它命中的整数集合。支持 `*`、列表、范围、步长,以及 dow/月的名字字面量。\n * 解析不出(`L`/`W`/`#`/`@` 等扩展语法)时返回 null —— 调用方据此判 unverified,不假装读懂了。\n */\nfunction expandCronField(field: string, max: number, names?: Record<string, number>): Set<number> | null {\n const out = new Set<number>()\n const num = (t: string): number => {\n const up = t.toUpperCase()\n if (names && up in names) return names[up]\n return /^\\d+$/.test(t) ? parseInt(t, 10) : NaN\n }\n for (const part of field.split(',')) {\n const [range, stepRaw] = part.split('/')\n const step = stepRaw ? parseInt(stepRaw, 10) : 1\n let lo = 0\n let hi = max\n if (range !== '*') {\n const [a, b] = range.split('-')\n lo = num(a)\n // robfig getRange 的特判:**单个数字 + 步长时上界拉到 max**——`5/15` 等价 `5-59/15` = {5,20,35,50},\n // 不是只有 {5}。当成 {5} 会漏掉 20/35/50,把真撞车判成 distinct → 正是本守卫要防的重复外发。\n hi = b === undefined ? (stepRaw ? max : lo) : num(b)\n }\n if (Number.isNaN(lo) || Number.isNaN(hi) || Number.isNaN(step) || step < 1) return null\n for (let v = lo; v <= hi && v <= max; v += step) out.add(v === 7 && max === 7 ? 0 : v)\n }\n // 空集合 = 越界(`70`)或倒序区间(`5-2`),是【没读懂】不是【永不 fire】。\n // 返回空集会让 setsIntersect 判 false → distinct(fail-open);返回 null → unverified,交给人核。\n return out.size === 0 ? null : out\n}\n\nfunction setsIntersect(a: Set<number>, b: Set<number>): boolean {\n for (const v of a) if (b.has(v)) return true\n return false\n}\n\ninterface ParsedCron { min: Set<number>; hour: Set<number>; dom: Set<number>; mon: Set<number>; dow: Set<number>; domStar: boolean; dowStar: boolean }\n\nfunction parseCron(cron: string): ParsedCron | null {\n const f = cron.trim().split(/\\s+/)\n if (f.length !== 5) return null\n const min = expandCronField(f[0], 59)\n const hour = expandCronField(f[1], 23)\n const dom = expandCronField(f[2], 31)\n const mon = expandCronField(f[3], 12, MON_NAMES)\n const dow = expandCronField(f[4], 7, DOW_NAMES)\n if (!min || !hour || !dom || !mon || !dow) return null\n return { min, hour, dom, mon, dow, domStar: f[2] === '*', dowStar: f[4] === '*' }\n}\n\n// robfig/cron v3 的日匹配:dom 和 dow 都受限时用 OR,否则各自 AND(* 视为全通过)。\nfunction dayMatch(p: ParsedCron, dom: number, dowV: number): boolean {\n if (!p.domStar && !p.dowStar) return p.dom.has(dom) || p.dow.has(dowV)\n return (p.domStar || p.dom.has(dom)) && (p.dowStar || p.dow.has(dowV))\n}\n\nconst isLeap = (y: number): boolean => (y % 4 === 0 && y % 100 !== 0) || y % 400 === 0\nconst DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]\nfunction daysInMonth(y: number, m: number): number {\n return m === 2 && isLeap(y) ? 29 : DAYS_IN_MONTH[m - 1]\n}\n\n/**\n * 两条节律会不会在【同一时刻】fire。返回 collide / distinct / unverified。\n * 判据【按真实 occurrence】算,不做简化字段交集——robfig 的 dom/dow 是 OR 语义、支持名字字面量,\n * 简化交集会误判(Multica 用 robfig/cron v3,`0 9 * * MON-FRI` 必须能识别)。\n * - 分/时/月任一集合不相交 → 一定不同刻 → distinct(便宜的早退)。\n * - 否则枚举【完整的 400 年格里高利周期】找同一天两条都 active。为什么必须整 400 年而不是几年窗口:\n * 像 `0 9 29 2 1` 与 `0 9 30 2 3` 的首个共同 fire 落在 2040-02-29,短窗口会漏判成 distinct(假阴性→漏真双发)。\n * 公历 (月,日,星期) 组合每 400 年完整重复,所以枚举一整轮 = 完备,\"整轮无碰撞\"才敢判 distinct。\n * - 任一字段解析不出(扩展语法)→ unverified:**绝不当 distinct**,交给人核。\n * export:给 scripts/check-schedule.mjs 做回归断言。\n */\nexport function firesCollide(cronA: string, cronB: string): 'collide' | 'distinct' | 'unverified' {\n const a = parseCron(cronA)\n const b = parseCron(cronB)\n if (!a || !b) return 'unverified'\n if (!setsIntersect(a.min, b.min) || !setsIntersect(a.hour, b.hour) || !setsIntersect(a.mon, b.mon)) {\n return 'distinct'\n }\n // 整 400 年周期手工走(不 new Date,避免 14 万次分配)。2000-01-01 是周六(dow=6)。\n let dow = 6\n for (let y = 2000; y < 2400; y++) {\n for (let m = 1; m <= 12; m++) {\n const monA = a.mon.has(m)\n const monB = b.mon.has(m)\n const dim = daysInMonth(y, m)\n if (!(monA && monB)) { dow = (dow + dim) % 7; continue } // 该月两条不同时活跃,跳过但推进 dow\n for (let dom = 1; dom <= dim; dom++) {\n if (dayMatch(a, dom, dow) && dayMatch(b, dom, dow)) return 'collide'\n dow = (dow + 1) % 7\n }\n }\n }\n return 'distinct'\n}\n\nfunction shellQuote(value: string): string {\n return `\"${value.replace(/([\"\\\\$`])/g, '\\\\$1')}\"`\n}\n\nfunction envVar(ref: string): string {\n // \"env:MULTICA_FDE_AGENT_ID\" -> \"$MULTICA_FDE_AGENT_ID\"\n return ref.startsWith('env:') ? `$${ref.slice(4)}` : ref\n}\n\nfunction multicaApplyCommand(\n schedule: DeclaredSchedule, profile: string, agentRef: string,\n): string {\n const parts = [\n 'python3 \"$MULTICA_EXT\" autopilot-create',\n `--profile ${profile}`,\n `--agent ${agentRef}`,\n `--title ${shellQuote(schedule.name)}`,\n `--mode ${MULTICA_MODE[schedule.mode]}`,\n `--cron ${shellQuote(schedule.cron)}`,\n `--timezone ${schedule.timezone}`,\n `--description ${shellQuote(schedule.wake || schedule.name)}`,\n ]\n return parts.join(' ')\n}\n\n/**\n * Build the reconcile plan for one workspace's provider. Returns supported:false\n * (with a reason) when the target platform does not offer schedules yet — the\n * declared rhythms are then a recorded gap, never a silent downgrade.\n */\nexport function buildSchedulePlan(info: AgentProjectInfo, workspaceName: string): SchedulePlan {\n const workspace = info.workspaces.find((item) => item.name === workspaceName)\n if (!workspace) throw new Error(`Project 未声明 Workspace: ${workspaceName}`)\n const declared = info.project.schedules\n const provider = workspace.provider.kind\n const analysis = analyzeSchedules(declared) // 撞车/未验证/缺判据——manifest 级,与 provider 无关\n\n if (provider === 'multica') {\n const profile = workspace.provider.profile || ''\n if (!profile) {\n throw new Error(`Workspace ${workspaceName} 的 multica provider 缺 profile`)\n }\n // Agent 绑定来源与 deploy 同一份回退链(multicaAgentIdSource):黄金路径下\n // scaffold 故意省略 agentIdFrom、首次 deploy 把真实 ID 写进 state——schedule\n // 此前不读 state,导致部署完的 Agent 声明节律时还被要求手工接线(E-03)。\n const source = multicaAgentIdSource(workspace)\n if (source.kind === 'none') {\n throw new Error(\n `Workspace ${workspaceName} 缺 Agent 绑定:manifest 未声明 provider.agentIdFrom,` +\n `且尚无部署 state(.dingtalk-agent/state/workspaces/${workspaceName}.json)。` +\n `先 dta deploy --workspace ${workspaceName} 创建 Agent(ID 会写入 state 自动复用),` +\n '或在 manifest 显式声明 provider.agentIdFrom')\n }\n const agentRef = source.kind === 'declared' ? envVar(source.ref) : source.agentId\n const agentIdSource = source.kind === 'declared' ? 'declared-env' as const : 'deploy-state' as const\n const schedules = declared.map((schedule) => ({\n ...schedule,\n applyCommand: multicaApplyCommand(schedule, profile, agentRef),\n metered: isMetered(schedule.mode),\n }))\n return {\n $schema: 'dingtalk-agent/schedule-plan@2',\n workspace: workspaceName, provider, supported: true, count: schedules.length,\n agentIdSource, schedules,\n ...analysis,\n reconcile: '由 dta-ops-multica 技能执行:先 autopilot-list 读线上,按 title 匹配,' +\n '只建缺的、改漂移的、删多余的;$MULTICA_EXT 指向 multica_ext.py' +\n (agentIdSource === 'declared-env'\n ? ',$…_AGENT_ID 由受信环境注入。'\n : ';Agent ID 来自本项目部署 state(首次 deploy 写入)。') +\n 'autopilot-create 非幂等,务必先 list 再建,别盲建(会造重复→重复外发)。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n }\n\n // opencode / local dev / not-yet-supported platforms: no schedule capability.\n const reason = provider === 'opencode'\n ? 'opencode 是本地开发运行时,不承载常驻节律;节律需部署到托管平台'\n : `provider ${provider} 尚未提供 schedule 能力`\n return {\n $schema: 'dingtalk-agent/schedule-plan@2',\n workspace: workspaceName, provider, supported: false, reason,\n count: declared.length, schedules: [],\n // provider 不支持,也照样报 manifest 自身的问题(撞车/未验证/缺判据),和平台支不支持无关。\n ...analysis,\n reconcile: `记为缺口:声明了 ${declared.length} 条节律,但目标 provider 不支持——` +\n '别静默降级成\"那就不定时了\"。',\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n\nexport interface ScheduleShow {\n $schema: 'dingtalk-agent/schedule-show@2'\n count: number\n schedules: DeclaredSchedule[]\n // 会同一时刻并发 fire 的节律对,如 \"轻拍×补拍\"。外发拍的送达回执是发完才写、没有锁,\n // 同刻并发会重复送第三方,所以这是必须静态拦下的一条。\n firesDistinct: boolean\n collidingPairs: string[]\n // 无法静态确认是否同刻的对(跨时区、或 cron 有本解析器读不懂的扩展语法)。\n // 【绝不当成 distinct】——只是本地静态检查证明不了,要人核或走 provider occurrence 预览。\n unverifiedPairs: string[]\n // 声明了 cron 却没写数据派生完成判据(completion)的节律名——见 SchedulePlan.missingCompletion。\n missingCompletion: string[]\n sideEffect: false\n dingtalkSideEffect: false\n}\n\n/**\n * Declared schedules + 两条与平台无关、能静态检的不变量:\n * 1) 两拍不得在【同一时刻】并发 fire。按【真实 occurrence】判(见 firesCollide:robfig dom/dow OR、\n * 名字字面量、闰年窗口枚举),不是简化字段交集。判不准的(跨时区/扩展语法)进 unverifiedPairs,不当 distinct。\n * 2) 每条节律都要有数据派生的完成判据(completion),否则只能靠\"那一拍跑没跑\"判完成,漏拍就永久丢一期。\n * ⚠️ 这两条都只看【本地声明的 manifest】,不查平台实际生效状态——线上真相要走 provider 回读。\n */\nexport function buildScheduleShow(start: string, cliVersion = ''): ScheduleShow {\n const info = inspectAgentProject(start, cliVersion)\n const declared = info.project.schedules\n const { collidingPairs, unverifiedPairs, missingCompletion } = analyzeSchedules(declared)\n return {\n $schema: 'dingtalk-agent/schedule-show@2',\n count: declared.length,\n schedules: declared,\n firesDistinct: collidingPairs.length === 0 && unverifiedPairs.length === 0,\n collidingPairs,\n unverifiedPairs,\n missingCompletion,\n sideEffect: false, dingtalkSideEffect: false,\n }\n}\n"]}
@@ -0,0 +1,255 @@
1
+ # 交付之后:一个数字员工在生产上是什么样
2
+
3
+ > 给**接手一个已经在跑的 Agent** 的人或 Coding Agent。
4
+ >
5
+ > [ARCHITECTURE](ARCHITECTURE.md) 回答"一个数字员工由什么构成、每件事谁说了算"——那是**设计视角**。
6
+ > 本文回答"它交付之后,一天到底怎么转、坏起来长什么样、你去哪看"——**运行视角**。
7
+ > 两篇不重复:设计视角讲边界为什么这么划,运行视角讲这些边界在生产上呈现成什么现象。
8
+ >
9
+ > 装机与升级见 [Installation](INSTALLATION.md);评测方法见 [Self-test](SELF-TEST.md);
10
+ > 行为协议正本见 [`dta-basic-behavior`](../skills/core/dta-basic-behavior/SKILL.md)。
11
+
12
+ ---
13
+
14
+ ## 0. 一句话
15
+
16
+ **它没有常驻进程。** 每一次"活过来"都是一个全新的、一次性的沙箱:装配 Skill → 干一件事 → 消失。
17
+ 两次运行之间它什么都不记得。
18
+
19
+ 这一条推出下面所有内容。**记忆不在模型里,全部在外部权威上**——落不到权威上的事,等于没发生。
20
+
21
+ ---
22
+
23
+ ## 1. 一天里它被叫醒几次、被谁叫醒
24
+
25
+ 三类唤醒源,语义完全不同(分类与权威归属见 [ARCHITECTURE §3.1](ARCHITECTURE.md#31-驱动一个数字员工怎么被叫醒)):
26
+
27
+ | 唤醒源 | 有人在等吗 | 常态 | 失败的代价 |
28
+ |---|---|---|---|
29
+ | **IM · @ 或单聊** | 有 | 该回一句(先过响应资格判断) | 用户直接看见 |
30
+ | **IM · 群里没 @ 它** | 不一定 | **默认 `silence`**——群消息未 @ 不擅自插话(响应资格由 Basic 判) | 乱插话反而是错 |
31
+ | **常驻节律**(平台 schedule) | 没有 | **静默** | 没人看见,会积压 |
32
+ | **受控任务**(平台 issue / 开发者派活) | 有,异步 | 有 issue 可回读 | 在平台上看得见 |
33
+
34
+ > 「有人在等就回」不等于「群里每句都回」。**响应资格先于一切**——@ 或单聊才默认该回,
35
+ > 群里没点名默认 `silence`,判据在 `dta-basic-behavior`,别把"有人在等"简化成"必须回一句"。
36
+
37
+ **运行时的量级差得很远。** 以本仓库的参考实现「FDE 教练」2026-07-23 一天为例:
38
+ IM 对话 **39 轮**,节律唤醒 **111 次**——**节律是 IM 的三倍**。
39
+ 这是数字员工与聊天机器人最大的运行时差别:**它大部分时间在没人看的时候干活。**
40
+
41
+ 只声明节律是不够的,要能读回来:
42
+
43
+ ```bash
44
+ dta schedule show # 只读【本地 manifest 声明】的节律 + 静态校验(同刻/完成判据)
45
+ dta schedule plan --workspace <name> # 把声明映射成 provider 该执行的 reconcile 命令
46
+ ```
47
+
48
+ > ⚠️ **这两条读的是【声明态】(本地 `dingtalk-agent.json`),不是平台的【实际态】。** 它们只 `inspectAgentProject`,不查 provider——`plan` 也只是打印该由 ops 技能执行的对账命令。排障时想知道**线上真实生效的节律**,要走 provider 回读(Multica 走 `dta-ops-multica` 的 `autopilot-list`),别拿一份可能没 apply 的 manifest 当线上真相。
49
+
50
+ > **`dta deploy` 不创建也不修改节律。** 节律属于平台,增删由 operator 执行。
51
+ > 这不是缺功能,是边界:Agent 不自建定时器、不自改节律([ARCHITECTURE §3.1](ARCHITECTURE.md#31-驱动一个数字员工怎么被叫醒))。
52
+
53
+ **排节律时的四条经验**(都从真实生产换来,`dta schedule show/plan` 会静态检出前两条):
54
+
55
+ 1. **每条节律必须有数据派生的完成判据(`completion`)。** 没有它就只能靠"那一拍跑没跑"判完成,而平台会漏拍、会跳过迟到过久的 fire——一次抖动就永久丢一期。`schedule show` 缺 `completion` 会告警。
56
+ 2. **两拍不得在同一时刻 fire。** 外发拍的送达回执是发完才写、没有锁,同刻并发会把同一条消息发两遍给第三方。(只错开分钟不够看——要看分钟×小时真不真相交。**也别用两个时区写节律**:`0 9 * * *`@Asia/Shanghai 和 `0 1 * * *`@UTC 是同一时刻,本地换算不了,`schedule show/plan` 一律当撞车挡住。全 Agent 统一一个 timezone。)
57
+ 3. **有时效的轻活排前,别让重活把它饿死。** 一拍预算有限。把每次都超时的重活排在序列最前,排它后面的轻活(对账、收件)会整轮轮不到——真事故:夜拍里留痕对账排第 7 位、前面的夜间重建每拍超时,结果留痕对账整夜没跑到。**正解**:有时效/轻的排前面,或给重活单独一条节律、独立预算,别和轻活挤同一拍。
58
+ 4. **入口是脉冲、出口是均匀——产能按峰值窗口算,不按日均。** 用户往往集中在某个时段涌入,而节律是固定频率。按日总量算"够用"会在高峰那几小时实际欠账。
59
+
60
+ ---
61
+
62
+ ## 2. 值班 ≠ 任务:运行上最常见的一组混淆
63
+
64
+ | | **值班(duty)** | **任务(task)** |
65
+ |---|---|---|
66
+ | 本质 | 永远做不完的**维护义务** | 有终态的**一件事** |
67
+ | 完成判据 | **从数据派生**(水位 ≥ 今天 / backlog = 0) | 对象自身的终态(如 Todo `isDone`) |
68
+ | 状态存哪 | **不存**。索引本身就是完成状态 | 存在那个对象上 |
69
+ | 漏一次 | 下一拍自愈 | 断链,**而且不自知** |
70
+
71
+ **为什么完成判据必须数据派生**:拍是无状态的、会漏、会被平台跳过、会换实例。
72
+ "今晚跑过没有"这个问题只有数据能回答。不记状态、不建标记、不上锁、不信会话记忆。
73
+
74
+ 这条有一个几乎所有人都会踩的推论:
75
+
76
+ > **任何「某时刻必须发生」的事,判据只能是「今天/本周它发生了没有」,不能是「那一拍跑了没有」。**
77
+
78
+ 托管平台普遍会塌缩漏掉的 fire、跳过迟到过久的 fire。把判据绑在"某一拍"上,一次 runtime 抖动就永久丢一期,
79
+ 事后还查不出来。
80
+
81
+ ---
82
+
83
+ ## 3. 两阶段值班:命令给清单,模型干活
84
+
85
+ 这是生产上最容易出事的一类,值得单独知道。
86
+
87
+ - **一阶段**:命令跑完,活就干完了(重建索引、对账、汇总)。
88
+ - **两阶段**:命令吐出来的是**待办清单**,真正的活是模型自己做——读全文 → 生成 → 逐条写回。
89
+
90
+ 于是:
91
+
92
+ > **这一拍到底做没做,只看它发出了几次写调用——不看它跑了几次命令。**
93
+ > 没有对应的写调用,就不许在自述里写「已处理 N 条」。
94
+
95
+ **为什么把这条写这么重**:谎报比不做更糟。不做,backlog 明摆着;谎报,backlog 在"已完成"的假象下
96
+ 无限积压,**没有任何人会发现**。
97
+
98
+ 配套的监督必须**绕开自述**:直接算准 backlog,和上一次快照比趋势。跑了多拍而 backlog 没往下走,就报警。
99
+ **写没写,用数据判,编不了。**
100
+
101
+ ---
102
+
103
+ ## 4. backlog 是队列:只看净值会误诊
104
+
105
+ 两阶段值班的积压本质是队列,三个数字才够用:
106
+
107
+ ```text
108
+ 入口速率 = 单位时间新进来的活
109
+ 出口速率 = 单位时间真被写掉的活
110
+ 净值 = 入口 − 出口
111
+ ```
112
+
113
+ **健康不是「backlog 小」,是「出口 ≥ 入口」。** 净值长期为正,backlog 多大都只是时间问题。
114
+
115
+ 只看净值分不清四种病,而它们的处方完全不同:
116
+
117
+ | 现象 | 病 | 处方 |
118
+ |---|---|---|
119
+ | 入 40,出 0 | 空转,或**写在了错的地方** | 查写调用到底落在哪一行 |
120
+ | 入 40,出 15 | 产能不足 | 加拍、加批量 |
121
+ | 入 40,出 40,backlog 不降 | 打平但清不掉历史欠账 | 要额外产能,或削入口 |
122
+ | 入 0,出 0 | 可能是**空读**(见 §5) | 先别判,下轮再看 |
123
+
124
+ **拆的办法很便宜**:对 backlog 的**键集合**做前后两次差分——本次在、上次不在 = 入口;上次在、本次不在 = 出口。
125
+
126
+ 还有两条运行时经验:
127
+
128
+ - **入口通常是脉冲的,出口是均匀的。** 只按日总量算产能会得到"够用"的错觉,而高峰那几小时实际在欠。
129
+ - **队列有头,头会堵。** 若实现是"先补最旧的一天",那一天一旦因为数据缺陷清不掉,**后面全部够不着**,
130
+ 而每一拍的写回都"成功"。这类故障的表征是:写调用一直在发、成功率 100%、backlog 纹丝不动。
131
+
132
+ ---
133
+
134
+ ## 5. 生产上的四类假信号
135
+
136
+ 按危险程度排序。**它们的共同点是:都伪装成成功。**
137
+
138
+ 1. **报 0 的 ✅ 不可信。** `活跃 0 人`、`0 行`、`待处理 0 条`——判据往往是结构性的
139
+ (`集合为空 → backlog 为空 → done=True`),**于是任何一次读表失败都会变成一个自信的 ✅**。
140
+ 规则:报 0 的那一项不算做过,下一轮照常再来(它幂等),同时记降级并点名。
141
+ 2. **写后回读读的是同一个错误对象。** 回读校验只有在**独立定位**目标时才有效。
142
+ 若写 `rows[0]`、回读也取 `rows[0]`,则永远自洽、永远发现不了写错了行。
143
+ 3. **两个完成判据分开写,迟早会漂。** 同一件事若在两处各判一次,一处说"没完成"、一处说"全好了",
144
+ 而调用方只问其中一个——补救路径就再也不会被触发,且没有任何一层报警。
145
+ **同一个语义只留一个谓词,两个调用方共用。**
146
+ 4. **命令被沙箱砍掉时不会留痕。** 留痕通常写在动作**之后**,被砍时那行根本没执行,
147
+ 事后看就像"这一拍没跑过"。超时后必须补一行降级。
148
+
149
+ 失败要说人话,四个状态别混:
150
+
151
+ | 状态 | 什么时候用 | 什么时候**不能**用 |
152
+ |---|---|---|
153
+ | `pass` | 真做成了,**且独立回读确认过** | "我试过了" |
154
+ | `degraded` | 做了一部分 / 被超时砍掉 / 读到可疑空集 | — |
155
+ | `fail` | 明确失败 | — |
156
+ | `blocked` | 缺上游前提(权限、配置、目标不存在) | 含糊报"出错了" |
157
+
158
+ **没送达就写 `pass`,比链条断掉更糟**——断链至少看得出来"今天没跑"。
159
+
160
+ ---
161
+
162
+ ## 6. 钱:先知道哪一块是看不见的
163
+
164
+ 托管平台通常只记 Token,不记钱;把 Token 折成钱是**调用方的责任**,需要一份人工维护、带来源与日期的费率表。
165
+
166
+ 运行时最重要的一件事是**知道盲区在哪**:
167
+
168
+ > **不产生 issue 的节律唤醒,平台往往没有可查的用量记录。**
169
+ > 于是报表上的成本是**低报**的,而且低报的比例可能比可见部分还大。
170
+
171
+ 两条纪律:
172
+
173
+ - **缺价的模型登记成 `unpriced` 并让报表直接失败**,不要按 0 计——按 0 计会让成本看起来更便宜,是最危险的失败方式。
174
+ - 节律的 `mode` 直接决定可计量性。**改 `mode` 就是在改计量口径**,要同步改报表说明,否则会重复计或漏计。
175
+ (`dta schedule show/plan` 现在会给每条节律标 `metered`,ephemeral 不记账的会显式提示。)
176
+
177
+ ### 观测这一层,dta 不替你造——但有可抄的范式
178
+
179
+ [ARCHITECTURE §8 ⑥](ARCHITECTURE.md#8-开发生命周期从定义到观测) 讲得很直白:**用量计量(交互量、任务量、线上 token、成本、按人按群分布)dta 一项都没实现**,`dta observe` 只是人工脱敏观察绑版本、不采线上数据;执行轨迹读取属平台(薄 wrapper,换平台即失效);连完成证据的**签发**都还没接进公开 CLI(`dta task` 只有 show/checkpoint)。
180
+
181
+ 所以观测是 **Agent 自己该建的一层**。目前最完整的参考实现是「FDE 教练」,它用三样东西把自己变得可观测,都可复用:
182
+
183
+ 1. **一张运行记录表 = 每个 Run 一行,只增不改**。问了什么、答了什么、花了多久、成败、证据、issue/会话/消息硬 ID 全落进去。它是日报、审计、"昨天到底发生了什么"的唯一来源。**没有它,一次运行事后等于没发生过。**
184
+ 2. **一张费率表 + 只读成本报表**。平台只给 token 数,把 token 折成钱、按任务类型分摊,是这一层干的(缺价的模型 fail-closed,不按 0 计)。
185
+ 3. **一个元节律做体检**:定期算准 backlog、和上次快照比趋势,卡住就报主人——它监督其它节律有没有真在干活,而不是信自述。
186
+
187
+ ### 判"一次自动化到底达成没有":L0–L4,别信自述
188
+
189
+ 跑了 ≠ 干了 ≠ 干对了。用可观测证据分层,每层的证据都要能独立采到:
190
+
191
+ | 层 | 含义 | 证据 |
192
+ |---|---|---|
193
+ | L0 | 被唤醒 | 平台账本有 fire |
194
+ | L1 | 命令跑完、拿到清单 | 运行记录有一行 |
195
+ | L2/L3 | 产生写调用、且写到**正确对象** | 有写调用 + 按 recordId 回读命中 |
196
+ | L4 | 目标量指标真的动了 | backlog 净降 / 水位推进(多读取并集当下界,别单次读) |
197
+
198
+ **大多数"空转"事故都是停在 L1**:命令在跑、runlog 在记,但没有 L3 的写调用、backlog 不动。而运行记录里的自述("已处理 N 条")**不是 L3 证据**——只有真实写调用数 + 回读是。判达成度看 L4,不看自述。
199
+
200
+ ---
201
+
202
+ ## 7. 改一行怎么到线上
203
+
204
+ ```text
205
+ 改 Agent Definition 或 Skills
206
+ ↓ git commit → 发布基线分支
207
+ ↓ dta workspace inspect → dta deploy --dry-run → dta deploy --plan-id … --yes
208
+ ↓ dta deploy --status --operation-id …
209
+ 托管平台上的 Agent
210
+ ```
211
+
212
+ 三条运行时纪律:
213
+
214
+ - **目录存在 ≠ Skill 已加载。** 以部署 Receipt、Definition hash、Skill assignment hash 和加载烟测为准,
215
+ 不能用 git push 或"页面上看得到"来推断已部署。
216
+ - **`inspect → dry-run → apply` 必须连续。** plan 的 scope 从那一次 inspection 冻结;
217
+ 中间插入别的调用会得到"与冻结 plan 不一致"。该门禁在任何远端写入**之前**失败,所以报错后可以安全重试。
218
+ - **最强的验收不是烟测,是功能证据。** 烟测能证明 Skill 被装配、Agent 会应答;
219
+ 它证明不了"这次改动真的改变了行为"。后者只能靠改动本身对应的**数据现象**——
220
+ 比如修了一条补录路径,就去看下一拍有没有真的补出数据来。
221
+
222
+ **哪些改动需要部署**:Agent Definition 和 Skills 要;节律声明不要(也不能,由 operator 在平台侧执行);
223
+ 仓库开发面(README / AGENTS / docs / 评测配置)不同步。
224
+
225
+ ---
226
+
227
+ ## 8. 它不对劲的时候,按这个顺序查
228
+
229
+ | 症状 | 先看哪 | 常见真因 |
230
+ |---|---|---|
231
+ | 该发生的事没发生 | 完成判据是不是绑在"那一拍跑了没有"上 | 平台跳过了迟到的 fire |
232
+ | backlog 一直涨 | **拆入口/出口**,别看净值 | 空转、写错行、队头阻塞、产能不足,处方各不相同 |
233
+ | 值班一路报 ✅ 但数据没动 | 完成判据有没有第二处、两处判得一样吗 | 判据分叉,补救路径从没被触发 |
234
+ | 写回"成功"但状态不变 | 回读是不是独立定位了目标 | 写和回读命中同一个错误对象 |
235
+ | 同一条消息发了两遍 | 有没有两条节律落在同一分钟 | 外发类留痕在动作之后,没有锁 |
236
+ | 数字忽然变成 0 | 当成可疑,别当完成 | 后端间歇性返回空集 |
237
+ | 成本对不上账单 | 有没有不产生 issue 的唤醒 | 计量盲区 |
238
+ | 改了但线上没变 | Receipt / hash / 加载烟测 | 根本没部署,或部署了但没验证功能 |
239
+
240
+ ---
241
+
242
+ ## 8.5 这些坑该由平台兜哪些
243
+
244
+ 上面这张排查表里的每一类,都可以问一句「这是不是不该由每个 Agent 各自踩一遍」。把六类真实事故映射到 dta 的原语/CLI闸门/Skill纪律/文档四层的分析,见 [Platform Guardrails](PLATFORM-GUARDRAILS.md)——它也复核了 `dta-people-group-memory` 的四条不变式在静默截断下哪条会失效。
245
+
246
+ ## 9. 一个活的参考实现
247
+
248
+ 本仓库的 Skill 与合同都来自真实生产。目前公开可读的完整案例是「FDE 教练」——
249
+ 一个每天被节律唤醒上百次、服务上百名校招生的数字员工。
250
+ 它把本文每一节都落成了具体的表、命令和判据,并且**把踩过的坑逐条写在了注释里**:
251
+
252
+ - 运行方式全景与十条真事故换来的坑:`fde-coach-agent/docs/HOW-IT-WORKS.md`
253
+ - 节律作用域与值班完成判据:该仓库 `agent/skills/fde-heartbeat/SKILL.md`
254
+
255
+ 读本文建立框架,读它建立手感。