baxian 1.2.60 → 2.0.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.
Files changed (212) hide show
  1. package/README.md +7 -4
  2. package/README.zh-CN.md +7 -4
  3. package/dist/agent/bootstrap-poller.d.ts +1 -1
  4. package/dist/agent/bootstrap-poller.d.ts.map +1 -1
  5. package/dist/agent/bootstrap-poller.js.map +1 -1
  6. package/dist/agent/bootstrap.d.ts +1 -1
  7. package/dist/agent/bootstrap.d.ts.map +1 -1
  8. package/dist/agent/bootstrap.js +6 -2
  9. package/dist/agent/bootstrap.js.map +1 -1
  10. package/dist/agent/branch.d.ts +1 -2
  11. package/dist/agent/branch.d.ts.map +1 -1
  12. package/dist/agent/branch.js +2 -2
  13. package/dist/agent/branch.js.map +1 -1
  14. package/dist/agent/dispatch-reconciler.d.ts +0 -1
  15. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  16. package/dist/agent/dispatch-reconciler.js +11 -50
  17. package/dist/agent/dispatch-reconciler.js.map +1 -1
  18. package/dist/agent/manager.d.ts +208 -85
  19. package/dist/agent/manager.d.ts.map +1 -1
  20. package/dist/agent/manager.js +3027 -1441
  21. package/dist/agent/manager.js.map +1 -1
  22. package/dist/agent/pane-streamer.js +1 -1
  23. package/dist/agent/pane-streamer.js.map +1 -1
  24. package/dist/agent/phase-signal-watcher.d.ts +47 -2
  25. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  26. package/dist/agent/phase-signal-watcher.js +318 -49
  27. package/dist/agent/phase-signal-watcher.js.map +1 -1
  28. package/dist/agent/phase-signal.d.ts +11 -7
  29. package/dist/agent/phase-signal.d.ts.map +1 -1
  30. package/dist/agent/phase-signal.js +67 -9
  31. package/dist/agent/phase-signal.js.map +1 -1
  32. package/dist/agent/preflight.d.ts +23 -1
  33. package/dist/agent/preflight.d.ts.map +1 -1
  34. package/dist/agent/preflight.js +223 -24
  35. package/dist/agent/preflight.js.map +1 -1
  36. package/dist/agent/prompt.d.ts +16 -0
  37. package/dist/agent/prompt.d.ts.map +1 -1
  38. package/dist/agent/prompt.js +71 -11
  39. package/dist/agent/prompt.js.map +1 -1
  40. package/dist/agent/repo-store.d.ts +6 -1
  41. package/dist/agent/repo-store.d.ts.map +1 -1
  42. package/dist/agent/repo-store.js +44 -11
  43. package/dist/agent/repo-store.js.map +1 -1
  44. package/dist/agent/review-transport.d.ts +20 -1
  45. package/dist/agent/review-transport.d.ts.map +1 -1
  46. package/dist/agent/review-transport.js +68 -20
  47. package/dist/agent/review-transport.js.map +1 -1
  48. package/dist/api/config.d.ts.map +1 -1
  49. package/dist/api/config.js +20 -11
  50. package/dist/api/config.js.map +1 -1
  51. package/dist/api/hosts.d.ts.map +1 -1
  52. package/dist/api/hosts.js +3 -2
  53. package/dist/api/hosts.js.map +1 -1
  54. package/dist/api/platform-guard.d.ts +10 -2
  55. package/dist/api/platform-guard.d.ts.map +1 -1
  56. package/dist/api/platform-guard.js +112 -10
  57. package/dist/api/platform-guard.js.map +1 -1
  58. package/dist/api/projects.d.ts.map +1 -1
  59. package/dist/api/projects.js +87 -11
  60. package/dist/api/projects.js.map +1 -1
  61. package/dist/api/tasks.d.ts.map +1 -1
  62. package/dist/api/tasks.js +22 -14
  63. package/dist/api/tasks.js.map +1 -1
  64. package/dist/app.d.ts +4 -4
  65. package/dist/app.d.ts.map +1 -1
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +7 -0
  68. package/dist/cli.js.map +1 -1
  69. package/dist/config/hot-reload.d.ts +11 -2
  70. package/dist/config/hot-reload.d.ts.map +1 -1
  71. package/dist/config/hot-reload.js +43 -8
  72. package/dist/config/hot-reload.js.map +1 -1
  73. package/dist/config/loader.js +1 -1
  74. package/dist/config/loader.js.map +1 -1
  75. package/dist/config/validator.d.ts +3 -1
  76. package/dist/config/validator.d.ts.map +1 -1
  77. package/dist/config/validator.js +57 -28
  78. package/dist/config/validator.js.map +1 -1
  79. package/dist/event/handlers.d.ts.map +1 -1
  80. package/dist/event/handlers.js +616 -1067
  81. package/dist/event/handlers.js.map +1 -1
  82. package/dist/event/log.d.ts.map +1 -1
  83. package/dist/event/log.js +17 -2
  84. package/dist/event/log.js.map +1 -1
  85. package/dist/event/server-handlers.d.ts.map +1 -1
  86. package/dist/event/server-handlers.js +667 -440
  87. package/dist/event/server-handlers.js.map +1 -1
  88. package/dist/index.d.ts +4 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +55 -79
  91. package/dist/index.js.map +1 -1
  92. package/dist/platform/command-renderer.d.ts +1 -0
  93. package/dist/platform/command-renderer.d.ts.map +1 -1
  94. package/dist/platform/command-renderer.js +16 -2
  95. package/dist/platform/command-renderer.js.map +1 -1
  96. package/dist/platform/comment-cursor.d.ts +9 -0
  97. package/dist/platform/comment-cursor.d.ts.map +1 -1
  98. package/dist/platform/comment-cursor.js +63 -5
  99. package/dist/platform/comment-cursor.js.map +1 -1
  100. package/dist/platform/driver-host.d.ts.map +1 -1
  101. package/dist/platform/driver-host.js +0 -2
  102. package/dist/platform/driver-host.js.map +1 -1
  103. package/dist/platform/driver-spec.d.ts.map +1 -1
  104. package/dist/platform/driver-spec.js +50 -6
  105. package/dist/platform/driver-spec.js.map +1 -1
  106. package/dist/platform/feedback.d.ts.map +1 -1
  107. package/dist/platform/feedback.js +0 -9
  108. package/dist/platform/feedback.js.map +1 -1
  109. package/dist/platform/git-driver.d.ts +10 -1
  110. package/dist/platform/git-driver.d.ts.map +1 -1
  111. package/dist/platform/git-driver.js +78 -5
  112. package/dist/platform/git-driver.js.map +1 -1
  113. package/dist/platform/platform-poller.d.ts +26 -5
  114. package/dist/platform/platform-poller.d.ts.map +1 -1
  115. package/dist/platform/platform-poller.js +362 -34
  116. package/dist/platform/platform-poller.js.map +1 -1
  117. package/dist/platform/plugin-registry.d.ts.map +1 -1
  118. package/dist/platform/plugin-registry.js +14 -2
  119. package/dist/platform/plugin-registry.js.map +1 -1
  120. package/dist/platform/plugins/github/baxian-plugin.json +1 -1
  121. package/dist/platform/plugins/github/driver.json +17 -3
  122. package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +21 -11
  123. package/dist/platform/plugins/github/skills/baxian-cli-gh/agents/openai.yaml +2 -0
  124. package/dist/platform/pr-binding.d.ts.map +1 -1
  125. package/dist/platform/pr-binding.js +0 -2
  126. package/dist/platform/pr-binding.js.map +1 -1
  127. package/dist/{github → platform}/pr-conversation-cache.d.ts +11 -4
  128. package/dist/platform/pr-conversation-cache.d.ts.map +1 -0
  129. package/dist/platform/pr-conversation-cache.js +199 -0
  130. package/dist/platform/pr-conversation-cache.js.map +1 -0
  131. package/dist/platform/preflight-exec.d.ts +21 -0
  132. package/dist/platform/preflight-exec.d.ts.map +1 -0
  133. package/dist/platform/preflight-exec.js +62 -0
  134. package/dist/platform/preflight-exec.js.map +1 -0
  135. package/dist/platform/review-timeline.d.ts +16 -0
  136. package/dist/platform/review-timeline.d.ts.map +1 -0
  137. package/dist/platform/review-timeline.js +195 -0
  138. package/dist/platform/review-timeline.js.map +1 -0
  139. package/dist/platform/row-schema.d.ts.map +1 -1
  140. package/dist/platform/row-schema.js +1 -0
  141. package/dist/platform/row-schema.js.map +1 -1
  142. package/dist/platform/startup.d.ts +34 -2
  143. package/dist/platform/startup.d.ts.map +1 -1
  144. package/dist/platform/startup.js +107 -4
  145. package/dist/platform/startup.js.map +1 -1
  146. package/dist/platform/types.d.ts +4 -0
  147. package/dist/platform/types.d.ts.map +1 -1
  148. package/dist/platform/types.js +6 -4
  149. package/dist/platform/types.js.map +1 -1
  150. package/dist/platform/verdict-engine.d.ts.map +1 -1
  151. package/dist/platform/verdict-engine.js +0 -2
  152. package/dist/platform/verdict-engine.js.map +1 -1
  153. package/dist/shared/git-url.d.ts +1 -0
  154. package/dist/shared/git-url.d.ts.map +1 -1
  155. package/dist/shared/git-url.js +3 -0
  156. package/dist/shared/git-url.js.map +1 -1
  157. package/dist/shared/types.d.ts +90 -9
  158. package/dist/shared/types.d.ts.map +1 -1
  159. package/dist/shared/types.js.map +1 -1
  160. package/dist/skill/registry.d.ts +9 -2
  161. package/dist/skill/registry.d.ts.map +1 -1
  162. package/dist/skill/registry.js +88 -54
  163. package/dist/skill/registry.js.map +1 -1
  164. package/dist/skills/baxian-pr-feedback/SKILL.md +5 -11
  165. package/dist/skills/baxian-pr-recheck/SKILL.md +4 -43
  166. package/dist/skills/baxian-pr-review/SKILL.md +5 -42
  167. package/dist/skills/baxian-research/SKILL.md +1 -1
  168. package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
  169. package/dist/skills/baxian-signals/SKILL.md +21 -8
  170. package/dist/skills/baxian-task-check/SKILL.md +5 -5
  171. package/dist/state/agent-store.d.ts.map +1 -1
  172. package/dist/state/agent-store.js +25 -2
  173. package/dist/state/agent-store.js.map +1 -1
  174. package/dist/state/review-store.d.ts +21 -1
  175. package/dist/state/review-store.d.ts.map +1 -1
  176. package/dist/state/review-store.js +132 -0
  177. package/dist/state/review-store.js.map +1 -1
  178. package/dist/state/task-store.d.ts +6 -0
  179. package/dist/state/task-store.d.ts.map +1 -1
  180. package/dist/state/task-store.js +262 -21
  181. package/dist/state/task-store.js.map +1 -1
  182. package/dist/terminal/attach.d.ts.map +1 -1
  183. package/dist/terminal/attach.js +7 -6
  184. package/dist/terminal/attach.js.map +1 -1
  185. package/dist/web/assets/{index-CwYBVTyn.css → index-BPv6vv3U.css} +1 -1
  186. package/dist/web/assets/index-BXfgaFNm.js +18 -0
  187. package/dist/web/index.html +2 -2
  188. package/package.json +1 -1
  189. package/dist/github/mapper.d.ts +0 -9
  190. package/dist/github/mapper.d.ts.map +0 -1
  191. package/dist/github/mapper.js +0 -25
  192. package/dist/github/mapper.js.map +0 -1
  193. package/dist/github/poller.d.ts +0 -73
  194. package/dist/github/poller.d.ts.map +0 -1
  195. package/dist/github/poller.js +0 -556
  196. package/dist/github/poller.js.map +0 -1
  197. package/dist/github/pr-conversation-cache.d.ts.map +0 -1
  198. package/dist/github/pr-conversation-cache.js +0 -105
  199. package/dist/github/pr-conversation-cache.js.map +0 -1
  200. package/dist/github/pr-conversation.d.ts +0 -7
  201. package/dist/github/pr-conversation.d.ts.map +0 -1
  202. package/dist/github/pr-conversation.js +0 -175
  203. package/dist/github/pr-conversation.js.map +0 -1
  204. package/dist/github/resolver.d.ts +0 -8
  205. package/dist/github/resolver.d.ts.map +0 -1
  206. package/dist/github/resolver.js +0 -28
  207. package/dist/github/resolver.js.map +0 -1
  208. package/dist/state/post-approve-store.d.ts +0 -23
  209. package/dist/state/post-approve-store.d.ts.map +0 -1
  210. package/dist/state/post-approve-store.js +0 -82
  211. package/dist/state/post-approve-store.js.map +0 -1
  212. package/dist/web/assets/index-DtnDvjOU.js +0 -16
@@ -3,13 +3,37 @@ import { computeBackoffMs } from '../timing/backoff.js';
3
3
  import { computePollerHealth } from './poller-health.js';
4
4
  import { BRANCH_PREFIX } from '../shared/constants.js';
5
5
  import { repoIdentityKey } from '../shared/git-url.js';
6
+ import { TASK_TERMINAL_STATUS_SET } from '../shared/constants.js';
6
7
  import { DriverOpError } from './git-driver.js';
7
8
  import { versionTimeOf } from './row-schema.js';
8
9
  import { CommentCursorStore } from './comment-cursor.js';
9
10
  import { VerdictEngine } from './verdict-engine.js';
10
- import { collectValidAcks } from './markers.js';
11
+ import { collectValidAcks, rowBodyDigest } from './markers.js';
12
+ import { createHash } from 'node:crypto';
11
13
  import { buildAckCarrierRows, feedbackEventTarget, scanCommentSourcesOnce } from './feedback.js';
12
14
  import { checkPrBinding } from './pr-binding.js';
15
+ // TaskState 到轮询视图的唯一投影:poller 只看这些字段,其余任务状态一律不进轮询面。
16
+ export function platformTaskView(task) {
17
+ return {
18
+ taskId: task.id,
19
+ terminal: TASK_TERMINAL_STATUS_SET.has(task.status),
20
+ reviewMode: task.reviewMode,
21
+ status: task.status,
22
+ inReview: task.status === 'review',
23
+ closedUnmergedAnchor: task.closedUnmergedAnchor !== undefined && task.closedUnmergedAnchor.cleared !== true,
24
+ ...(task.branch !== undefined ? { branch: task.branch } : {}),
25
+ ...(task.phase !== undefined ? { phase: task.phase } : {}),
26
+ ...(task.prNumber !== undefined ? { prNumber: task.prNumber } : {}),
27
+ ...(task.reviewHeadAnchorSha !== undefined ? { anchorSha: task.reviewHeadAnchorSha } : {}),
28
+ ...(task.passToken !== undefined ? { passToken: task.passToken } : {}),
29
+ ...(task.failToken !== undefined ? { failToken: task.failToken } : {}),
30
+ ...(task.signalToken !== undefined ? { signalToken: task.signalToken } : {}),
31
+ ...(task.baseBranch !== undefined ? { expectedBase: task.baseBranch } : {}),
32
+ ...(task.latestHeadSha !== undefined ? { latestHeadSha: task.latestHeadSha } : {}),
33
+ ...(task.replyActorId !== undefined ? { replyActorId: task.replyActorId } : {}),
34
+ ...(task.replyActorStatus !== undefined ? { replyActorStatus: task.replyActorStatus } : {}),
35
+ };
36
+ }
13
37
  // 限流后必须停止请求并退避(GitHub 平台要求,持续触发 secondary limit 会招致封禁):
14
38
  // 命中即短路该 entry 本周期剩余工作,退避窗口内整周期跳过,成功周期重置。
15
39
  // abort 携带此前已入列的全部失败:混合周期(如 ACCESS_DENIED → RATE_LIMIT)的真实
@@ -31,10 +55,17 @@ class CycleFailuresError extends Error {
31
55
  this.name = 'CycleFailuresError';
32
56
  }
33
57
  }
58
+ class EntryRetiredError extends Error {
59
+ constructor() {
60
+ super('platform poller entry retired during reconciliation');
61
+ this.name = 'EntryRetiredError';
62
+ }
63
+ }
34
64
  const RATE_LIMIT_BACKOFF_MIN_MS = 60_000;
35
65
  const RATE_LIMIT_BACKOFF_MAX_MS = 900_000;
36
66
  // 与 spec §8 base 快照同一时效纪律:projectView 连续失败 ≥3 周期后缓存默认分支视为缺失。
37
67
  const DEFAULT_BRANCH_STALE_CYCLES = 3;
68
+ const ADOPTION_DEFERRAL_INTERVENTION_CYCLES = 3;
38
69
  export class PlatformPoller {
39
70
  opts;
40
71
  entries = [];
@@ -42,6 +73,7 @@ export class PlatformPoller {
42
73
  periodicRunner;
43
74
  isPolling = false;
44
75
  intervalMs;
76
+ lifecycleHook;
45
77
  now;
46
78
  constructor(opts) {
47
79
  this.opts = opts;
@@ -51,20 +83,89 @@ export class PlatformPoller {
51
83
  this.entries.push({
52
84
  ...init,
53
85
  repoKey: repoIdentityKey(init.repoUrl),
86
+ retired: false,
54
87
  status: { isPolling: false, consecutiveFailures: 0 },
55
88
  defaultBranchStale: 0,
56
89
  observedPr: new Map(),
90
+ conversationDigest: new Map(),
57
91
  deliveredVerdicts: new Map(),
58
92
  loggedUndated: new Set(),
59
93
  baseMismatch: new Map(),
94
+ adoptionDeferrals: new Map(),
60
95
  });
61
96
  }
62
- remove(projectId) {
63
- const i = this.entries.findIndex(e => e.projectId === projectId);
64
- if (i === -1)
65
- return false;
66
- this.entries.splice(i, 1);
67
- return true;
97
+ reconcile(wanted) {
98
+ const wantedByKey = this.indexWantedEntries(wanted);
99
+ this.retireUnwantedEntries(wantedByKey);
100
+ this.upsertWantedEntries(wantedByKey);
101
+ this.fireLifecycle();
102
+ }
103
+ indexWantedEntries(wanted) {
104
+ const wantedByKey = new Map();
105
+ for (const init of wanted) {
106
+ const key = repoIdentityKey(init.repoUrl);
107
+ if (!wantedByKey.has(key))
108
+ wantedByKey.set(key, init);
109
+ }
110
+ return wantedByKey;
111
+ }
112
+ retireUnwantedEntries(wantedByKey) {
113
+ for (let i = this.entries.length - 1; i >= 0; i--) {
114
+ if (!wantedByKey.has(this.entries[i].repoKey)) {
115
+ this.entries[i].retired = true;
116
+ this.entries.splice(i, 1);
117
+ }
118
+ }
119
+ }
120
+ upsertWantedEntries(wantedByKey) {
121
+ // Reusing repo-keyed entries keeps durable cursors and in-memory observations aligned across reloads.
122
+ const existingByKey = new Map(this.entries.map(e => [e.repoKey, e]));
123
+ for (const [key, init] of wantedByKey) {
124
+ const existing = existingByKey.get(key);
125
+ if (existing) {
126
+ if (existing.status.isPolling) {
127
+ existing.pendingInit = { projectId: init.projectId, driver: init.driver };
128
+ }
129
+ else {
130
+ this.applyEntryInit(existing, init);
131
+ }
132
+ }
133
+ else {
134
+ this.add(init);
135
+ }
136
+ }
137
+ }
138
+ applyEntryInit(entry, init) {
139
+ if (entry.retired)
140
+ return;
141
+ entry.projectId = init.projectId;
142
+ const samePreflight = entry.driver.preflightIdentity !== undefined
143
+ && init.driver.preflightIdentity !== undefined
144
+ ? entry.driver.preflightIdentity === init.driver.preflightIdentity
145
+ : entry.driver === init.driver;
146
+ if (!samePreflight)
147
+ entry.preflightPassed = undefined;
148
+ entry.driver = init.driver;
149
+ }
150
+ setLifecycleHook(fn) {
151
+ this.lifecycleHook = fn;
152
+ }
153
+ fireLifecycle() {
154
+ if (!this.lifecycleHook)
155
+ return;
156
+ try {
157
+ this.lifecycleHook();
158
+ }
159
+ catch (err) {
160
+ console.error('[PlatformPoller] lifecycle hook threw:', err);
161
+ }
162
+ }
163
+ // 超龄即视为缺失;纯缓存读,派发路径零网络调用。
164
+ defaultBranchSnapshot(repoKey) {
165
+ const entry = this.entries.find(e => e.repoKey === repoKey);
166
+ if (entry?.defaultBranch === undefined)
167
+ return undefined;
168
+ return entry.defaultBranchStale < DEFAULT_BRANCH_STALE_CYCLES ? entry.defaultBranch : undefined;
68
169
  }
69
170
  snapshots() {
70
171
  return this.entries.map(e => ({
@@ -98,6 +199,12 @@ export class PlatformPoller {
98
199
  }
99
200
  this.periodicRunner.start();
100
201
  }
202
+ // 热重载改 pollIntervalMs:只调整已在运行的 runner,不替一个从未 start 的 poller 启动
203
+ // (启动是 index 装配的职责)。未运行时仅记下新间隔,下次 start 生效。
204
+ reschedule(intervalMs) {
205
+ this.intervalMs = intervalMs;
206
+ this.periodicRunner?.reschedule(intervalMs);
207
+ }
101
208
  stop() {
102
209
  this.periodicRunner?.stop();
103
210
  }
@@ -110,11 +217,15 @@ export class PlatformPoller {
110
217
  }
111
218
  this.isPolling = true;
112
219
  try {
113
- for (const entry of this.entries) {
220
+ for (const entry of [...this.entries]) {
221
+ if (entry.retired || !this.entries.includes(entry))
222
+ continue;
114
223
  try {
115
224
  await this.pollOne(entry);
116
225
  }
117
226
  catch (e) {
227
+ if (e instanceof EntryRetiredError)
228
+ continue;
118
229
  console.warn(`[PlatformPoller] entry ${entry.repoKey} failed:`, e instanceof Error ? e.message : e);
119
230
  }
120
231
  }
@@ -124,11 +235,15 @@ export class PlatformPoller {
124
235
  }
125
236
  }
126
237
  async pollOne(entry) {
127
- if (entry.rateLimit !== undefined && this.now() < entry.rateLimit.until)
238
+ this.assertEntryActive(entry);
239
+ if (entry.rateLimit !== undefined && this.now() < entry.rateLimit.until) {
240
+ entry.defaultBranchStale += 1;
128
241
  return;
242
+ }
129
243
  const startedAt = this.now();
130
244
  entry.status.isPolling = true;
131
245
  entry.status.lastPollStartedAt = new Date(startedAt).toISOString();
246
+ this.fireLifecycle();
132
247
  try {
133
248
  if (entry.cursor === undefined) {
134
249
  const cursor = new CommentCursorStore(entry.statePath, entry.repoUrl);
@@ -145,6 +260,8 @@ export class PlatformPoller {
145
260
  entry.status.lastErrorClass = undefined;
146
261
  }
147
262
  catch (e) {
263
+ if (e instanceof EntryRetiredError)
264
+ return;
148
265
  if (e instanceof RateLimitAbort) {
149
266
  // 不计凭据健康度(spec §5.3 要点):退避顺延重试并在状态里如实标注 rate-limited。
150
267
  const attempt = (entry.rateLimit?.attempt ?? 0) + 1;
@@ -169,9 +286,14 @@ export class PlatformPoller {
169
286
  }
170
287
  finally {
171
288
  entry.status.isPolling = false;
289
+ if (!entry.retired && entry.pendingInit !== undefined) {
290
+ this.applyEntryInit(entry, entry.pendingInit);
291
+ entry.pendingInit = undefined;
292
+ }
172
293
  const endedAt = this.now();
173
294
  entry.status.lastPollEndedAt = new Date(endedAt).toISOString();
174
295
  entry.status.lastPollDurationMs = endedAt - startedAt;
296
+ this.fireLifecycle();
175
297
  }
176
298
  }
177
299
  recordFailure(entry, message, errorClass) {
@@ -183,6 +305,8 @@ export class PlatformPoller {
183
305
  async pollCycle(entry) {
184
306
  const failures = [];
185
307
  const fail = (context, e) => {
308
+ if (e instanceof EntryRetiredError)
309
+ throw e;
186
310
  const failure = {
187
311
  message: `${context}: ${e instanceof Error ? e.message : String(e)}`,
188
312
  errorClass: e instanceof DriverOpError ? e.info.errorClass : undefined,
@@ -193,17 +317,29 @@ export class PlatformPoller {
193
317
  };
194
318
  try {
195
319
  await entry.cursor.flushIfDirty();
320
+ this.assertEntryActive(entry);
196
321
  }
197
322
  catch (e) {
198
323
  fail('cursor flush', e);
199
324
  }
200
325
  const tasks = await this.opts.tasks(entry.projectId);
326
+ this.assertEntryActive(entry);
201
327
  const byId = new Map(tasks.map(t => [t.taskId, t]));
202
328
  for (const gen of entry.cursor.generations()) {
203
- const t = byId.get(gen);
204
- if (t === undefined || t.terminal) {
329
+ let authoritative;
330
+ try {
331
+ authoritative = await this.opts.task(gen);
332
+ this.assertEntryActive(entry);
333
+ }
334
+ catch (e) {
335
+ fail(`read task ${gen}`, e);
336
+ continue;
337
+ }
338
+ if (authoritative === null || authoritative.terminal) {
205
339
  try {
340
+ this.assertEntryActive(entry);
206
341
  await entry.cursor.dropGeneration(gen);
342
+ this.assertEntryActive(entry);
207
343
  }
208
344
  catch (e) {
209
345
  fail(`dropGeneration ${gen}`, e);
@@ -236,18 +372,54 @@ export class PlatformPoller {
236
372
  if (!activeKeys.has(key))
237
373
  entry.deliveredVerdicts.delete(key);
238
374
  }
375
+ for (const key of entry.conversationDigest.keys()) {
376
+ if (!activeKeys.has(key))
377
+ entry.conversationDigest.delete(key);
378
+ }
239
379
  for (const key of entry.loggedUndated) {
240
380
  if (!activePrs.has(Number(key.split(':')[0])))
241
381
  entry.loggedUndated.delete(key);
242
382
  }
383
+ // 首周期自证,失败进健康度周期重试;早退周期未刷新 projectView,须同步老化缓存。
384
+ if (entry.preflightPassed !== true && entry.driver.runPreflightSteps !== undefined) {
385
+ let steps;
386
+ try {
387
+ steps = await entry.driver.runPreflightSteps();
388
+ this.assertEntryActive(entry);
389
+ }
390
+ catch (e) {
391
+ entry.defaultBranchStale += 1;
392
+ fail('driver-preflight', e);
393
+ return failures;
394
+ }
395
+ const bad = steps.find(s => !s.ok);
396
+ if (bad !== undefined) {
397
+ entry.defaultBranchStale += 1;
398
+ fail(bad.step, new Error(bad.message));
399
+ return failures;
400
+ }
401
+ }
243
402
  let defaultBranch;
244
403
  try {
245
404
  const [row] = await entry.driver.runOp('projectView');
405
+ this.assertEntryActive(entry);
246
406
  if (row !== undefined && typeof row.defaultBranch === 'string') {
247
407
  entry.defaultBranch = row.defaultBranch;
248
408
  entry.defaultBranchStale = 0;
249
409
  defaultBranch = row.defaultBranch;
250
410
  }
411
+ if (row !== undefined && entry.preflightPassed !== true && entry.driver.runPreflightSteps !== undefined) {
412
+ if (row.pushPermitted === false) {
413
+ fail('preflight-push', new Error(`push permission missing for ${entry.repoUrl} — server merge/close requires write access`));
414
+ return failures;
415
+ }
416
+ else {
417
+ if (row.pushPermitted === undefined) {
418
+ console.warn(`[PlatformPoller] ${entry.repoKey}: plugin did not report pushPermitted — write access cannot be asserted`);
419
+ }
420
+ entry.preflightPassed = true;
421
+ }
422
+ }
251
423
  }
252
424
  catch (e) {
253
425
  entry.defaultBranchStale += 1;
@@ -260,6 +432,7 @@ export class PlatformPoller {
260
432
  ? entry.defaultBranch
261
433
  : undefined);
262
434
  await this.pollListPrs(entry, tasks, byId, defaultBranch, fail, failures);
435
+ this.assertEntryActive(entry);
263
436
  for (const task of tasks) {
264
437
  if (task.terminal || task.prNumber === undefined)
265
438
  continue;
@@ -268,6 +441,7 @@ export class PlatformPoller {
268
441
  if (task.closedUnmergedAnchor === true)
269
442
  continue;
270
443
  await this.subPoll(entry, task, usableDefault, fail);
444
+ this.assertEntryActive(entry);
271
445
  }
272
446
  return failures;
273
447
  }
@@ -299,19 +473,56 @@ export class PlatformPoller {
299
473
  return vt !== undefined && vt < prsCursor.watermarkTime;
300
474
  });
301
475
  });
476
+ this.assertEntryActive(entry);
302
477
  }
303
478
  catch (e) {
304
479
  fail('listPrs', e);
305
480
  return;
306
481
  }
482
+ const listedPrRows = [...prRows];
483
+ const pendingByPr = new Map();
484
+ const activeDeferrals = new Set();
485
+ for (const pending of entry.cursor.pendingAdoptions()) {
486
+ let task;
487
+ try {
488
+ task = await this.opts.task(pending.taskId);
489
+ this.assertEntryActive(entry);
490
+ if (task === null || task.terminal || (task.prNumber !== undefined && task.prNumber !== pending.prNumber)) {
491
+ await entry.cursor.clearAdoptionPending(pending.taskId, pending.prNumber);
492
+ continue;
493
+ }
494
+ if (task.prNumber === pending.prNumber) {
495
+ await entry.cursor.markAdoptionDelivered(pending.taskId, pending.prNumber);
496
+ continue;
497
+ }
498
+ byId.set(task.taskId, task);
499
+ pendingByPr.set(pending.prNumber, pending.taskId);
500
+ if (prRows.some(row => row.prNumber === pending.prNumber))
501
+ continue;
502
+ const [row] = await entry.driver.runOp('prView', { prNumber: pending.prNumber });
503
+ this.assertEntryActive(entry);
504
+ if (row !== undefined)
505
+ prRows.push({ ...row, prNumber: pending.prNumber });
506
+ }
507
+ catch (e) {
508
+ activeDeferrals.add(pending.prNumber);
509
+ fail(`retry adoption pr#${pending.prNumber}`, e);
510
+ }
511
+ }
307
512
  const boundByPr = new Map();
308
513
  for (const t of tasks) {
309
514
  if (!t.terminal && t.prNumber !== undefined)
310
515
  boundByPr.set(t.prNumber, t);
311
516
  }
312
517
  const failuresBefore = failures.length;
313
- let deferredAdoptions = 0;
314
518
  const seenPrs = new Set();
519
+ const clearPending = async (prNumber) => {
520
+ const taskId = pendingByPr.get(prNumber);
521
+ if (taskId === undefined)
522
+ return;
523
+ await entry.cursor.clearAdoptionPending(taskId, prNumber);
524
+ pendingByPr.delete(prNumber);
525
+ };
315
526
  for (const row of prRows) {
316
527
  const prNumber = row.prNumber;
317
528
  seenPrs.add(prNumber);
@@ -322,10 +533,11 @@ export class PlatformPoller {
322
533
  };
323
534
  const boundTask = boundByPr.get(prNumber);
324
535
  if (boundTask !== undefined) {
536
+ await clearPending(prNumber);
325
537
  // reopen 是显式 durable 观察:closed-unmerged 锚在 TaskState,主轮询见重开即合成(spec §6)。
326
538
  if (boundTask.closedUnmergedAnchor === true && row.state === 'open') {
327
539
  try {
328
- await this.emit(entry, 'pr.updated', { ...base, kind: 'reopened' });
540
+ await this.emit(entry, 'pr.updated', { ...base, kind: 'reopened' }, boundTask.taskId);
329
541
  entry.observedPr.delete(`${boundTask.taskId}:${prNumber}`);
330
542
  }
331
543
  catch (e) {
@@ -334,31 +546,48 @@ export class PlatformPoller {
334
546
  }
335
547
  continue;
336
548
  }
337
- if (row.state !== 'open' || row.draft === true)
549
+ if (row.state !== 'open' || row.draft === true) {
550
+ await clearPending(prNumber);
338
551
  continue;
339
- if (!base.branch.startsWith(BRANCH_PREFIX))
552
+ }
553
+ if (!base.branch.startsWith(BRANCH_PREFIX)) {
554
+ await clearPending(prNumber);
340
555
  continue;
556
+ }
341
557
  const task = byId.get(base.branch.slice(BRANCH_PREFIX.length));
342
- if (task === undefined || task.terminal || task.prNumber !== undefined)
558
+ if (task === undefined || task.terminal || task.prNumber !== undefined) {
559
+ await clearPending(prNumber);
343
560
  continue;
561
+ }
562
+ const pendingTaskId = pendingByPr.get(prNumber);
563
+ if (pendingTaskId !== undefined && pendingTaskId !== task.taskId)
564
+ await clearPending(prNumber);
344
565
  // 统一谓词 branch === task.branch:branch 缺失的任务视图无从核验绑定,收编一并 fail closed
345
566
  // (与子轮询 bindingCheck 对称——「无法核验」不是「匹配」)。
346
- if (task.branch !== base.branch)
567
+ if (task.branch !== base.branch) {
568
+ await clearPending(prNumber);
347
569
  continue;
570
+ }
348
571
  if (row.sourceProjectId === null || row.sourceProjectId === undefined
349
- || row.sourceProjectId !== row.targetProjectId)
572
+ || row.sourceProjectId !== row.targetProjectId) {
573
+ await clearPending(prNumber);
350
574
  continue;
575
+ }
351
576
  const target = String(row.targetBranch);
577
+ if (entry.cursor.isAdoptionDelivered(task.taskId, prNumber))
578
+ continue;
352
579
  try {
353
580
  if (task.expectedBase !== undefined) {
354
- if (target !== task.expectedBase)
581
+ if (target !== task.expectedBase) {
582
+ await clearPending(prNumber);
355
583
  continue;
584
+ }
356
585
  }
357
586
  else {
358
587
  if (defaultBranch === undefined) {
359
- // 收编推迟必须同时冻结水位:否则该 PR 被水位判「已处理」,projectView 恢复后
360
- // 它已落在停止页之外、永不再被扫描。
361
- deferredAdoptions += 1;
588
+ await entry.cursor.markAdoptionPending(task.taskId, prNumber);
589
+ pendingByPr.set(prNumber, task.taskId);
590
+ activeDeferrals.add(prNumber);
362
591
  continue;
363
592
  }
364
593
  if (target !== defaultBranch) {
@@ -368,17 +597,61 @@ export class PlatformPoller {
368
597
  if (entry.baseMismatch.get(prNumber) !== fp) {
369
598
  await this.emit(entry, 'human.intervention', {
370
599
  ...base, reason: 'base-mismatch', targetBranch: target, expectedBase: defaultBranch, taskId: task.taskId,
371
- });
600
+ }, task.taskId);
372
601
  entry.baseMismatch.set(prNumber, fp);
373
602
  }
603
+ await clearPending(prNumber);
374
604
  continue;
375
605
  }
376
606
  }
607
+ const adoptionFingerprint = [
608
+ task.taskId,
609
+ task.status ?? '',
610
+ task.phase ?? '',
611
+ task.signalToken ?? '',
612
+ task.reviewMode,
613
+ base.branch,
614
+ target,
615
+ String(row.headSha),
616
+ ].join('\0');
617
+ const deferred = entry.adoptionDeferrals.get(prNumber);
618
+ if (deferred?.fingerprint === adoptionFingerprint) {
619
+ deferred.cycles += 1;
620
+ activeDeferrals.add(prNumber);
621
+ if (!deferred.interventionDelivered
622
+ && deferred.cycles >= ADOPTION_DEFERRAL_INTERVENTION_CYCLES) {
623
+ await this.emit(entry, 'human.intervention', {
624
+ ...base,
625
+ reason: 'pr-adoption-deferred',
626
+ taskId: task.taskId,
627
+ status: task.status,
628
+ phase: task.phase,
629
+ cycles: deferred.cycles,
630
+ }, task.taskId);
631
+ deferred.interventionDelivered = true;
632
+ }
633
+ continue;
634
+ }
635
+ entry.adoptionDeferrals.delete(prNumber);
377
636
  await this.emit(entry, 'pr.created', {
378
637
  ...base, headSha: String(row.headSha), targetBranch: target,
379
638
  ...(typeof row.prAuthorId === 'string' ? { prAuthorId: row.prAuthorId } : {}),
380
- });
639
+ }, task.taskId);
640
+ const adoptedTask = await this.opts.task(task.taskId);
641
+ if (adoptedTask?.prNumber !== prNumber) {
642
+ await entry.cursor.markAdoptionPending(task.taskId, prNumber);
643
+ pendingByPr.set(prNumber, task.taskId);
644
+ entry.adoptionDeferrals.set(prNumber, {
645
+ fingerprint: adoptionFingerprint,
646
+ cycles: 1,
647
+ interventionDelivered: false,
648
+ });
649
+ activeDeferrals.add(prNumber);
650
+ continue;
651
+ }
652
+ await entry.cursor.markAdoptionDelivered(task.taskId, prNumber);
381
653
  entry.baseMismatch.delete(prNumber);
654
+ entry.adoptionDeferrals.delete(prNumber);
382
655
  }
383
656
  catch (e) {
384
657
  fail(`adopt pr#${prNumber}`, e);
@@ -388,10 +661,15 @@ export class PlatformPoller {
388
661
  if (!seenPrs.has(pr))
389
662
  entry.baseMismatch.delete(pr);
390
663
  }
391
- if (failures.length > failuresBefore || deferredAdoptions > 0)
664
+ for (const pr of entry.adoptionDeferrals.keys()) {
665
+ if (!activeDeferrals.has(pr))
666
+ entry.adoptionDeferrals.delete(pr);
667
+ }
668
+ if (failures.length > failuresBefore)
392
669
  return;
393
670
  try {
394
- await entry.cursor.commitListPrs(prRows, scanStartedAt - entry.driver.visibilityLagMs);
671
+ this.assertEntryActive(entry);
672
+ await entry.cursor.commitListPrs(listedPrRows, scanStartedAt - entry.driver.visibilityLagMs);
395
673
  }
396
674
  catch (e) {
397
675
  fail('listPrs cursor commit', e);
@@ -403,6 +681,7 @@ export class PlatformPoller {
403
681
  let prRow;
404
682
  try {
405
683
  [prRow] = await entry.driver.runOp('prView', { prNumber });
684
+ this.assertEntryActive(entry);
406
685
  }
407
686
  catch (e) {
408
687
  fail(`prView pr#${prNumber}`, e);
@@ -425,7 +704,7 @@ export class PlatformPoller {
425
704
  taskId: task.taskId,
426
705
  targetBranch: String(prRow.targetBranch),
427
706
  ...(task.expectedBase !== undefined ? { expectedBase: task.expectedBase } : {}),
428
- });
707
+ }, task.taskId);
429
708
  observed.bindingMismatch = binding.mismatch;
430
709
  }
431
710
  catch (e) {
@@ -440,7 +719,7 @@ export class PlatformPoller {
440
719
  if (typeof prRow.mergedAt === 'string') {
441
720
  if (observed.merged !== true) {
442
721
  try {
443
- await this.emit(entry, 'pr.merged', base);
722
+ await this.emit(entry, 'pr.merged', base, task.taskId);
444
723
  observed.merged = true;
445
724
  }
446
725
  catch (e) {
@@ -450,7 +729,7 @@ export class PlatformPoller {
450
729
  }
451
730
  else if (observed.closedUnmerged !== true) {
452
731
  try {
453
- await this.emit(entry, 'pr.updated', { ...base, kind: 'closed-unmerged' });
732
+ await this.emit(entry, 'pr.updated', { ...base, kind: 'closed-unmerged' }, task.taskId);
454
733
  observed.closedUnmerged = true;
455
734
  }
456
735
  catch (e) {
@@ -461,10 +740,14 @@ export class PlatformPoller {
461
740
  return;
462
741
  }
463
742
  observed.closedUnmerged = false;
743
+ if (task.reviewMode === 'server') {
744
+ this.engine.dropCandidate(task.taskId, prNumber);
745
+ return;
746
+ }
464
747
  const headSha = String(prRow.headSha);
465
748
  if (task.latestHeadSha !== undefined && headSha !== task.latestHeadSha.toLowerCase() && observed.pushSha !== headSha) {
466
749
  try {
467
- await this.emit(entry, 'pr.updated', { ...base, kind: 'push', headSha });
750
+ await this.emit(entry, 'pr.updated', { ...base, kind: 'push', headSha }, task.taskId);
468
751
  observed.pushSha = headSha;
469
752
  }
470
753
  catch (e) {
@@ -472,6 +755,7 @@ export class PlatformPoller {
472
755
  }
473
756
  }
474
757
  const scans = await this.scanCommentSources(entry, task, base, fail);
758
+ await this.noteConversationProjection(entry, task.taskId, obsKey, scans);
475
759
  const anchorSha = task.anchorSha?.toLowerCase();
476
760
  // 裁决资格门:head 偏离锚点只能走 push/recheck,旧锚令牌不得批准新 head;signalToken
477
761
  // 缺失**或空白**的事件会绕过既有 handler 的令牌比对(guard 是 truthy 双与,两个空值照样
@@ -505,6 +789,7 @@ export class PlatformPoller {
505
789
  // 评论源扫描最长可达分钟级,扫描期间 head/状态可被平台改写(TOCTOU):emit 前以
506
790
  // 权威 prView 复核 open/!draft/绑定/锚点,失配清候选走 push/recheck,查询失败按瞬态重试。
507
791
  const [freshRow] = await entry.driver.runOp('prView', { prNumber });
792
+ this.assertEntryActive(entry);
508
793
  const freshEligible = freshRow !== undefined
509
794
  && this.bindingCheck(task, freshRow, defaultBranch) === undefined
510
795
  && freshRow.state === 'open' && freshRow.draft === false
@@ -515,6 +800,7 @@ export class PlatformPoller {
515
800
  }
516
801
  await this.emit(entry, 'review.submitted', {
517
802
  ...base,
803
+ source: 'platform-poller',
518
804
  action: decision.kind === 'pass' ? 'APPROVE' : 'REQUEST_CHANGES',
519
805
  headSha: decision.anchorSha,
520
806
  currentHeadSha: String(freshRow.headSha),
@@ -523,13 +809,49 @@ export class PlatformPoller {
523
809
  verdictToken: decision.token,
524
810
  verdictConflict: decision.conflict,
525
811
  verdictCarrier: decision.carrier,
526
- });
812
+ }, task.taskId);
527
813
  entry.deliveredVerdicts.set(obsKey, fp);
528
814
  }
529
815
  catch (e) {
530
816
  fail(`verdict pr#${prNumber}`, e);
531
817
  }
532
818
  }
819
+ // 全源成功才比对(失败源缺行会伪刷新两次);回调失败不推进基线,下轮重试。
820
+ async noteConversationProjection(entry, taskId, obsKey, scans) {
821
+ const notify = this.opts.onConversationRevision;
822
+ if (notify === undefined)
823
+ return;
824
+ if (scans.length === 0 || scans.some(s => !s.ok))
825
+ return;
826
+ const h = createHash('sha256');
827
+ for (const scan of [...scans].sort((a, b) => a.key.localeCompare(b.key))) {
828
+ // 源未承诺稳定行序:按行身份规范化后再散列,重排不产生伪 revision。
829
+ const rows = [...scan.rows].sort((a, b) => {
830
+ const ai = String(a.id);
831
+ const bi = String(b.id);
832
+ if (ai !== bi)
833
+ return ai < bi ? -1 : 1;
834
+ const ad = rowBodyDigest(a);
835
+ const bd = rowBodyDigest(b);
836
+ return ad < bd ? -1 : ad > bd ? 1 : 0;
837
+ });
838
+ for (const row of rows) {
839
+ // versionTime 入摘要:正文相同但 updatedAt 变化会改变时间线排序(进而改变分轮),
840
+ // 不计入就永远不推进展示 revision。
841
+ h.update(`${scan.key}:${String(row.id)}:${rowBodyDigest(row)}:${String(row.reviewState ?? '')}:${String(versionTimeOf(row) ?? '')}\n`);
842
+ }
843
+ }
844
+ const digest = h.digest('hex');
845
+ if (entry.conversationDigest.get(obsKey) === digest)
846
+ return;
847
+ try {
848
+ await notify(taskId);
849
+ entry.conversationDigest.set(obsKey, digest);
850
+ }
851
+ catch (e) {
852
+ console.warn('[PlatformPoller] onConversationRevision failed:', e instanceof Error ? e.message : e);
853
+ }
854
+ }
533
855
  async scanCommentSources(entry, task, base, fail) {
534
856
  const scans = await scanCommentSourcesOnce(entry.driver, base.prNumber, this.now, (key, error) => fail(`listComments[${key}] pr#${base.prNumber}`, error));
535
857
  // ack 完成集跨源计算一次:issue 类顶层 ack 可指向任意源的 revision,threaded 隶属
@@ -569,7 +891,7 @@ export class PlatformPoller {
569
891
  ...(scan.sourceClass === 'threaded' && row.discussionId !== null && row.discussionId !== undefined
570
892
  ? { reviewCommentReply: true }
571
893
  : {}),
572
- });
894
+ }, task.taskId);
573
895
  await entry.cursor.markDelivered(task.taskId, base.prNumber, scan.key, id, digest);
574
896
  }
575
897
  catch (e) {
@@ -579,6 +901,7 @@ export class PlatformPoller {
579
901
  }
580
902
  if (delivered) {
581
903
  try {
904
+ this.assertEntryActive(entry);
582
905
  await entry.cursor.commitSource(task.taskId, base.prNumber, scan.key, scan.rows, cutoff);
583
906
  }
584
907
  catch (e) {
@@ -598,8 +921,13 @@ export class PlatformPoller {
598
921
  }
599
922
  return scans;
600
923
  }
601
- async emit(entry, type, data) {
602
- await this.opts.onEvent(entry.projectId, { type, repo: entry.repoKey, data });
924
+ async emit(entry, type, data, taskId) {
925
+ this.assertEntryActive(entry);
926
+ await this.opts.onEvent(entry.projectId, { type, repo: entry.repoKey, data, taskId });
927
+ }
928
+ assertEntryActive(entry) {
929
+ if (entry.retired || !this.entries.includes(entry))
930
+ throw new EntryRetiredError();
603
931
  }
604
932
  }
605
933
  //# sourceMappingURL=platform-poller.js.map