ftown 0.19.65 → 0.19.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command-rpc.d.ts +8 -0
- package/dist/command-rpc.js +42 -1
- package/dist/command-rpc.js.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/local-api-server.d.ts +5 -0
- package/dist/local-api-server.js +10 -0
- package/dist/local-api-server.js.map +1 -1
- package/dist/mail-delivery.d.ts +2 -0
- package/dist/mail-delivery.js +18 -7
- package/dist/mail-delivery.js.map +1 -1
- package/dist/mail-store.d.ts +9 -1
- package/dist/mail-store.js +71 -18
- package/dist/mail-store.js.map +1 -1
- package/dist/pull-request-events.d.ts +40 -0
- package/dist/pull-request-events.js +205 -0
- package/dist/pull-request-events.js.map +1 -0
- package/dist/pull-request-status.d.ts +16 -1
- package/dist/pull-request-status.js +80 -34
- package/dist/pull-request-status.js.map +1 -1
- package/dist/session-controller.js +8 -2
- package/dist/session-controller.js.map +1 -1
- package/dist/session-inbox-protocol.d.ts +13 -0
- package/dist/session-inbox-protocol.js +2 -0
- package/dist/session-inbox-protocol.js.map +1 -0
- package/dist/session-wire.js +1 -1
- package/dist/session-wire.js.map +1 -1
- package/dist/types.d.ts +4 -1
- package/package.json +2 -2
- package/skills/ftown/SKILL.md +11 -3
- package/skills/ftown/references/sessions.md +12 -0
package/dist/mail-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { appendFile, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { randomBytes } from 'node:crypto';
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
5
5
|
/** Rewrites cap the inbox file at the newest N messages. */
|
|
6
6
|
const MAX_STORED_MESSAGES = 500;
|
|
7
7
|
export function createMailMessage(input) {
|
|
@@ -11,6 +11,11 @@ export function createMailMessage(input) {
|
|
|
11
11
|
...input,
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
function checkpoint(value) {
|
|
15
|
+
if (value.version !== 1 || typeof value.linkId !== 'string' || !value.linkId || !['in_progress', 'failed', 'passed', 'merged'].includes(String(value.state)) || !Number.isSafeInteger(value.ordinal) || Number(value.ordinal) < 0 || (value.state !== 'merged' && (typeof value.headOid !== 'string' || !value.headOid)) || (value.headOid !== undefined && typeof value.headOid !== 'string'))
|
|
16
|
+
throw new Error('Invalid PR event checkpoint');
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
14
19
|
/**
|
|
15
20
|
* Per-session append-only inbox (inbox.jsonl in the session's data dir).
|
|
16
21
|
* Delivery marking rewrites the file atomically (tmp+rename); files are small.
|
|
@@ -31,33 +36,84 @@ export class MailStore {
|
|
|
31
36
|
this.locks.set(sessionId, next.catch(() => undefined));
|
|
32
37
|
return next;
|
|
33
38
|
}
|
|
34
|
-
/**
|
|
35
|
-
async
|
|
39
|
+
/** Old malformed message lines remain ignored; recognized checkpoints fail closed. */
|
|
40
|
+
async readInbox(sessionId) {
|
|
36
41
|
const filePath = this.inboxPath(sessionId);
|
|
37
|
-
if (!existsSync(filePath))
|
|
38
|
-
return [];
|
|
39
|
-
}
|
|
42
|
+
if (!existsSync(filePath))
|
|
43
|
+
return { messages: [] };
|
|
40
44
|
const raw = await readFile(filePath, 'utf-8');
|
|
41
45
|
const messages = [];
|
|
46
|
+
let state;
|
|
42
47
|
for (const line of raw.split('\n')) {
|
|
43
48
|
if (!line.trim())
|
|
44
49
|
continue;
|
|
50
|
+
let value;
|
|
45
51
|
try {
|
|
46
|
-
|
|
47
|
-
if (msg && typeof msg.id === 'string' && typeof msg.body === 'string') {
|
|
48
|
-
messages.push(msg);
|
|
49
|
-
}
|
|
52
|
+
value = JSON.parse(line);
|
|
50
53
|
}
|
|
51
54
|
catch {
|
|
52
|
-
//
|
|
55
|
+
// A broken recognizable metadata record must not silently reset deduplication.
|
|
56
|
+
if (line.includes('pr-event-checkpoint'))
|
|
57
|
+
throw new Error('Invalid PR event checkpoint');
|
|
58
|
+
continue;
|
|
53
59
|
}
|
|
60
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
61
|
+
continue;
|
|
62
|
+
const item = value;
|
|
63
|
+
if (item._ftown === 'pr-event-checkpoint') {
|
|
64
|
+
if (state)
|
|
65
|
+
throw new Error('Duplicate PR event checkpoint');
|
|
66
|
+
state = checkpoint(item);
|
|
67
|
+
}
|
|
68
|
+
else if (typeof item.id === 'string' && typeof item.body === 'string')
|
|
69
|
+
messages.push(item);
|
|
70
|
+
}
|
|
71
|
+
return { messages, checkpoint: state };
|
|
72
|
+
}
|
|
73
|
+
async readAll(sessionId) {
|
|
74
|
+
return (await this.readInbox(sessionId)).messages;
|
|
75
|
+
}
|
|
76
|
+
async replaceInbox(sessionId, inbox) {
|
|
77
|
+
const filePath = this.inboxPath(sessionId);
|
|
78
|
+
const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
79
|
+
const lines = [...(inbox.checkpoint ? [JSON.stringify(inbox.checkpoint)] : []), ...inbox.messages.map(message => JSON.stringify(message))];
|
|
80
|
+
try {
|
|
81
|
+
await writeFile(tmp, lines.join('\n') + '\n', { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
|
|
82
|
+
await rename(tmp, filePath);
|
|
54
83
|
}
|
|
55
|
-
|
|
84
|
+
catch (error) {
|
|
85
|
+
await import('node:fs/promises').then(fs => fs.rm(tmp, { force: true })).catch(() => { });
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Caller fences the link under the session mutation lock; never creates its directory. */
|
|
90
|
+
async commitPullRequestObservation(sessionId, linkId, observation) {
|
|
91
|
+
if (observation.stale || !observation.checkedAt || observation.reason || !['in_progress', 'failed', 'passed', 'merged'].includes(observation.state) || (observation.state !== 'merged' && !observation.headOid))
|
|
92
|
+
return { enqueued: false };
|
|
93
|
+
return this.withLock(sessionId, async () => {
|
|
94
|
+
const inbox = await this.readInbox(sessionId);
|
|
95
|
+
const old = inbox.checkpoint?.linkId === linkId ? inbox.checkpoint : undefined;
|
|
96
|
+
if (old?.state === 'merged' || (old && old.state === observation.state && old.headOid === observation.headOid))
|
|
97
|
+
return { enqueued: false };
|
|
98
|
+
const ordinal = (old?.ordinal ?? 0) + 1;
|
|
99
|
+
const state = observation.state;
|
|
100
|
+
inbox.checkpoint = { _ftown: 'pr-event-checkpoint', version: 1, linkId, state, ordinal, ...(observation.headOid ? { headOid: observation.headOid } : {}) };
|
|
101
|
+
let messageId;
|
|
102
|
+
if (state !== 'in_progress') {
|
|
103
|
+
messageId = `pr-${createHash('sha256').update(JSON.stringify([linkId, ordinal, state, observation.headOid ?? 'merged'])).digest('hex')}`;
|
|
104
|
+
const heading = state === 'merged' ? 'Pull request merged.' : state === 'failed' ? 'GitHub rollup checks failed.' : 'GitHub rollup checks passed.';
|
|
105
|
+
const advice = state === 'failed' ? 'Inspect the checks and reproduce failures before fixing them within your existing task authorization.' : 'Follow your existing task and user instructions; this notification does not authorize merging or other actions.';
|
|
106
|
+
inbox.messages.push({ id: messageId, ts: new Date().toISOString(), from: 'external', fromName: 'FTown PR updates', to: sessionId, type: 'message', body: `${heading}\nPR: ${observation.url}\nChecks: ${observation.url}/checks\n${observation.headOid ? `Observed head: ${observation.headOid}\n` : ''}Observed at: ${observation.checkedAt}\n${advice}` });
|
|
107
|
+
}
|
|
108
|
+
await this.replaceInbox(sessionId, inbox);
|
|
109
|
+
return { enqueued: !!messageId, ...(messageId ? { messageId } : {}) };
|
|
110
|
+
});
|
|
56
111
|
}
|
|
57
112
|
async append(msg) {
|
|
58
113
|
await this.withLock(msg.to, async () => {
|
|
59
114
|
const dir = this.resolveDir(msg.to);
|
|
60
115
|
await mkdir(dir, { recursive: true });
|
|
116
|
+
await this.readInbox(msg.to);
|
|
61
117
|
// Mail can carry task details; owner-only like the session registry.
|
|
62
118
|
await appendFile(this.inboxPath(msg.to), `${JSON.stringify(msg)}\n`, {
|
|
63
119
|
encoding: 'utf-8',
|
|
@@ -80,7 +136,8 @@ export class MailStore {
|
|
|
80
136
|
if (ids.length === 0) {
|
|
81
137
|
return [];
|
|
82
138
|
}
|
|
83
|
-
const
|
|
139
|
+
const inbox = await this.readInbox(sessionId);
|
|
140
|
+
const all = inbox.messages;
|
|
84
141
|
const wanted = new Set(ids);
|
|
85
142
|
const now = new Date().toISOString();
|
|
86
143
|
const marked = [];
|
|
@@ -104,11 +161,7 @@ export class MailStore {
|
|
|
104
161
|
}
|
|
105
162
|
return true;
|
|
106
163
|
});
|
|
107
|
-
|
|
108
|
-
const tmp = `${filePath}.tmp`;
|
|
109
|
-
const payload = kept.map((m) => JSON.stringify(m)).join('\n') + '\n';
|
|
110
|
-
await writeFile(tmp, payload, { encoding: 'utf-8', mode: 0o600 });
|
|
111
|
-
await rename(tmp, filePath);
|
|
164
|
+
await this.replaceInbox(sessionId, { ...inbox, messages: kept });
|
|
112
165
|
return marked;
|
|
113
166
|
});
|
|
114
167
|
}
|
package/dist/mail-store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mail-store.js","sourceRoot":"","sources":["../src/mail-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAClF,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"mail-store.js","sourceRoot":"","sources":["../src/mail-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { SessionStore } from './session-store.js';
|
|
2
|
+
import type { Session } from './types.js';
|
|
3
|
+
import type { PullRequestStatusService } from './pull-request-status.js';
|
|
4
|
+
import type { MailStore } from './mail-store.js';
|
|
5
|
+
type Timer = ReturnType<typeof setTimeout>;
|
|
6
|
+
interface Options {
|
|
7
|
+
bridgeId: string;
|
|
8
|
+
store: Pick<SessionStore, 'listSessions' | 'loadSession' | 'saveSession'>;
|
|
9
|
+
withSessionMutation: (id: string, task: () => Promise<void>) => Promise<void>;
|
|
10
|
+
statusService: Pick<PullRequestStatusService, 'getObservations' | 'setBackgroundUrls'>;
|
|
11
|
+
mailStore: Pick<MailStore, 'commitPullRequestObservation'>;
|
|
12
|
+
notifyCommitted: (session: Session, sender: string) => Promise<void>;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
setTimeout?: (fn: () => void, ms: number) => Timer;
|
|
15
|
+
clearTimeout?: (timer: Timer) => void;
|
|
16
|
+
}
|
|
17
|
+
/** Polls only observed changes using the same cache and quota as dashboard reads. */
|
|
18
|
+
export declare class PullRequestEventWatcher {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private stopped;
|
|
21
|
+
private started;
|
|
22
|
+
private epoch;
|
|
23
|
+
private flight?;
|
|
24
|
+
private timer?;
|
|
25
|
+
private group;
|
|
26
|
+
private baseline;
|
|
27
|
+
private cursor;
|
|
28
|
+
private pendingWake;
|
|
29
|
+
private nextDelay;
|
|
30
|
+
private readonly now;
|
|
31
|
+
private readonly setTimer;
|
|
32
|
+
private readonly clearTimer;
|
|
33
|
+
constructor(options: Options);
|
|
34
|
+
start(): void;
|
|
35
|
+
private schedule;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
runOnce(): Promise<void>;
|
|
38
|
+
private scan;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { normalizePullRequestUrl } from './pull-request-url.js';
|
|
3
|
+
function canonical(value) {
|
|
4
|
+
if (!value)
|
|
5
|
+
return undefined;
|
|
6
|
+
try {
|
|
7
|
+
const url = normalizePullRequestUrl(value);
|
|
8
|
+
return Number(url.slice(url.lastIndexOf('/') + 1)) <= 2_147_483_647 ? url : undefined;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function eligible(session, bridgeId) {
|
|
15
|
+
return !!session && session.bridgeId === bridgeId && session.shellType !== 'shell' && !!canonical(session.pullRequestUrl);
|
|
16
|
+
}
|
|
17
|
+
/** Polls only observed changes using the same cache and quota as dashboard reads. */
|
|
18
|
+
export class PullRequestEventWatcher {
|
|
19
|
+
options;
|
|
20
|
+
stopped = false;
|
|
21
|
+
started = false;
|
|
22
|
+
epoch = 0;
|
|
23
|
+
flight;
|
|
24
|
+
timer;
|
|
25
|
+
group = [];
|
|
26
|
+
baseline = new Map();
|
|
27
|
+
cursor = '';
|
|
28
|
+
pendingWake = new Map();
|
|
29
|
+
nextDelay = 60_000;
|
|
30
|
+
now;
|
|
31
|
+
setTimer;
|
|
32
|
+
clearTimer;
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.now = options.now ?? Date.now;
|
|
36
|
+
this.setTimer = options.setTimeout ?? ((fn, ms) => setTimeout(fn, ms));
|
|
37
|
+
this.clearTimer = options.clearTimeout ?? (timer => clearTimeout(timer));
|
|
38
|
+
}
|
|
39
|
+
start() {
|
|
40
|
+
if (this.started || this.stopped)
|
|
41
|
+
return;
|
|
42
|
+
this.started = true;
|
|
43
|
+
void this.runOnce().catch(error => console.error('[PR events] Scan failed:', error)).finally(() => this.schedule());
|
|
44
|
+
}
|
|
45
|
+
schedule() {
|
|
46
|
+
if (!this.started || this.stopped || this.timer)
|
|
47
|
+
return;
|
|
48
|
+
this.timer = this.setTimer(() => { this.timer = undefined; void this.runOnce().catch(error => console.error('[PR events] Scan failed:', error)).finally(() => this.schedule()); }, Math.max(60_000, this.nextDelay));
|
|
49
|
+
this.timer.unref?.();
|
|
50
|
+
}
|
|
51
|
+
async stop() {
|
|
52
|
+
this.stopped = true;
|
|
53
|
+
this.epoch++;
|
|
54
|
+
if (this.timer) {
|
|
55
|
+
this.clearTimer(this.timer);
|
|
56
|
+
this.timer = undefined;
|
|
57
|
+
}
|
|
58
|
+
await this.flight?.catch(() => { });
|
|
59
|
+
this.options.statusService.setBackgroundUrls([]);
|
|
60
|
+
}
|
|
61
|
+
runOnce() {
|
|
62
|
+
if (this.stopped)
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
if (this.flight)
|
|
65
|
+
return this.flight;
|
|
66
|
+
const epoch = this.epoch;
|
|
67
|
+
this.flight = this.scan(epoch).finally(() => { this.flight = undefined; });
|
|
68
|
+
return this.flight;
|
|
69
|
+
}
|
|
70
|
+
async scan(epoch) {
|
|
71
|
+
const { store, bridgeId, withSessionMutation, statusService, mailStore, notifyCommitted } = this.options;
|
|
72
|
+
const active = () => !this.stopped && this.epoch === epoch;
|
|
73
|
+
const links = [];
|
|
74
|
+
for (const candidate of await store.listSessions()) {
|
|
75
|
+
if (!active())
|
|
76
|
+
return;
|
|
77
|
+
if (!eligible(candidate, bridgeId))
|
|
78
|
+
continue;
|
|
79
|
+
try {
|
|
80
|
+
await withSessionMutation(candidate.id, async () => {
|
|
81
|
+
if (!active())
|
|
82
|
+
return;
|
|
83
|
+
const session = await store.loadSession(candidate.id);
|
|
84
|
+
if (!eligible(session, bridgeId))
|
|
85
|
+
return;
|
|
86
|
+
const url = canonical(session.pullRequestUrl);
|
|
87
|
+
const linkId = session.pullRequestLinkId ?? randomUUID();
|
|
88
|
+
if (!session.pullRequestLinkId)
|
|
89
|
+
await store.saveSession({ ...session, pullRequestLinkId: linkId });
|
|
90
|
+
links.push({ id: session.id, url, key: url.toLowerCase(), linkId });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// One damaged session must not prevent unrelated links from being scanned.
|
|
95
|
+
console.error(`[PR events] Could not inspect session ${candidate.id}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (!active())
|
|
99
|
+
return;
|
|
100
|
+
// A committed message survives a failed wake; retry without depending on a fresh gh result.
|
|
101
|
+
for (const [id, pending] of this.pendingWake) {
|
|
102
|
+
let sessionForWake;
|
|
103
|
+
try {
|
|
104
|
+
await withSessionMutation(id, async () => {
|
|
105
|
+
if (!active())
|
|
106
|
+
return;
|
|
107
|
+
const session = await store.loadSession(id);
|
|
108
|
+
if (eligible(session, bridgeId) && canonical(session.pullRequestUrl)?.toLowerCase() === pending.key && session.pullRequestLinkId === pending.linkId)
|
|
109
|
+
sessionForWake = session;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Keep the retry pending; other sessions and status groups can still progress.
|
|
114
|
+
console.error(`[PR events] Could not inspect session ${id}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!active())
|
|
118
|
+
return;
|
|
119
|
+
if (!sessionForWake) {
|
|
120
|
+
this.pendingWake.delete(id);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
await notifyCommitted(sessionForWake, 'FTown PR updates');
|
|
125
|
+
this.pendingWake.delete(id);
|
|
126
|
+
}
|
|
127
|
+
catch { /* Retry on the next scan. */ }
|
|
128
|
+
}
|
|
129
|
+
const urls = [...new Map(links.map(link => [link.key, link.url])).entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
130
|
+
const keys = new Set(urls.map(([key]) => key));
|
|
131
|
+
if (this.group.some(key => !keys.has(key))) {
|
|
132
|
+
this.group = [];
|
|
133
|
+
this.baseline.clear();
|
|
134
|
+
}
|
|
135
|
+
if (!this.group.length && urls.length) {
|
|
136
|
+
let start = urls.findIndex(([key]) => key > this.cursor);
|
|
137
|
+
if (start < 0)
|
|
138
|
+
start = 0;
|
|
139
|
+
const selected = [...urls.slice(start), ...urls.slice(0, start)].slice(0, 20);
|
|
140
|
+
this.group = selected.map(([key]) => key);
|
|
141
|
+
// Capture attempt ordinals before pinning: a quota-blocked read must not advance the group.
|
|
142
|
+
const before = await statusService.getObservations(selected.map(([, url]) => url));
|
|
143
|
+
if (!active())
|
|
144
|
+
return;
|
|
145
|
+
this.baseline = new Map(before.observations.map(item => [item.url.toLowerCase(), item.attempt]));
|
|
146
|
+
statusService.setBackgroundUrls(selected.map(([, url]) => url));
|
|
147
|
+
}
|
|
148
|
+
if (!this.group.length) {
|
|
149
|
+
statusService.setBackgroundUrls([]);
|
|
150
|
+
this.nextDelay = 60_000;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const selectedUrls = this.group.map(key => urls.find(([entry]) => entry === key)?.[1]).filter((url) => !!url);
|
|
154
|
+
const result = await statusService.getObservations(selectedUrls);
|
|
155
|
+
if (!active())
|
|
156
|
+
return;
|
|
157
|
+
this.nextDelay = Math.max(60_000, Date.parse(result.nextRefreshAt) - this.now() || 60_000);
|
|
158
|
+
const observed = new Map(result.observations.map(item => [item.url.toLowerCase(), item]));
|
|
159
|
+
for (const link of links) {
|
|
160
|
+
const observation = observed.get(link.key);
|
|
161
|
+
if (!observation || observation.stale || observation.reason || !observation.checkedAt || !['in_progress', 'failed', 'passed', 'merged'].includes(observation.state))
|
|
162
|
+
continue;
|
|
163
|
+
let sessionForWake;
|
|
164
|
+
let attemptingCommit = false;
|
|
165
|
+
try {
|
|
166
|
+
await withSessionMutation(link.id, async () => {
|
|
167
|
+
if (!active())
|
|
168
|
+
return;
|
|
169
|
+
const session = await store.loadSession(link.id);
|
|
170
|
+
if (!eligible(session, bridgeId) || canonical(session.pullRequestUrl)?.toLowerCase() !== link.key || session.pullRequestLinkId !== link.linkId)
|
|
171
|
+
return;
|
|
172
|
+
attemptingCommit = true;
|
|
173
|
+
await mailStore.commitPullRequestObservation(session.id, link.linkId, observation);
|
|
174
|
+
if (active())
|
|
175
|
+
sessionForWake = session;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// A failed atomic commit may have renamed successfully before reporting an error.
|
|
180
|
+
if (active() && attemptingCommit)
|
|
181
|
+
this.pendingWake.set(link.id, { key: link.key, linkId: link.linkId });
|
|
182
|
+
console.error(`[PR events] Could not commit observation for session ${link.id}`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (active() && sessionForWake) {
|
|
186
|
+
try {
|
|
187
|
+
await notifyCommitted(sessionForWake, 'FTown PR updates');
|
|
188
|
+
this.pendingWake.delete(link.id);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
this.pendingWake.set(link.id, { key: link.key, linkId: link.linkId });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (this.group.every(key => { const observation = observed.get(key); return !!observation && ((!observation.stale && !!observation.checkedAt && !observation.reason) || observation.attempt !== (this.baseline.get(key) ?? 0)); })) {
|
|
196
|
+
this.cursor = this.group.at(-1);
|
|
197
|
+
this.group = [];
|
|
198
|
+
this.baseline.clear();
|
|
199
|
+
statusService.setBackgroundUrls([]);
|
|
200
|
+
// Keep discovering new links and rotating other groups at the shared quota cadence.
|
|
201
|
+
this.nextDelay = 60_000;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=pull-request-events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pull-request-events.js","sourceRoot":"","sources":["../src/pull-request-events.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import type { PullRequestStatusesResponse } from './pull-request-status-protocol.js';
|
|
1
|
+
import type { PullRequestStatus, PullRequestStatusesResponse } from './pull-request-status-protocol.js';
|
|
2
2
|
type RunnerResult = {
|
|
3
3
|
stdout: string;
|
|
4
4
|
stderr: string;
|
|
5
5
|
exitCode: number;
|
|
6
6
|
};
|
|
7
7
|
type Runner = (input: string) => Promise<RunnerResult>;
|
|
8
|
+
export interface PullRequestObservation extends PullRequestStatus {
|
|
9
|
+
headOid?: string;
|
|
10
|
+
attempt: number;
|
|
11
|
+
}
|
|
8
12
|
/** Run a single bounded gh process; credentials stay in gh's environment. */
|
|
9
13
|
export declare function runGh(input: string): Promise<RunnerResult>;
|
|
10
14
|
/** Process-wide, demand-driven PR status cache and quota gate. */
|
|
@@ -16,8 +20,19 @@ export declare class PullRequestStatusService {
|
|
|
16
20
|
private cooldown;
|
|
17
21
|
private flight;
|
|
18
22
|
private sequence;
|
|
23
|
+
private attemptSequence;
|
|
24
|
+
private background;
|
|
25
|
+
private inFlight;
|
|
19
26
|
constructor(now?: () => number, runner?: Runner);
|
|
27
|
+
private entry;
|
|
28
|
+
private evict;
|
|
29
|
+
setBackgroundUrls(urls: readonly string[]): void;
|
|
20
30
|
get(urls: readonly string[]): Promise<Pick<PullRequestStatusesResponse, 'statuses' | 'nextRefreshAt'>>;
|
|
31
|
+
getObservations(urls: readonly string[]): Promise<{
|
|
32
|
+
observations: PullRequestObservation[];
|
|
33
|
+
nextRefreshAt: string;
|
|
34
|
+
}>;
|
|
35
|
+
private read;
|
|
21
36
|
private refresh;
|
|
22
37
|
}
|
|
23
38
|
export {};
|
|
@@ -55,7 +55,9 @@ function stateOf(value) {
|
|
|
55
55
|
if (!pr)
|
|
56
56
|
return { state: 'unknown', reason: 'unavailable' };
|
|
57
57
|
if (pr.merged === true || pr.state === 'MERGED')
|
|
58
|
-
return { state: 'merged' };
|
|
58
|
+
return pr.merged === true && pr.state === 'MERGED' ? { state: 'merged' } : { state: 'unknown', reason: 'unavailable' };
|
|
59
|
+
if (pr.merged !== false)
|
|
60
|
+
return { state: 'unknown', reason: 'unavailable' };
|
|
59
61
|
if (pr.state === 'CLOSED')
|
|
60
62
|
return { state: 'closed' };
|
|
61
63
|
if (pr.state !== 'OPEN' || typeof pr.headRefOid !== 'string' || !pr.headRefOid)
|
|
@@ -74,10 +76,10 @@ function stateOf(value) {
|
|
|
74
76
|
return { state: 'unknown', reason: 'no_checks' };
|
|
75
77
|
switch (rollup.state) {
|
|
76
78
|
case 'FAILURE':
|
|
77
|
-
case 'ERROR': return { state: 'failed' };
|
|
79
|
+
case 'ERROR': return { state: 'failed', headOid: pr.headRefOid };
|
|
78
80
|
case 'PENDING':
|
|
79
|
-
case 'EXPECTED': return { state: 'in_progress' };
|
|
80
|
-
case 'SUCCESS': return { state: 'passed' };
|
|
81
|
+
case 'EXPECTED': return { state: 'in_progress', headOid: pr.headRefOid };
|
|
82
|
+
case 'SUCCESS': return { state: 'passed', headOid: pr.headRefOid };
|
|
81
83
|
default: return { state: 'unknown', reason: 'unavailable' };
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -123,59 +125,102 @@ export class PullRequestStatusService {
|
|
|
123
125
|
cooldown = 0;
|
|
124
126
|
flight;
|
|
125
127
|
sequence = 0;
|
|
128
|
+
attemptSequence = 0;
|
|
129
|
+
background = new Set();
|
|
130
|
+
inFlight = new Set();
|
|
126
131
|
constructor(now = Date.now, runner = runGh) {
|
|
127
132
|
this.now = now;
|
|
128
133
|
this.runner = runner;
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
keys.push(key);
|
|
146
|
-
let entry = this.cache.get(key);
|
|
147
|
-
if (!entry) {
|
|
148
|
-
entry = { url, state: 'unknown', checkedAt: null, lastAttempt: 0, nextAttempt: 0, used: 0 };
|
|
149
|
-
this.cache.set(key, entry);
|
|
150
|
-
}
|
|
151
|
-
entry.used = ++this.sequence;
|
|
135
|
+
entry(value) {
|
|
136
|
+
let url;
|
|
137
|
+
try {
|
|
138
|
+
url = normalizePullRequestUrl(value);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
if (Number(url.slice(url.lastIndexOf('/') + 1)) > 2_147_483_647)
|
|
144
|
+
return undefined;
|
|
145
|
+
const key = url.toLowerCase();
|
|
146
|
+
let entry = this.cache.get(key);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
entry = { url, state: 'unknown', checkedAt: null, attempt: 0, lastAttempt: 0, nextAttempt: 0, used: 0 };
|
|
149
|
+
this.cache.set(key, entry);
|
|
152
150
|
}
|
|
151
|
+
entry.used = ++this.sequence;
|
|
152
|
+
return key;
|
|
153
|
+
}
|
|
154
|
+
evict() {
|
|
153
155
|
while (this.cache.size > 500) {
|
|
154
|
-
const oldest = [...this.cache.entries()].sort((a, b) => a[1].used - b[1].used)[0];
|
|
156
|
+
const oldest = [...this.cache.entries()].filter(([key]) => !this.background.has(key) && !this.inFlight.has(key)).sort((a, b) => a[1].used - b[1].used)[0];
|
|
157
|
+
if (!oldest)
|
|
158
|
+
break;
|
|
155
159
|
this.cache.delete(oldest[0]);
|
|
156
160
|
}
|
|
161
|
+
}
|
|
162
|
+
setBackgroundUrls(urls) {
|
|
163
|
+
this.background.clear();
|
|
164
|
+
for (const url of urls) {
|
|
165
|
+
if (this.background.size >= 20)
|
|
166
|
+
break;
|
|
167
|
+
const key = this.entry(url);
|
|
168
|
+
if (key)
|
|
169
|
+
this.background.add(key);
|
|
170
|
+
}
|
|
171
|
+
this.evict();
|
|
172
|
+
}
|
|
173
|
+
async get(urls) {
|
|
174
|
+
const { observations, nextRefreshAt } = await this.read(urls, true);
|
|
175
|
+
return { statuses: observations.map(({ headOid: _headOid, attempt: _attempt, ...status }) => status), nextRefreshAt };
|
|
176
|
+
}
|
|
177
|
+
async getObservations(urls) {
|
|
178
|
+
return this.read(urls, false);
|
|
179
|
+
}
|
|
180
|
+
async read(urls, foreground) {
|
|
181
|
+
const keys = [];
|
|
182
|
+
for (const value of urls) {
|
|
183
|
+
const key = this.entry(value);
|
|
184
|
+
if (key && !keys.includes(key))
|
|
185
|
+
keys.push(key);
|
|
186
|
+
}
|
|
187
|
+
this.evict();
|
|
157
188
|
if (this.flight)
|
|
158
189
|
await this.flight;
|
|
159
190
|
else {
|
|
160
191
|
const now = this.now();
|
|
161
192
|
this.launches = this.launches.filter(time => time > now - HOUR);
|
|
162
193
|
const allowed = Math.max(this.cooldown, (this.launches.at(-1) ?? -Infinity) + MINUTE, this.launches.length >= 60 ? this.launches[0] + HOUR : 0);
|
|
163
|
-
const due =
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
const due = (key) => { const e = this.cache.get(key); return !!e && e.state !== 'merged' && now >= e.nextAttempt && (e.checkedAt === null || now >= e.checkedAt + ttl(e.state)); };
|
|
195
|
+
const sort = (a, b) => a.lastAttempt - b.lastAttempt || a.used - b.used;
|
|
196
|
+
const foregroundDue = (foreground ? keys : []).filter(due).map(key => this.cache.get(key)).sort(sort);
|
|
197
|
+
const backgroundDue = [...this.background].filter(due).map(key => this.cache.get(key)).sort(sort);
|
|
198
|
+
const selected = [];
|
|
199
|
+
const add = (entry) => { if (entry && !selected.includes(entry) && selected.length < 20)
|
|
200
|
+
selected.push(entry); };
|
|
201
|
+
if (foregroundDue.length && backgroundDue.length) {
|
|
202
|
+
add(foregroundDue[0]);
|
|
203
|
+
add(backgroundDue[0]);
|
|
204
|
+
}
|
|
205
|
+
for (const entry of [...new Set([...foregroundDue, ...backgroundDue])].sort(sort))
|
|
206
|
+
add(entry);
|
|
207
|
+
if (selected.length && now >= allowed) {
|
|
166
208
|
this.launches.push(now);
|
|
167
|
-
for (const entry of
|
|
209
|
+
for (const entry of selected) {
|
|
168
210
|
entry.lastAttempt = now || 1;
|
|
169
|
-
|
|
211
|
+
entry.attempt = ++this.attemptSequence;
|
|
212
|
+
this.inFlight.add(entry.url.toLowerCase());
|
|
213
|
+
}
|
|
214
|
+
this.flight = this.refresh(selected, now).finally(() => { this.inFlight.clear(); this.flight = undefined; this.evict(); });
|
|
170
215
|
await this.flight;
|
|
171
216
|
}
|
|
172
217
|
}
|
|
173
218
|
const now = this.now();
|
|
174
219
|
const entries = keys.map(key => this.cache.get(key)).filter((entry) => !!entry);
|
|
175
|
-
const
|
|
220
|
+
const observations = entries.map(entry => ({ url: entry.url, state: entry.state, checkedAt: entry.checkedAt === null ? null : new Date(entry.checkedAt).toISOString(), stale: entry.checkedAt === null || now >= entry.checkedAt + ttl(entry.state) || (entry.reason !== undefined && entry.reason !== 'no_checks'), ...(entry.reason ? { reason: entry.reason } : {}), ...(entry.headOid ? { headOid: entry.headOid } : {}), attempt: entry.attempt }));
|
|
176
221
|
const nextDue = entries.filter(entry => entry.state !== 'merged').map(entry => Math.max(entry.nextAttempt, entry.checkedAt === null ? 0 : entry.checkedAt + ttl(entry.state)));
|
|
177
222
|
const next = nextDue.length ? Math.max(now + MINUTE, this.cooldown, (this.launches.at(-1) ?? -Infinity) + MINUTE, Math.min(...nextDue), this.launches.length >= 60 ? this.launches[0] + HOUR : 0) : now + HOUR;
|
|
178
|
-
return {
|
|
223
|
+
return { observations, nextRefreshAt: new Date(next).toISOString() };
|
|
179
224
|
}
|
|
180
225
|
async refresh(entries, now) {
|
|
181
226
|
const fail = (reason, until) => {
|
|
@@ -238,6 +283,7 @@ export class PullRequestStatusService {
|
|
|
238
283
|
else {
|
|
239
284
|
entry.state = mapped.state;
|
|
240
285
|
entry.reason = mapped.reason;
|
|
286
|
+
entry.headOid = mapped.headOid;
|
|
241
287
|
entry.checkedAt = now;
|
|
242
288
|
entry.nextAttempt = now + ttl(mapped.state);
|
|
243
289
|
}
|