baxian 1.2.61 → 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 (178) 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 +5 -44
  17. package/dist/agent/dispatch-reconciler.js.map +1 -1
  18. package/dist/agent/manager.d.ts +175 -82
  19. package/dist/agent/manager.d.ts.map +1 -1
  20. package/dist/agent/manager.js +2453 -1447
  21. package/dist/agent/manager.js.map +1 -1
  22. package/dist/agent/phase-signal-watcher.d.ts +5 -1
  23. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  24. package/dist/agent/phase-signal-watcher.js +22 -11
  25. package/dist/agent/phase-signal-watcher.js.map +1 -1
  26. package/dist/agent/phase-signal.d.ts +2 -7
  27. package/dist/agent/phase-signal.d.ts.map +1 -1
  28. package/dist/agent/phase-signal.js +4 -4
  29. package/dist/agent/phase-signal.js.map +1 -1
  30. package/dist/agent/preflight.d.ts +4 -1
  31. package/dist/agent/preflight.d.ts.map +1 -1
  32. package/dist/agent/preflight.js +46 -45
  33. package/dist/agent/preflight.js.map +1 -1
  34. package/dist/agent/prompt.d.ts +8 -0
  35. package/dist/agent/prompt.d.ts.map +1 -1
  36. package/dist/agent/prompt.js +30 -13
  37. package/dist/agent/prompt.js.map +1 -1
  38. package/dist/agent/repo-store.d.ts +4 -0
  39. package/dist/agent/repo-store.d.ts.map +1 -1
  40. package/dist/agent/repo-store.js +37 -8
  41. package/dist/agent/repo-store.js.map +1 -1
  42. package/dist/agent/review-transport.d.ts +20 -1
  43. package/dist/agent/review-transport.d.ts.map +1 -1
  44. package/dist/agent/review-transport.js +68 -20
  45. package/dist/agent/review-transport.js.map +1 -1
  46. package/dist/api/config.d.ts.map +1 -1
  47. package/dist/api/config.js +12 -11
  48. package/dist/api/config.js.map +1 -1
  49. package/dist/api/hosts.d.ts.map +1 -1
  50. package/dist/api/hosts.js +3 -2
  51. package/dist/api/hosts.js.map +1 -1
  52. package/dist/api/platform-guard.d.ts +10 -2
  53. package/dist/api/platform-guard.d.ts.map +1 -1
  54. package/dist/api/platform-guard.js +112 -10
  55. package/dist/api/platform-guard.js.map +1 -1
  56. package/dist/api/projects.d.ts.map +1 -1
  57. package/dist/api/projects.js +34 -9
  58. package/dist/api/projects.js.map +1 -1
  59. package/dist/api/tasks.d.ts.map +1 -1
  60. package/dist/api/tasks.js +21 -36
  61. package/dist/api/tasks.js.map +1 -1
  62. package/dist/app.d.ts +4 -4
  63. package/dist/app.d.ts.map +1 -1
  64. package/dist/config/hot-reload.d.ts +11 -2
  65. package/dist/config/hot-reload.d.ts.map +1 -1
  66. package/dist/config/hot-reload.js +43 -8
  67. package/dist/config/hot-reload.js.map +1 -1
  68. package/dist/config/loader.js +1 -1
  69. package/dist/config/loader.js.map +1 -1
  70. package/dist/config/validator.d.ts +3 -1
  71. package/dist/config/validator.d.ts.map +1 -1
  72. package/dist/config/validator.js +53 -31
  73. package/dist/config/validator.js.map +1 -1
  74. package/dist/event/handlers.d.ts.map +1 -1
  75. package/dist/event/handlers.js +616 -1067
  76. package/dist/event/handlers.js.map +1 -1
  77. package/dist/event/server-handlers.d.ts.map +1 -1
  78. package/dist/event/server-handlers.js +667 -440
  79. package/dist/event/server-handlers.js.map +1 -1
  80. package/dist/index.d.ts +4 -1
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +54 -79
  83. package/dist/index.js.map +1 -1
  84. package/dist/platform/command-renderer.d.ts +1 -0
  85. package/dist/platform/command-renderer.d.ts.map +1 -1
  86. package/dist/platform/command-renderer.js +16 -2
  87. package/dist/platform/command-renderer.js.map +1 -1
  88. package/dist/platform/comment-cursor.d.ts +9 -0
  89. package/dist/platform/comment-cursor.d.ts.map +1 -1
  90. package/dist/platform/comment-cursor.js +63 -5
  91. package/dist/platform/comment-cursor.js.map +1 -1
  92. package/dist/platform/driver-host.d.ts.map +1 -1
  93. package/dist/platform/driver-host.js +0 -2
  94. package/dist/platform/driver-host.js.map +1 -1
  95. package/dist/platform/driver-spec.d.ts.map +1 -1
  96. package/dist/platform/driver-spec.js +17 -6
  97. package/dist/platform/driver-spec.js.map +1 -1
  98. package/dist/platform/feedback.d.ts.map +1 -1
  99. package/dist/platform/feedback.js +0 -9
  100. package/dist/platform/feedback.js.map +1 -1
  101. package/dist/platform/git-driver.d.ts +3 -0
  102. package/dist/platform/git-driver.d.ts.map +1 -1
  103. package/dist/platform/git-driver.js +45 -3
  104. package/dist/platform/git-driver.js.map +1 -1
  105. package/dist/platform/platform-poller.d.ts +18 -5
  106. package/dist/platform/platform-poller.d.ts.map +1 -1
  107. package/dist/platform/platform-poller.js +282 -33
  108. package/dist/platform/platform-poller.js.map +1 -1
  109. package/dist/platform/plugins/github/baxian-plugin.json +1 -1
  110. package/dist/platform/plugins/github/driver.json +14 -3
  111. package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +7 -6
  112. package/dist/platform/plugins/github/skills/baxian-cli-gh/agents/openai.yaml +2 -0
  113. package/dist/platform/pr-binding.d.ts.map +1 -1
  114. package/dist/platform/pr-binding.js +0 -2
  115. package/dist/platform/pr-binding.js.map +1 -1
  116. package/dist/platform/pr-conversation-cache.d.ts.map +1 -0
  117. package/dist/platform/pr-conversation-cache.js.map +1 -0
  118. package/dist/platform/row-schema.d.ts.map +1 -1
  119. package/dist/platform/row-schema.js +1 -0
  120. package/dist/platform/row-schema.js.map +1 -1
  121. package/dist/platform/startup.d.ts +29 -1
  122. package/dist/platform/startup.d.ts.map +1 -1
  123. package/dist/platform/startup.js +82 -4
  124. package/dist/platform/startup.js.map +1 -1
  125. package/dist/platform/types.d.ts +2 -0
  126. package/dist/platform/types.d.ts.map +1 -1
  127. package/dist/platform/types.js +6 -4
  128. package/dist/platform/types.js.map +1 -1
  129. package/dist/platform/verdict-engine.d.ts.map +1 -1
  130. package/dist/platform/verdict-engine.js +0 -2
  131. package/dist/platform/verdict-engine.js.map +1 -1
  132. package/dist/shared/types.d.ts +73 -5
  133. package/dist/shared/types.d.ts.map +1 -1
  134. package/dist/shared/types.js.map +1 -1
  135. package/dist/skills/baxian-pr-feedback/SKILL.md +4 -12
  136. package/dist/skills/baxian-pr-recheck/SKILL.md +3 -44
  137. package/dist/skills/baxian-pr-review/SKILL.md +4 -43
  138. package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
  139. package/dist/skills/baxian-signals/SKILL.md +5 -7
  140. package/dist/skills/baxian-task-check/SKILL.md +3 -5
  141. package/dist/state/agent-store.js +0 -2
  142. package/dist/state/agent-store.js.map +1 -1
  143. package/dist/state/review-store.d.ts +21 -1
  144. package/dist/state/review-store.d.ts.map +1 -1
  145. package/dist/state/review-store.js +132 -0
  146. package/dist/state/review-store.js.map +1 -1
  147. package/dist/state/task-store.d.ts +6 -0
  148. package/dist/state/task-store.d.ts.map +1 -1
  149. package/dist/state/task-store.js +259 -18
  150. package/dist/state/task-store.js.map +1 -1
  151. package/dist/web/assets/index-BXfgaFNm.js +18 -0
  152. package/dist/web/index.html +1 -1
  153. package/package.json +1 -1
  154. package/dist/github/mapper.d.ts +0 -9
  155. package/dist/github/mapper.d.ts.map +0 -1
  156. package/dist/github/mapper.js +0 -25
  157. package/dist/github/mapper.js.map +0 -1
  158. package/dist/github/poller.d.ts +0 -73
  159. package/dist/github/poller.d.ts.map +0 -1
  160. package/dist/github/poller.js +0 -556
  161. package/dist/github/poller.js.map +0 -1
  162. package/dist/github/pr-conversation-cache.d.ts.map +0 -1
  163. package/dist/github/pr-conversation-cache.js.map +0 -1
  164. package/dist/github/pr-conversation.d.ts +0 -7
  165. package/dist/github/pr-conversation.d.ts.map +0 -1
  166. package/dist/github/pr-conversation.js +0 -175
  167. package/dist/github/pr-conversation.js.map +0 -1
  168. package/dist/github/resolver.d.ts +0 -8
  169. package/dist/github/resolver.d.ts.map +0 -1
  170. package/dist/github/resolver.js +0 -28
  171. package/dist/github/resolver.js.map +0 -1
  172. package/dist/state/post-approve-store.d.ts +0 -23
  173. package/dist/state/post-approve-store.d.ts.map +0 -1
  174. package/dist/state/post-approve-store.js +0 -82
  175. package/dist/state/post-approve-store.js.map +0 -1
  176. package/dist/web/assets/index-CUJtd0bC.js +0 -16
  177. /package/dist/{github → platform}/pr-conversation-cache.d.ts +0 -0
  178. /package/dist/{github → platform}/pr-conversation-cache.js +0 -0
@@ -3,6 +3,7 @@ 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';
@@ -11,6 +12,28 @@ import { collectValidAcks, rowBodyDigest } from './markers.js';
11
12
  import { createHash } from 'node:crypto';
12
13
  import { buildAckCarrierRows, feedbackEventTarget, scanCommentSourcesOnce } from './feedback.js';
13
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
+ }
14
37
  // 限流后必须停止请求并退避(GitHub 平台要求,持续触发 secondary limit 会招致封禁):
15
38
  // 命中即短路该 entry 本周期剩余工作,退避窗口内整周期跳过,成功周期重置。
16
39
  // abort 携带此前已入列的全部失败:混合周期(如 ACCESS_DENIED → RATE_LIMIT)的真实
@@ -32,10 +55,17 @@ class CycleFailuresError extends Error {
32
55
  this.name = 'CycleFailuresError';
33
56
  }
34
57
  }
58
+ class EntryRetiredError extends Error {
59
+ constructor() {
60
+ super('platform poller entry retired during reconciliation');
61
+ this.name = 'EntryRetiredError';
62
+ }
63
+ }
35
64
  const RATE_LIMIT_BACKOFF_MIN_MS = 60_000;
36
65
  const RATE_LIMIT_BACKOFF_MAX_MS = 900_000;
37
66
  // 与 spec §8 base 快照同一时效纪律:projectView 连续失败 ≥3 周期后缓存默认分支视为缺失。
38
67
  const DEFAULT_BRANCH_STALE_CYCLES = 3;
68
+ const ADOPTION_DEFERRAL_INTERVENTION_CYCLES = 3;
39
69
  export class PlatformPoller {
40
70
  opts;
41
71
  entries = [];
@@ -43,6 +73,7 @@ export class PlatformPoller {
43
73
  periodicRunner;
44
74
  isPolling = false;
45
75
  intervalMs;
76
+ lifecycleHook;
46
77
  now;
47
78
  constructor(opts) {
48
79
  this.opts = opts;
@@ -52,6 +83,7 @@ export class PlatformPoller {
52
83
  this.entries.push({
53
84
  ...init,
54
85
  repoKey: repoIdentityKey(init.repoUrl),
86
+ retired: false,
55
87
  status: { isPolling: false, consecutiveFailures: 0 },
56
88
  defaultBranchStale: 0,
57
89
  observedPr: new Map(),
@@ -59,14 +91,74 @@ export class PlatformPoller {
59
91
  deliveredVerdicts: new Map(),
60
92
  loggedUndated: new Set(),
61
93
  baseMismatch: new Map(),
94
+ adoptionDeferrals: new Map(),
62
95
  });
63
96
  }
64
- remove(projectId) {
65
- const i = this.entries.findIndex(e => e.projectId === projectId);
66
- if (i === -1)
67
- return false;
68
- this.entries.splice(i, 1);
69
- 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
+ }
70
162
  }
71
163
  // 超龄即视为缺失;纯缓存读,派发路径零网络调用。
72
164
  defaultBranchSnapshot(repoKey) {
@@ -107,6 +199,12 @@ export class PlatformPoller {
107
199
  }
108
200
  this.periodicRunner.start();
109
201
  }
202
+ // 热重载改 pollIntervalMs:只调整已在运行的 runner,不替一个从未 start 的 poller 启动
203
+ // (启动是 index 装配的职责)。未运行时仅记下新间隔,下次 start 生效。
204
+ reschedule(intervalMs) {
205
+ this.intervalMs = intervalMs;
206
+ this.periodicRunner?.reschedule(intervalMs);
207
+ }
110
208
  stop() {
111
209
  this.periodicRunner?.stop();
112
210
  }
@@ -119,11 +217,15 @@ export class PlatformPoller {
119
217
  }
120
218
  this.isPolling = true;
121
219
  try {
122
- for (const entry of this.entries) {
220
+ for (const entry of [...this.entries]) {
221
+ if (entry.retired || !this.entries.includes(entry))
222
+ continue;
123
223
  try {
124
224
  await this.pollOne(entry);
125
225
  }
126
226
  catch (e) {
227
+ if (e instanceof EntryRetiredError)
228
+ continue;
127
229
  console.warn(`[PlatformPoller] entry ${entry.repoKey} failed:`, e instanceof Error ? e.message : e);
128
230
  }
129
231
  }
@@ -133,11 +235,15 @@ export class PlatformPoller {
133
235
  }
134
236
  }
135
237
  async pollOne(entry) {
136
- 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;
137
241
  return;
242
+ }
138
243
  const startedAt = this.now();
139
244
  entry.status.isPolling = true;
140
245
  entry.status.lastPollStartedAt = new Date(startedAt).toISOString();
246
+ this.fireLifecycle();
141
247
  try {
142
248
  if (entry.cursor === undefined) {
143
249
  const cursor = new CommentCursorStore(entry.statePath, entry.repoUrl);
@@ -154,6 +260,8 @@ export class PlatformPoller {
154
260
  entry.status.lastErrorClass = undefined;
155
261
  }
156
262
  catch (e) {
263
+ if (e instanceof EntryRetiredError)
264
+ return;
157
265
  if (e instanceof RateLimitAbort) {
158
266
  // 不计凭据健康度(spec §5.3 要点):退避顺延重试并在状态里如实标注 rate-limited。
159
267
  const attempt = (entry.rateLimit?.attempt ?? 0) + 1;
@@ -178,9 +286,14 @@ export class PlatformPoller {
178
286
  }
179
287
  finally {
180
288
  entry.status.isPolling = false;
289
+ if (!entry.retired && entry.pendingInit !== undefined) {
290
+ this.applyEntryInit(entry, entry.pendingInit);
291
+ entry.pendingInit = undefined;
292
+ }
181
293
  const endedAt = this.now();
182
294
  entry.status.lastPollEndedAt = new Date(endedAt).toISOString();
183
295
  entry.status.lastPollDurationMs = endedAt - startedAt;
296
+ this.fireLifecycle();
184
297
  }
185
298
  }
186
299
  recordFailure(entry, message, errorClass) {
@@ -192,6 +305,8 @@ export class PlatformPoller {
192
305
  async pollCycle(entry) {
193
306
  const failures = [];
194
307
  const fail = (context, e) => {
308
+ if (e instanceof EntryRetiredError)
309
+ throw e;
195
310
  const failure = {
196
311
  message: `${context}: ${e instanceof Error ? e.message : String(e)}`,
197
312
  errorClass: e instanceof DriverOpError ? e.info.errorClass : undefined,
@@ -202,17 +317,29 @@ export class PlatformPoller {
202
317
  };
203
318
  try {
204
319
  await entry.cursor.flushIfDirty();
320
+ this.assertEntryActive(entry);
205
321
  }
206
322
  catch (e) {
207
323
  fail('cursor flush', e);
208
324
  }
209
325
  const tasks = await this.opts.tasks(entry.projectId);
326
+ this.assertEntryActive(entry);
210
327
  const byId = new Map(tasks.map(t => [t.taskId, t]));
211
328
  for (const gen of entry.cursor.generations()) {
212
- const t = byId.get(gen);
213
- 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) {
214
339
  try {
340
+ this.assertEntryActive(entry);
215
341
  await entry.cursor.dropGeneration(gen);
342
+ this.assertEntryActive(entry);
216
343
  }
217
344
  catch (e) {
218
345
  fail(`dropGeneration ${gen}`, e);
@@ -258,6 +385,7 @@ export class PlatformPoller {
258
385
  let steps;
259
386
  try {
260
387
  steps = await entry.driver.runPreflightSteps();
388
+ this.assertEntryActive(entry);
261
389
  }
262
390
  catch (e) {
263
391
  entry.defaultBranchStale += 1;
@@ -274,6 +402,7 @@ export class PlatformPoller {
274
402
  let defaultBranch;
275
403
  try {
276
404
  const [row] = await entry.driver.runOp('projectView');
405
+ this.assertEntryActive(entry);
277
406
  if (row !== undefined && typeof row.defaultBranch === 'string') {
278
407
  entry.defaultBranch = row.defaultBranch;
279
408
  entry.defaultBranchStale = 0;
@@ -282,6 +411,7 @@ export class PlatformPoller {
282
411
  if (row !== undefined && entry.preflightPassed !== true && entry.driver.runPreflightSteps !== undefined) {
283
412
  if (row.pushPermitted === false) {
284
413
  fail('preflight-push', new Error(`push permission missing for ${entry.repoUrl} — server merge/close requires write access`));
414
+ return failures;
285
415
  }
286
416
  else {
287
417
  if (row.pushPermitted === undefined) {
@@ -302,6 +432,7 @@ export class PlatformPoller {
302
432
  ? entry.defaultBranch
303
433
  : undefined);
304
434
  await this.pollListPrs(entry, tasks, byId, defaultBranch, fail, failures);
435
+ this.assertEntryActive(entry);
305
436
  for (const task of tasks) {
306
437
  if (task.terminal || task.prNumber === undefined)
307
438
  continue;
@@ -310,6 +441,7 @@ export class PlatformPoller {
310
441
  if (task.closedUnmergedAnchor === true)
311
442
  continue;
312
443
  await this.subPoll(entry, task, usableDefault, fail);
444
+ this.assertEntryActive(entry);
313
445
  }
314
446
  return failures;
315
447
  }
@@ -341,19 +473,56 @@ export class PlatformPoller {
341
473
  return vt !== undefined && vt < prsCursor.watermarkTime;
342
474
  });
343
475
  });
476
+ this.assertEntryActive(entry);
344
477
  }
345
478
  catch (e) {
346
479
  fail('listPrs', e);
347
480
  return;
348
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
+ }
349
512
  const boundByPr = new Map();
350
513
  for (const t of tasks) {
351
514
  if (!t.terminal && t.prNumber !== undefined)
352
515
  boundByPr.set(t.prNumber, t);
353
516
  }
354
517
  const failuresBefore = failures.length;
355
- let deferredAdoptions = 0;
356
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
+ };
357
526
  for (const row of prRows) {
358
527
  const prNumber = row.prNumber;
359
528
  seenPrs.add(prNumber);
@@ -364,10 +533,11 @@ export class PlatformPoller {
364
533
  };
365
534
  const boundTask = boundByPr.get(prNumber);
366
535
  if (boundTask !== undefined) {
536
+ await clearPending(prNumber);
367
537
  // reopen 是显式 durable 观察:closed-unmerged 锚在 TaskState,主轮询见重开即合成(spec §6)。
368
538
  if (boundTask.closedUnmergedAnchor === true && row.state === 'open') {
369
539
  try {
370
- await this.emit(entry, 'pr.updated', { ...base, kind: 'reopened' });
540
+ await this.emit(entry, 'pr.updated', { ...base, kind: 'reopened' }, boundTask.taskId);
371
541
  entry.observedPr.delete(`${boundTask.taskId}:${prNumber}`);
372
542
  }
373
543
  catch (e) {
@@ -376,31 +546,48 @@ export class PlatformPoller {
376
546
  }
377
547
  continue;
378
548
  }
379
- if (row.state !== 'open' || row.draft === true)
549
+ if (row.state !== 'open' || row.draft === true) {
550
+ await clearPending(prNumber);
380
551
  continue;
381
- if (!base.branch.startsWith(BRANCH_PREFIX))
552
+ }
553
+ if (!base.branch.startsWith(BRANCH_PREFIX)) {
554
+ await clearPending(prNumber);
382
555
  continue;
556
+ }
383
557
  const task = byId.get(base.branch.slice(BRANCH_PREFIX.length));
384
- if (task === undefined || task.terminal || task.prNumber !== undefined)
558
+ if (task === undefined || task.terminal || task.prNumber !== undefined) {
559
+ await clearPending(prNumber);
385
560
  continue;
561
+ }
562
+ const pendingTaskId = pendingByPr.get(prNumber);
563
+ if (pendingTaskId !== undefined && pendingTaskId !== task.taskId)
564
+ await clearPending(prNumber);
386
565
  // 统一谓词 branch === task.branch:branch 缺失的任务视图无从核验绑定,收编一并 fail closed
387
566
  // (与子轮询 bindingCheck 对称——「无法核验」不是「匹配」)。
388
- if (task.branch !== base.branch)
567
+ if (task.branch !== base.branch) {
568
+ await clearPending(prNumber);
389
569
  continue;
570
+ }
390
571
  if (row.sourceProjectId === null || row.sourceProjectId === undefined
391
- || row.sourceProjectId !== row.targetProjectId)
572
+ || row.sourceProjectId !== row.targetProjectId) {
573
+ await clearPending(prNumber);
392
574
  continue;
575
+ }
393
576
  const target = String(row.targetBranch);
577
+ if (entry.cursor.isAdoptionDelivered(task.taskId, prNumber))
578
+ continue;
394
579
  try {
395
580
  if (task.expectedBase !== undefined) {
396
- if (target !== task.expectedBase)
581
+ if (target !== task.expectedBase) {
582
+ await clearPending(prNumber);
397
583
  continue;
584
+ }
398
585
  }
399
586
  else {
400
587
  if (defaultBranch === undefined) {
401
- // 收编推迟必须同时冻结水位:否则该 PR 被水位判「已处理」,projectView 恢复后
402
- // 它已落在停止页之外、永不再被扫描。
403
- deferredAdoptions += 1;
588
+ await entry.cursor.markAdoptionPending(task.taskId, prNumber);
589
+ pendingByPr.set(prNumber, task.taskId);
590
+ activeDeferrals.add(prNumber);
404
591
  continue;
405
592
  }
406
593
  if (target !== defaultBranch) {
@@ -410,17 +597,61 @@ export class PlatformPoller {
410
597
  if (entry.baseMismatch.get(prNumber) !== fp) {
411
598
  await this.emit(entry, 'human.intervention', {
412
599
  ...base, reason: 'base-mismatch', targetBranch: target, expectedBase: defaultBranch, taskId: task.taskId,
413
- });
600
+ }, task.taskId);
414
601
  entry.baseMismatch.set(prNumber, fp);
415
602
  }
603
+ await clearPending(prNumber);
416
604
  continue;
417
605
  }
418
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);
419
636
  await this.emit(entry, 'pr.created', {
420
637
  ...base, headSha: String(row.headSha), targetBranch: target,
421
638
  ...(typeof row.prAuthorId === 'string' ? { prAuthorId: row.prAuthorId } : {}),
422
- });
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);
423
653
  entry.baseMismatch.delete(prNumber);
654
+ entry.adoptionDeferrals.delete(prNumber);
424
655
  }
425
656
  catch (e) {
426
657
  fail(`adopt pr#${prNumber}`, e);
@@ -430,10 +661,15 @@ export class PlatformPoller {
430
661
  if (!seenPrs.has(pr))
431
662
  entry.baseMismatch.delete(pr);
432
663
  }
433
- 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)
434
669
  return;
435
670
  try {
436
- await entry.cursor.commitListPrs(prRows, scanStartedAt - entry.driver.visibilityLagMs);
671
+ this.assertEntryActive(entry);
672
+ await entry.cursor.commitListPrs(listedPrRows, scanStartedAt - entry.driver.visibilityLagMs);
437
673
  }
438
674
  catch (e) {
439
675
  fail('listPrs cursor commit', e);
@@ -445,6 +681,7 @@ export class PlatformPoller {
445
681
  let prRow;
446
682
  try {
447
683
  [prRow] = await entry.driver.runOp('prView', { prNumber });
684
+ this.assertEntryActive(entry);
448
685
  }
449
686
  catch (e) {
450
687
  fail(`prView pr#${prNumber}`, e);
@@ -467,7 +704,7 @@ export class PlatformPoller {
467
704
  taskId: task.taskId,
468
705
  targetBranch: String(prRow.targetBranch),
469
706
  ...(task.expectedBase !== undefined ? { expectedBase: task.expectedBase } : {}),
470
- });
707
+ }, task.taskId);
471
708
  observed.bindingMismatch = binding.mismatch;
472
709
  }
473
710
  catch (e) {
@@ -482,7 +719,7 @@ export class PlatformPoller {
482
719
  if (typeof prRow.mergedAt === 'string') {
483
720
  if (observed.merged !== true) {
484
721
  try {
485
- await this.emit(entry, 'pr.merged', base);
722
+ await this.emit(entry, 'pr.merged', base, task.taskId);
486
723
  observed.merged = true;
487
724
  }
488
725
  catch (e) {
@@ -492,7 +729,7 @@ export class PlatformPoller {
492
729
  }
493
730
  else if (observed.closedUnmerged !== true) {
494
731
  try {
495
- await this.emit(entry, 'pr.updated', { ...base, kind: 'closed-unmerged' });
732
+ await this.emit(entry, 'pr.updated', { ...base, kind: 'closed-unmerged' }, task.taskId);
496
733
  observed.closedUnmerged = true;
497
734
  }
498
735
  catch (e) {
@@ -503,10 +740,14 @@ export class PlatformPoller {
503
740
  return;
504
741
  }
505
742
  observed.closedUnmerged = false;
743
+ if (task.reviewMode === 'server') {
744
+ this.engine.dropCandidate(task.taskId, prNumber);
745
+ return;
746
+ }
506
747
  const headSha = String(prRow.headSha);
507
748
  if (task.latestHeadSha !== undefined && headSha !== task.latestHeadSha.toLowerCase() && observed.pushSha !== headSha) {
508
749
  try {
509
- await this.emit(entry, 'pr.updated', { ...base, kind: 'push', headSha });
750
+ await this.emit(entry, 'pr.updated', { ...base, kind: 'push', headSha }, task.taskId);
510
751
  observed.pushSha = headSha;
511
752
  }
512
753
  catch (e) {
@@ -548,6 +789,7 @@ export class PlatformPoller {
548
789
  // 评论源扫描最长可达分钟级,扫描期间 head/状态可被平台改写(TOCTOU):emit 前以
549
790
  // 权威 prView 复核 open/!draft/绑定/锚点,失配清候选走 push/recheck,查询失败按瞬态重试。
550
791
  const [freshRow] = await entry.driver.runOp('prView', { prNumber });
792
+ this.assertEntryActive(entry);
551
793
  const freshEligible = freshRow !== undefined
552
794
  && this.bindingCheck(task, freshRow, defaultBranch) === undefined
553
795
  && freshRow.state === 'open' && freshRow.draft === false
@@ -558,6 +800,7 @@ export class PlatformPoller {
558
800
  }
559
801
  await this.emit(entry, 'review.submitted', {
560
802
  ...base,
803
+ source: 'platform-poller',
561
804
  action: decision.kind === 'pass' ? 'APPROVE' : 'REQUEST_CHANGES',
562
805
  headSha: decision.anchorSha,
563
806
  currentHeadSha: String(freshRow.headSha),
@@ -566,7 +809,7 @@ export class PlatformPoller {
566
809
  verdictToken: decision.token,
567
810
  verdictConflict: decision.conflict,
568
811
  verdictCarrier: decision.carrier,
569
- });
812
+ }, task.taskId);
570
813
  entry.deliveredVerdicts.set(obsKey, fp);
571
814
  }
572
815
  catch (e) {
@@ -648,7 +891,7 @@ export class PlatformPoller {
648
891
  ...(scan.sourceClass === 'threaded' && row.discussionId !== null && row.discussionId !== undefined
649
892
  ? { reviewCommentReply: true }
650
893
  : {}),
651
- });
894
+ }, task.taskId);
652
895
  await entry.cursor.markDelivered(task.taskId, base.prNumber, scan.key, id, digest);
653
896
  }
654
897
  catch (e) {
@@ -658,6 +901,7 @@ export class PlatformPoller {
658
901
  }
659
902
  if (delivered) {
660
903
  try {
904
+ this.assertEntryActive(entry);
661
905
  await entry.cursor.commitSource(task.taskId, base.prNumber, scan.key, scan.rows, cutoff);
662
906
  }
663
907
  catch (e) {
@@ -677,8 +921,13 @@ export class PlatformPoller {
677
921
  }
678
922
  return scans;
679
923
  }
680
- async emit(entry, type, data) {
681
- 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();
682
931
  }
683
932
  }
684
933
  //# sourceMappingURL=platform-poller.js.map