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.
- package/README.md +7 -4
- package/README.zh-CN.md +7 -4
- package/dist/agent/bootstrap-poller.d.ts +1 -1
- package/dist/agent/bootstrap-poller.d.ts.map +1 -1
- package/dist/agent/bootstrap-poller.js.map +1 -1
- package/dist/agent/bootstrap.d.ts +1 -1
- package/dist/agent/bootstrap.d.ts.map +1 -1
- package/dist/agent/bootstrap.js +6 -2
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/branch.d.ts +1 -2
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +2 -2
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts +0 -1
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +11 -50
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +208 -85
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +3027 -1441
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/pane-streamer.js +1 -1
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +47 -2
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +318 -49
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +11 -7
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +67 -9
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts +23 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +223 -24
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +16 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +71 -11
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +6 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +44 -11
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.d.ts +20 -1
- package/dist/agent/review-transport.d.ts.map +1 -1
- package/dist/agent/review-transport.js +68 -20
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +20 -11
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +3 -2
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/platform-guard.d.ts +10 -2
- package/dist/api/platform-guard.d.ts.map +1 -1
- package/dist/api/platform-guard.js +112 -10
- package/dist/api/platform-guard.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +87 -11
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +22 -14
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +4 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/hot-reload.d.ts +11 -2
- package/dist/config/hot-reload.d.ts.map +1 -1
- package/dist/config/hot-reload.js +43 -8
- package/dist/config/hot-reload.js.map +1 -1
- package/dist/config/loader.js +1 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.d.ts +3 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +57 -28
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +616 -1067
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/log.d.ts.map +1 -1
- package/dist/event/log.js +17 -2
- package/dist/event/log.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +667 -440
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -79
- package/dist/index.js.map +1 -1
- package/dist/platform/command-renderer.d.ts +1 -0
- package/dist/platform/command-renderer.d.ts.map +1 -1
- package/dist/platform/command-renderer.js +16 -2
- package/dist/platform/command-renderer.js.map +1 -1
- package/dist/platform/comment-cursor.d.ts +9 -0
- package/dist/platform/comment-cursor.d.ts.map +1 -1
- package/dist/platform/comment-cursor.js +63 -5
- package/dist/platform/comment-cursor.js.map +1 -1
- package/dist/platform/driver-host.d.ts.map +1 -1
- package/dist/platform/driver-host.js +0 -2
- package/dist/platform/driver-host.js.map +1 -1
- package/dist/platform/driver-spec.d.ts.map +1 -1
- package/dist/platform/driver-spec.js +50 -6
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/feedback.d.ts.map +1 -1
- package/dist/platform/feedback.js +0 -9
- package/dist/platform/feedback.js.map +1 -1
- package/dist/platform/git-driver.d.ts +10 -1
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +78 -5
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +26 -5
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +362 -34
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugin-registry.d.ts.map +1 -1
- package/dist/platform/plugin-registry.js +14 -2
- package/dist/platform/plugin-registry.js.map +1 -1
- package/dist/platform/plugins/github/baxian-plugin.json +1 -1
- package/dist/platform/plugins/github/driver.json +17 -3
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +21 -11
- package/dist/platform/plugins/github/skills/baxian-cli-gh/agents/openai.yaml +2 -0
- package/dist/platform/pr-binding.d.ts.map +1 -1
- package/dist/platform/pr-binding.js +0 -2
- package/dist/platform/pr-binding.js.map +1 -1
- package/dist/{github → platform}/pr-conversation-cache.d.ts +11 -4
- package/dist/platform/pr-conversation-cache.d.ts.map +1 -0
- package/dist/platform/pr-conversation-cache.js +199 -0
- package/dist/platform/pr-conversation-cache.js.map +1 -0
- package/dist/platform/preflight-exec.d.ts +21 -0
- package/dist/platform/preflight-exec.d.ts.map +1 -0
- package/dist/platform/preflight-exec.js +62 -0
- package/dist/platform/preflight-exec.js.map +1 -0
- package/dist/platform/review-timeline.d.ts +16 -0
- package/dist/platform/review-timeline.d.ts.map +1 -0
- package/dist/platform/review-timeline.js +195 -0
- package/dist/platform/review-timeline.js.map +1 -0
- package/dist/platform/row-schema.d.ts.map +1 -1
- package/dist/platform/row-schema.js +1 -0
- package/dist/platform/row-schema.js.map +1 -1
- package/dist/platform/startup.d.ts +34 -2
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +107 -4
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts +4 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +6 -4
- package/dist/platform/types.js.map +1 -1
- package/dist/platform/verdict-engine.d.ts.map +1 -1
- package/dist/platform/verdict-engine.js +0 -2
- package/dist/platform/verdict-engine.js.map +1 -1
- package/dist/shared/git-url.d.ts +1 -0
- package/dist/shared/git-url.d.ts.map +1 -1
- package/dist/shared/git-url.js +3 -0
- package/dist/shared/git-url.js.map +1 -1
- package/dist/shared/types.d.ts +90 -9
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/skill/registry.d.ts +9 -2
- package/dist/skill/registry.d.ts.map +1 -1
- package/dist/skill/registry.js +88 -54
- package/dist/skill/registry.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +5 -11
- package/dist/skills/baxian-pr-recheck/SKILL.md +4 -43
- package/dist/skills/baxian-pr-review/SKILL.md +5 -42
- package/dist/skills/baxian-research/SKILL.md +1 -1
- package/dist/skills/baxian-server-feedback/SKILL.md +7 -5
- package/dist/skills/baxian-signals/SKILL.md +21 -8
- package/dist/skills/baxian-task-check/SKILL.md +5 -5
- package/dist/state/agent-store.d.ts.map +1 -1
- package/dist/state/agent-store.js +25 -2
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/review-store.d.ts +21 -1
- package/dist/state/review-store.d.ts.map +1 -1
- package/dist/state/review-store.js +132 -0
- package/dist/state/review-store.js.map +1 -1
- package/dist/state/task-store.d.ts +6 -0
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +262 -21
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +7 -6
- package/dist/terminal/attach.js.map +1 -1
- package/dist/web/assets/{index-CwYBVTyn.css → index-BPv6vv3U.css} +1 -1
- package/dist/web/assets/index-BXfgaFNm.js +18 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/github/mapper.d.ts +0 -9
- package/dist/github/mapper.d.ts.map +0 -1
- package/dist/github/mapper.js +0 -25
- package/dist/github/mapper.js.map +0 -1
- package/dist/github/poller.d.ts +0 -73
- package/dist/github/poller.d.ts.map +0 -1
- package/dist/github/poller.js +0 -556
- package/dist/github/poller.js.map +0 -1
- package/dist/github/pr-conversation-cache.d.ts.map +0 -1
- package/dist/github/pr-conversation-cache.js +0 -105
- package/dist/github/pr-conversation-cache.js.map +0 -1
- package/dist/github/pr-conversation.d.ts +0 -7
- package/dist/github/pr-conversation.d.ts.map +0 -1
- package/dist/github/pr-conversation.js +0 -175
- package/dist/github/pr-conversation.js.map +0 -1
- package/dist/github/resolver.d.ts +0 -8
- package/dist/github/resolver.d.ts.map +0 -1
- package/dist/github/resolver.js +0 -28
- package/dist/github/resolver.js.map +0 -1
- package/dist/state/post-approve-store.d.ts +0 -23
- package/dist/state/post-approve-store.d.ts.map +0 -1
- package/dist/state/post-approve-store.js +0 -82
- package/dist/state/post-approve-store.js.map +0 -1
- 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
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|