@teamvibe/poller 0.8.3 → 0.8.4-rc.1
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 +13 -0
- package/dist/brain-directory.d.ts +14 -0
- package/dist/brain-directory.js +16 -0
- package/dist/brain-manager.d.ts +2 -2
- package/dist/brain-manager.js +32 -19
- package/dist/brain-path.d.ts +9 -0
- package/dist/brain-path.js +65 -0
- package/dist/claude-spawner.d.ts +3 -2
- package/dist/claude-spawner.js +189 -71
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/event-queue.js +14 -1
- package/dist/inbox-manager.d.ts +11 -0
- package/dist/inbox-manager.js +101 -6
- package/dist/inbox-watcher.d.ts +9 -4
- package/dist/inbox-watcher.js +219 -49
- package/dist/maintenance-cron.d.ts +4 -2
- package/dist/maintenance-cron.js +56 -14
- package/dist/metrics/scheduler.d.ts +4 -1
- package/dist/metrics/scheduler.js +8 -6
- package/dist/poller.d.ts +2 -1
- package/dist/poller.js +50 -24
- package/dist/pool-logging.js +2 -2
- package/dist/pool-telemetry.d.ts +20 -4
- package/dist/pool-telemetry.js +23 -0
- package/dist/session-store.d.ts +1 -1
- package/dist/session-store.js +6 -2
- package/dist/startup-auth.js +4 -1
- package/dist/subscription-pool-client.js +7 -0
- package/dist/subscription-pool.d.ts +1 -0
- package/dist/subscription-pool.js +1 -1
- package/dist/subscription-runtime.d.ts +20 -2
- package/dist/subscription-runtime.js +4 -4
- package/dist/subscription-selection.d.ts +13 -3
- package/dist/subscription-selection.js +18 -3
- package/dist/thread-routing.d.ts +23 -0
- package/dist/thread-routing.js +81 -0
- package/dist/types.d.ts +4 -10
- package/dist/usage-limit-messages.d.ts +1 -1
- package/dist/usage-limit-messages.js +1 -1
- package/dist/usage-probe-contract.d.ts +15 -0
- package/dist/usage-probe-contract.js +14 -0
- package/dist/usage-probe-job.js +18 -5
- package/dist/worktree-manager.d.ts +2 -1
- package/dist/worktree-manager.js +38 -10
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -39,20 +39,20 @@ export async function preparePool(msg, resolved, pin, fetched, log) {
|
|
|
39
39
|
if (other.accountId !== account.accountId)
|
|
40
40
|
poolInfo(log, `skipped account=${poolLabel(other)} reason=session pinned elsewhere`);
|
|
41
41
|
}
|
|
42
|
-
return { account, store, pool };
|
|
42
|
+
return { account, store, pool, selection: selected };
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
const selected = await selectPoolForResolvedModel(resolved, { workspaceId: msg.teamvibe.workspaceId, pool, store, now: Date.now(), log });
|
|
46
|
+
const selected = await selectPoolForResolvedModel(resolved, { workspaceId: msg.teamvibe.workspaceId, pool, store, now: Date.now(), log, currentAccountId: pin?.workspaceId === msg.teamvibe.workspaceId ? pin.accountId : undefined });
|
|
47
47
|
if (selected.kind === 'selected') {
|
|
48
48
|
if (log && pool.kind === 'ready' && pin?.workspaceId === msg.teamvibe.workspaceId && pin.accountId !== selected.account.accountId) {
|
|
49
49
|
const previous = pool.accounts.find(a => a.accountId === pin.accountId);
|
|
50
50
|
poolInfo(log, `switch from=${poolLabel(previous ?? {})} to=${poolLabel(selected.account)} reason=pinned account unavailable`);
|
|
51
51
|
}
|
|
52
|
-
return { account: selected.account, store, pool };
|
|
52
|
+
return { account: selected.account, store, pool, selection: selected };
|
|
53
53
|
}
|
|
54
54
|
if (selected.kind === 'no_accounts_configured')
|
|
55
|
-
return { account: undefined, store, pool };
|
|
55
|
+
return { account: undefined, store, pool, selection: undefined };
|
|
56
56
|
throw await poolDeferredError(pool, resolved, store, selected.kind === 'deferred' ? selected.eligibleAt : undefined);
|
|
57
57
|
}
|
|
58
58
|
export function accountPin(account, resolved) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AccountProbeFailureReason } from './usage-probe-contract.js';
|
|
1
2
|
import type { ClaudeQuotaFailure } from './claude-quota.js';
|
|
2
3
|
export declare const USAGE_THRESHOLD_PERCENT = 90;
|
|
3
4
|
/** Passive snapshots are useful for five minutes, then become unknown. */
|
|
@@ -14,6 +15,9 @@ export interface AccountIdentity {
|
|
|
14
15
|
export type PoolFetchResult<A extends AccountIdentity = AccountIdentity> = {
|
|
15
16
|
kind: 'ready';
|
|
16
17
|
accounts: readonly A[];
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'probe_account_unavailable';
|
|
20
|
+
reason: AccountProbeFailureReason;
|
|
17
21
|
} | {
|
|
18
22
|
kind: 'no_accounts_configured';
|
|
19
23
|
} | {
|
|
@@ -46,13 +50,17 @@ export interface AccountUsageState {
|
|
|
46
50
|
usage?: UsageObservation;
|
|
47
51
|
reactive?: readonly ReactiveExhaustion[];
|
|
48
52
|
}
|
|
49
|
-
export
|
|
53
|
+
export interface HeadroomScore {
|
|
54
|
+
score: number;
|
|
55
|
+
rateWindow: UsageWindow;
|
|
56
|
+
reason: 'headroom_per_hour';
|
|
57
|
+
}
|
|
58
|
+
export type SelectionOutcome<A extends AccountIdentity = AccountIdentity> = ({
|
|
50
59
|
kind: 'selected';
|
|
51
60
|
account: A;
|
|
52
61
|
provenance: 'known';
|
|
53
|
-
score: number;
|
|
54
62
|
source: 'passive' | 'probe';
|
|
55
|
-
} | {
|
|
63
|
+
} & HeadroomScore) | {
|
|
56
64
|
kind: 'selected';
|
|
57
65
|
account: A;
|
|
58
66
|
provenance: 'unknown';
|
|
@@ -73,5 +81,7 @@ export declare function selectSubscriptionAccount<A extends AccountIdentity>(inp
|
|
|
73
81
|
/** Stable account ID within the pool's single workspace. */
|
|
74
82
|
states: Readonly<Record<string, AccountUsageState | undefined>>;
|
|
75
83
|
model: string | undefined;
|
|
84
|
+
/** Tie preference only; session pins are enforced separately by the runtime. */
|
|
85
|
+
currentAccountId?: string;
|
|
76
86
|
now: number;
|
|
77
87
|
}): SelectionOutcome<A>;
|
|
@@ -92,7 +92,7 @@ export function normalizeProbeUsage(value, observedAt) {
|
|
|
92
92
|
return { source: 'probe', observedAt, windows };
|
|
93
93
|
}
|
|
94
94
|
function family(model) {
|
|
95
|
-
return model?.toLowerCase().match(/^(?:claude-)?(sonnet|opus|haiku)(?:-\d[\w.-]*)?(?:\[1m\])?$/)?.[1];
|
|
95
|
+
return model?.toLowerCase().match(/^(?:claude-)?(sonnet|opus|haiku|fable)(?:-\d[\w.-]*)?(?:\[1m\])?$/)?.[1];
|
|
96
96
|
}
|
|
97
97
|
function applicable(scope, model) {
|
|
98
98
|
return scope === 'global' || scope === 'uncertain' || family(model) === undefined || scope === family(model);
|
|
@@ -101,6 +101,21 @@ export function reactiveEligibleAt(reactive) {
|
|
|
101
101
|
const parsed = reactive.quota.resetAt ? Date.parse(reactive.quota.resetAt) : NaN;
|
|
102
102
|
return Number.isFinite(parsed) && parsed > reactive.observedAt ? parsed : reactive.observedAt + POOL_RECHECK_MS;
|
|
103
103
|
}
|
|
104
|
+
/** Called only for nonempty, applicable known windows after all blockers are ruled out.
|
|
105
|
+
* The minimum rate is the account's binding window; retain its numbers for explanation.
|
|
106
|
+
*/
|
|
107
|
+
function scoreHeadroom(windows, threshold, now) {
|
|
108
|
+
let score = Infinity;
|
|
109
|
+
let rateWindow = windows[0];
|
|
110
|
+
for (const window of windows) {
|
|
111
|
+
const rate = (threshold - window.percent) / Math.max((window.resetAt - now) / 3600_000, 0.25);
|
|
112
|
+
if (rate < score) {
|
|
113
|
+
score = rate;
|
|
114
|
+
rateWindow = window;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { score, rateWindow, reason: 'headroom_per_hour' };
|
|
118
|
+
}
|
|
104
119
|
/** Pure policy. Unknown is eligible after every known/reactive blocker is ruled out. */
|
|
105
120
|
export function selectSubscriptionAccount(input) {
|
|
106
121
|
if (input.pool.kind !== 'ready')
|
|
@@ -140,14 +155,14 @@ export function selectSubscriptionAccount(input) {
|
|
|
140
155
|
deadlines.push(Math.max(...blockers));
|
|
141
156
|
}
|
|
142
157
|
else if (windows.length && !uncertain && windows.every(w => w.percent !== undefined && w.resetAt !== undefined)) {
|
|
143
|
-
known.push({ account,
|
|
158
|
+
known.push({ account, ...scoreHeadroom(windows, account.useFullQuota ? 100 : USAGE_THRESHOLD_PERCENT, input.now), source: usage.source });
|
|
144
159
|
}
|
|
145
160
|
else {
|
|
146
161
|
unknown.push({ account, uncertainApplicability: uncertain });
|
|
147
162
|
}
|
|
148
163
|
}
|
|
149
164
|
const compare = (a, b) => a.accountId < b.accountId ? -1 : a.accountId > b.accountId ? 1 : 0;
|
|
150
|
-
known.sort((a, b) =>
|
|
165
|
+
known.sort((a, b) => b.score - a.score || Number(b.account.accountId === input.currentAccountId) - Number(a.account.accountId === input.currentAccountId) || compare(a.account, b.account));
|
|
151
166
|
unknown.sort((a, b) => compare(a.account, b.account));
|
|
152
167
|
if (known[0])
|
|
153
168
|
return { kind: 'selected', provenance: 'known', ...known[0] };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ThreadRouting {
|
|
2
|
+
workspaceId: string;
|
|
3
|
+
channelId: string;
|
|
4
|
+
brainId?: string;
|
|
5
|
+
/** Historical evidence only. Recovery always derives the current directory. */
|
|
6
|
+
directory: string;
|
|
7
|
+
threadId?: string;
|
|
8
|
+
processedDropIds?: string[];
|
|
9
|
+
}
|
|
10
|
+
/** Poller state survives inbox pruning and process restarts; contains no credentials. */
|
|
11
|
+
export declare function saveThreadRouting(threadId: string, routing: ThreadRouting): void;
|
|
12
|
+
export declare function readThreadRouting(threadId: string): ThreadRouting | undefined;
|
|
13
|
+
export declare function inboxDropId(threadId: string, filename: string, content: string | Uint8Array): string;
|
|
14
|
+
export declare function recordProcessedDrop(threadId: string, filename: string, content: string | Uint8Array): void;
|
|
15
|
+
export interface StoredThreadRouting {
|
|
16
|
+
key: string;
|
|
17
|
+
threadId?: string;
|
|
18
|
+
modifiedAt: number;
|
|
19
|
+
}
|
|
20
|
+
/** Older records have no threadId; discovery can match their hashed file name. */
|
|
21
|
+
export declare function listThreadRoutings(): StoredThreadRouting[];
|
|
22
|
+
export declare function threadRoutingKey(threadId: string): string;
|
|
23
|
+
export declare function removeThreadRouting(key: string): void;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { config } from './config.js';
|
|
5
|
+
function routingPath(threadId) {
|
|
6
|
+
return join(config.TEAMVIBE_DATA_DIR, 'thread-routing', threadRoutingKey(threadId));
|
|
7
|
+
}
|
|
8
|
+
/** Poller state survives inbox pruning and process restarts; contains no credentials. */
|
|
9
|
+
export function saveThreadRouting(threadId, routing) {
|
|
10
|
+
mkdirSync(join(config.TEAMVIBE_DATA_DIR, 'thread-routing'), { recursive: true, mode: 0o700 });
|
|
11
|
+
const path = routingPath(threadId);
|
|
12
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(temporary, JSON.stringify({ ...readThreadRouting(threadId), ...routing, threadId, updatedAt: Date.now() }), { mode: 0o600 });
|
|
15
|
+
renameSync(temporary, path);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
rmSync(temporary, { force: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function readThreadRouting(threadId) {
|
|
22
|
+
try {
|
|
23
|
+
const routing = JSON.parse(readFileSync(routingPath(threadId), 'utf8'));
|
|
24
|
+
if (typeof routing.workspaceId !== 'string' || typeof routing.channelId !== 'string' || typeof routing.directory !== 'string' || routing.brainId !== undefined && typeof routing.brainId !== 'string')
|
|
25
|
+
return undefined;
|
|
26
|
+
if (routing.processedDropIds !== undefined && (!Array.isArray(routing.processedDropIds) || !routing.processedDropIds.every((id) => typeof id === 'string')))
|
|
27
|
+
return undefined;
|
|
28
|
+
return routing;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function inboxDropId(threadId, filename, content) {
|
|
35
|
+
return createHash('sha256').update(JSON.stringify([threadId, filename, createHash('sha256').update(content).digest('hex')])).digest('hex');
|
|
36
|
+
}
|
|
37
|
+
export function recordProcessedDrop(threadId, filename, content) {
|
|
38
|
+
const routing = readThreadRouting(threadId);
|
|
39
|
+
if (!routing)
|
|
40
|
+
return;
|
|
41
|
+
const id = inboxDropId(threadId, filename, content);
|
|
42
|
+
saveThreadRouting(threadId, { ...routing, processedDropIds: [...new Set([...(routing.processedDropIds ?? []), id])] });
|
|
43
|
+
}
|
|
44
|
+
/** Older records have no threadId; discovery can match their hashed file name. */
|
|
45
|
+
export function listThreadRoutings() {
|
|
46
|
+
const root = join(config.TEAMVIBE_DATA_DIR, 'thread-routing');
|
|
47
|
+
let names;
|
|
48
|
+
try {
|
|
49
|
+
names = readdirSync(root);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
return names.filter(name => /^[a-f0-9]{64}\.json$/.test(name)).flatMap(name => {
|
|
55
|
+
try {
|
|
56
|
+
const path = join(root, name);
|
|
57
|
+
let modifiedAt = statSync(path).mtimeMs;
|
|
58
|
+
let threadId;
|
|
59
|
+
try {
|
|
60
|
+
const value = JSON.parse(readFileSync(path, 'utf8'));
|
|
61
|
+
if (typeof value.updatedAt === 'number' && Number.isFinite(value.updatedAt))
|
|
62
|
+
modifiedAt = value.updatedAt;
|
|
63
|
+
if (typeof value.threadId === 'string' && routingPath(value.threadId) === path)
|
|
64
|
+
threadId = value.threadId;
|
|
65
|
+
}
|
|
66
|
+
catch { /* Corrupt records still expire; match any drops by the hash. */ }
|
|
67
|
+
return [{ key: name, threadId, modifiedAt }];
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function threadRoutingKey(threadId) {
|
|
75
|
+
return `${createHash('sha256').update(threadId).digest('hex')}.json`;
|
|
76
|
+
}
|
|
77
|
+
export function removeThreadRouting(key) {
|
|
78
|
+
if (!/^[a-f0-9]{64}\.json$/.test(key))
|
|
79
|
+
throw new Error('Invalid routing key');
|
|
80
|
+
rmSync(join(config.TEAMVIBE_DATA_DIR, 'thread-routing', key), { force: true });
|
|
81
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { UsageProbeMessage } from './usage-probe-contract.js';
|
|
2
|
+
export type { UsageProbeMessage, UsageProbeFailureReason } from './usage-probe-contract.js';
|
|
1
3
|
export interface ControlMessage {
|
|
2
4
|
id: string;
|
|
3
5
|
source: 'control';
|
|
@@ -69,6 +71,7 @@ export interface TeamVibeQueueMessage {
|
|
|
69
71
|
export interface SubscriptionPin {
|
|
70
72
|
inboxFilename?: string;
|
|
71
73
|
resumePending?: boolean;
|
|
74
|
+
/** Comparison evidence only; replay derives execution paths from current routing. */
|
|
72
75
|
effectiveCwd?: string;
|
|
73
76
|
claudeConfigDir?: string;
|
|
74
77
|
sawSendMessageCall?: boolean;
|
|
@@ -92,7 +95,7 @@ export interface SessionRecord {
|
|
|
92
95
|
last_message_ts?: string;
|
|
93
96
|
ttl: number;
|
|
94
97
|
}
|
|
95
|
-
export type SessionStatus = 'processing' | 'idle' | 'waiting_for_input';
|
|
98
|
+
export type SessionStatus = 'processing' | 'idle' | 'waiting_for_input' | 'completed' | 'failed' | 'aborted';
|
|
96
99
|
export interface SessionMetrics {
|
|
97
100
|
/** Input occupancy of the last assistant message, independent of run totals. */
|
|
98
101
|
context_tokens?: number;
|
|
@@ -107,12 +110,3 @@ export interface SessionMetrics {
|
|
|
107
110
|
cache_creation_input_tokens?: number;
|
|
108
111
|
model?: string;
|
|
109
112
|
}
|
|
110
|
-
export interface UsageProbeMessage {
|
|
111
|
-
kind: 'usage_probe';
|
|
112
|
-
id: string;
|
|
113
|
-
workspaceId: string;
|
|
114
|
-
accountId: string;
|
|
115
|
-
credentialVersion: string;
|
|
116
|
-
requestId: string;
|
|
117
|
-
expiresAt: number;
|
|
118
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Owner-facing account policy location. */
|
|
2
|
-
export declare const FULL_QUOTA_SETTINGS_PATH = "
|
|
2
|
+
export declare const FULL_QUOTA_SETTINGS_PATH = "Connections (account menu \u2192 Use until exhausted)";
|
|
3
3
|
export interface UsageResetTiming {
|
|
4
4
|
resetAt?: number;
|
|
5
5
|
now?: number;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Owner-facing account policy location. */
|
|
2
|
-
export const FULL_QUOTA_SETTINGS_PATH = '
|
|
2
|
+
export const FULL_QUOTA_SETTINGS_PATH = 'Connections (account menu → Use until exhausted)';
|
|
3
3
|
export function formatUsageReset(input) {
|
|
4
4
|
if (input.resetAt === undefined || !Number.isFinite(input.resetAt))
|
|
5
5
|
return undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Shared wire contract for request-scoped usage probes. */
|
|
2
|
+
export declare const ACCOUNT_PROBE_FAILURE_REASONS: readonly ["account_changed", "account_disabled", "account_missing"];
|
|
3
|
+
export declare const USAGE_PROBE_FAILURE_REASONS: readonly ["account_changed", "account_disabled", "account_missing", "token_rejected", "cli_at_capacity", "timeout", "unknown"];
|
|
4
|
+
export type AccountProbeFailureReason = (typeof ACCOUNT_PROBE_FAILURE_REASONS)[number];
|
|
5
|
+
export type UsageProbeFailureReason = (typeof USAGE_PROBE_FAILURE_REASONS)[number];
|
|
6
|
+
export declare const USAGE_UPDATE_TTL_MS: number;
|
|
7
|
+
export interface UsageProbeMessage {
|
|
8
|
+
kind: 'usage_probe';
|
|
9
|
+
id: string;
|
|
10
|
+
workspaceId: string;
|
|
11
|
+
accountId: string;
|
|
12
|
+
credentialVersion: string;
|
|
13
|
+
requestId: string;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Shared wire contract for request-scoped usage probes. */
|
|
2
|
+
export const ACCOUNT_PROBE_FAILURE_REASONS = [
|
|
3
|
+
'account_changed',
|
|
4
|
+
'account_disabled',
|
|
5
|
+
'account_missing',
|
|
6
|
+
];
|
|
7
|
+
export const USAGE_PROBE_FAILURE_REASONS = [
|
|
8
|
+
...ACCOUNT_PROBE_FAILURE_REASONS,
|
|
9
|
+
'token_rejected',
|
|
10
|
+
'cli_at_capacity',
|
|
11
|
+
'timeout',
|
|
12
|
+
'unknown',
|
|
13
|
+
];
|
|
14
|
+
export const USAGE_UPDATE_TTL_MS = 10 * 60_000;
|
package/dist/usage-probe-job.js
CHANGED
|
@@ -40,12 +40,25 @@ export async function handleUsageProbe(job, options) {
|
|
|
40
40
|
const pool = await options.fetchPool();
|
|
41
41
|
const account = pool.kind === 'ready'
|
|
42
42
|
? pool.accounts.find((a) => a.workspaceId === job.workspaceId &&
|
|
43
|
-
a.accountId === job.accountId
|
|
44
|
-
a.credentialVersion === job.credentialVersion &&
|
|
45
|
-
a.enabled)
|
|
43
|
+
a.accountId === job.accountId)
|
|
46
44
|
: undefined;
|
|
47
|
-
if (!account)
|
|
45
|
+
if (!account) {
|
|
46
|
+
result =
|
|
47
|
+
pool.kind === 'probe_account_unavailable'
|
|
48
|
+
? pool.reason
|
|
49
|
+
: pool.kind === 'ready' || pool.kind === 'no_accounts_configured'
|
|
50
|
+
? 'account_missing'
|
|
51
|
+
: 'unknown';
|
|
48
52
|
return;
|
|
53
|
+
}
|
|
54
|
+
if (!account.enabled) {
|
|
55
|
+
result = 'account_disabled';
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (account.credentialVersion !== job.credentialVersion) {
|
|
59
|
+
result = 'account_changed';
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
49
62
|
if (options.isAtCapacity()) {
|
|
50
63
|
result = 'cli_at_capacity';
|
|
51
64
|
return;
|
|
@@ -181,6 +194,6 @@ export async function handleUsageProbe(job, options) {
|
|
|
181
194
|
reason: result,
|
|
182
195
|
});
|
|
183
196
|
if (!duplicate)
|
|
184
|
-
options.log(`[Pool] usage probe
|
|
197
|
+
options.log(`[Pool] usage probe request=${job.requestId} result=${result}`);
|
|
185
198
|
}
|
|
186
199
|
}
|
|
@@ -21,7 +21,8 @@ export declare function cleanupOrphanWorktrees(brainPath: string, maxAgeMs?: num
|
|
|
21
21
|
removed: string[];
|
|
22
22
|
}>;
|
|
23
23
|
/**
|
|
24
|
-
* Sweep orphan worktrees
|
|
24
|
+
* Sweep orphan worktrees under `brainsRoot/<workspaceId>/<brainId>`.
|
|
25
|
+
* Top-level legacy clones are left untouched.
|
|
25
26
|
* Intended to run once at poller startup. Tolerates the entries that aren't
|
|
26
27
|
* brain repos (no `.git` subdir) by skipping them silently.
|
|
27
28
|
*/
|
package/dist/worktree-manager.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { existsSync } from 'fs';
|
|
1
2
|
import { mkdir, readdir, stat, appendFile, readFile } from 'fs/promises';
|
|
2
3
|
import { join } from 'path';
|
|
3
4
|
import { execFile } from 'child_process';
|
|
4
5
|
import { promisify } from 'util';
|
|
6
|
+
import { validateBrainId, resolveBrainDir } from './brain-path.js';
|
|
5
7
|
import { readInboxRuntime } from './inbox-manager.js';
|
|
6
8
|
const execFileAsync = promisify(execFile);
|
|
7
9
|
const WORKTREES_SUBDIR = '.worktrees';
|
|
@@ -112,7 +114,8 @@ export async function cleanupOrphanWorktrees(brainPath, maxAgeMs = 24 * 60 * 60
|
|
|
112
114
|
return { removed };
|
|
113
115
|
}
|
|
114
116
|
/**
|
|
115
|
-
* Sweep orphan worktrees
|
|
117
|
+
* Sweep orphan worktrees under `brainsRoot/<workspaceId>/<brainId>`.
|
|
118
|
+
* Top-level legacy clones are left untouched.
|
|
116
119
|
* Intended to run once at poller startup. Tolerates the entries that aren't
|
|
117
120
|
* brain repos (no `.git` subdir) by skipping them silently.
|
|
118
121
|
*/
|
|
@@ -126,19 +129,44 @@ export async function cleanupOrphanWorktreesForAllBrains(brainsRoot, maxAgeMs =
|
|
|
126
129
|
}
|
|
127
130
|
let scanned = 0;
|
|
128
131
|
let removed = 0;
|
|
129
|
-
for (const
|
|
130
|
-
const brainPath = join(brainsRoot, entry);
|
|
132
|
+
for (const workspace of entries) {
|
|
131
133
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
validateBrainId(workspace);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const workspacePath = join(brainsRoot, workspace);
|
|
140
|
+
// A top-level repo is a legacy clone and must remain untouched.
|
|
141
|
+
if (existsSync(join(workspacePath, '.git')))
|
|
142
|
+
continue;
|
|
143
|
+
let brains;
|
|
144
|
+
try {
|
|
145
|
+
brains = await readdir(workspacePath);
|
|
135
146
|
}
|
|
136
147
|
catch {
|
|
137
|
-
continue;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const brain of brains) {
|
|
151
|
+
let brainPath;
|
|
152
|
+
try {
|
|
153
|
+
brainPath = resolveBrainDir(workspace, brain, brainsRoot);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const gitStat = await stat(join(brainPath, '.git'));
|
|
160
|
+
if (!gitStat.isDirectory())
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
scanned++;
|
|
167
|
+
const { removed: gone } = await cleanupOrphanWorktrees(brainPath, maxAgeMs);
|
|
168
|
+
removed += gone.length;
|
|
138
169
|
}
|
|
139
|
-
scanned++;
|
|
140
|
-
const { removed: gone } = await cleanupOrphanWorktrees(brainPath, maxAgeMs);
|
|
141
|
-
removed += gone.length;
|
|
142
170
|
}
|
|
143
171
|
return { scanned, removed };
|
|
144
172
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamvibe/poller",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4-rc.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@teamvibe/poller",
|
|
9
|
-
"version": "0.8.
|
|
9
|
+
"version": "0.8.4-rc.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@aws-sdk/client-dynamodb": "3.1108.0",
|
|
12
12
|
"@aws-sdk/client-sqs": "3.1108.0",
|