@primitive.ai/prim 0.1.0-alpha.40 → 0.1.0-alpha.42
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 +64 -6
- package/SKILL.md +1 -1
- package/dist/{chunk-UWAJCF7P.js → chunk-BSZGI5RL.js} +1 -1
- package/dist/{chunk-S2O4P4A3.js → chunk-F5QCFBJ6.js} +14 -3
- package/dist/{chunk-HSZPTVKU.js → chunk-F7O7V6ZM.js} +88 -12
- package/dist/chunk-IMAIBPUC.js +210 -0
- package/dist/chunk-NDIK4FBF.js +184 -0
- package/dist/{chunk-H4OR42TJ.js → chunk-OKUXEBPT.js} +149 -36
- package/dist/{chunk-AAGJFO7C.js → chunk-QY75BQMF.js} +25 -1
- package/dist/chunk-S5NUHFAD.js +55 -0
- package/dist/{chunk-26VA3ADF.js → chunk-W6PAKEDO.js} +32 -6
- package/dist/daemon/server.js +1150 -77
- package/dist/hooks/post-commit.js +5 -5
- package/dist/hooks/post-tool-use.js +31 -19
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +3 -5
- package/dist/hooks/prim-hook.js +77 -20
- package/dist/hooks/session-start.js +79 -24
- package/dist/index.js +1610 -320
- package/dist/statusline-main.js +248 -0
- package/package.json +6 -5
- package/dist/chunk-LUPD2JSH.js +0 -78
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getClient
|
|
3
|
+
} from "./chunk-W6PAKEDO.js";
|
|
4
|
+
|
|
5
|
+
// src/decisions/feedback.ts
|
|
6
|
+
var FEEDBACK_PROTOCOL_VERSION = 1;
|
|
7
|
+
var FEEDBACK_DEADLINE_MS = 3e3;
|
|
8
|
+
var MAX_FEEDBACK_EVENTS = 40;
|
|
9
|
+
var MAX_FEEDBACK_MESSAGE_CODE_POINTS = 8e3;
|
|
10
|
+
var MAX_FEEDBACK_INTENT_CODE_POINTS = 180;
|
|
11
|
+
var LEASE_PATH = "/api/cli/decisions/feedback/lease";
|
|
12
|
+
var ACK_PATH = "/api/cli/decisions/feedback/ack";
|
|
13
|
+
var STATUS_PATH = "/api/cli/decisions/feedback/status";
|
|
14
|
+
var SHORT_ID = /^[0-9a-f]{8}$/u;
|
|
15
|
+
var MAX_EVENT_ID_CHARS = 128;
|
|
16
|
+
var MAX_RAW_INTENT_CODE_UNITS = 512;
|
|
17
|
+
function isRecord(value) {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
function isUnsafeControl(point) {
|
|
21
|
+
const c0OrC1 = point <= 31 || point >= 127 && point <= 159;
|
|
22
|
+
const bidiControl = point === 1564 || point === 8206 || point === 8207 || point >= 8234 && point <= 8238 || point >= 8294 && point <= 8297;
|
|
23
|
+
return c0OrC1 || bidiControl;
|
|
24
|
+
}
|
|
25
|
+
function replaceIsolatedSurrogates(value) {
|
|
26
|
+
let out = "";
|
|
27
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
28
|
+
const unit = value.charCodeAt(index);
|
|
29
|
+
if (unit >= 55296 && unit <= 56319) {
|
|
30
|
+
const next = value.charCodeAt(index + 1);
|
|
31
|
+
if (next >= 56320 && next <= 57343) {
|
|
32
|
+
out += value[index] + value[index + 1];
|
|
33
|
+
index += 1;
|
|
34
|
+
} else {
|
|
35
|
+
out += "\uFFFD";
|
|
36
|
+
}
|
|
37
|
+
} else if (unit >= 56320 && unit <= 57343) {
|
|
38
|
+
out += "\uFFFD";
|
|
39
|
+
} else {
|
|
40
|
+
out += value[index];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function normalizeFeedbackIntent(value) {
|
|
46
|
+
const safeUnicode = replaceIsolatedSurrogates(value);
|
|
47
|
+
const whitespace = safeUnicode.replace(/\s+/gu, " ");
|
|
48
|
+
const withoutControls = Array.from(whitespace).filter((character) => !isUnsafeControl(character.codePointAt(0) ?? 0)).join("").replace(/\s+/gu, " ").trim();
|
|
49
|
+
const points = Array.from(withoutControls);
|
|
50
|
+
if (points.length <= MAX_FEEDBACK_INTENT_CODE_POINTS) {
|
|
51
|
+
return withoutControls;
|
|
52
|
+
}
|
|
53
|
+
return `${points.slice(0, MAX_FEEDBACK_INTENT_CODE_POINTS - 1).join("")}\u2026`;
|
|
54
|
+
}
|
|
55
|
+
function parseEvent(value) {
|
|
56
|
+
if (!isRecord(value)) return void 0;
|
|
57
|
+
if (typeof value.eventId !== "string" || value.eventId.length === 0 || value.eventId.length > MAX_EVENT_ID_CHARS || Array.from(value.eventId).some((character) => isUnsafeControl(character.codePointAt(0) ?? 0))) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
if (!Number.isSafeInteger(value.leaseVersion) || Number(value.leaseVersion) < 1) {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value.shortId !== "string" || !SHORT_ID.test(value.shortId)) {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value.intent !== "string" || value.intent.length > MAX_RAW_INTENT_CODE_UNITS) {
|
|
67
|
+
return void 0;
|
|
68
|
+
}
|
|
69
|
+
const intent = normalizeFeedbackIntent(value.intent);
|
|
70
|
+
if (!intent) return void 0;
|
|
71
|
+
return {
|
|
72
|
+
eventId: value.eventId,
|
|
73
|
+
leaseVersion: Number(value.leaseVersion),
|
|
74
|
+
shortId: value.shortId,
|
|
75
|
+
intent
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function parseFeedbackLease(value) {
|
|
79
|
+
if (!isRecord(value) || value.protocolVersion !== FEEDBACK_PROTOCOL_VERSION) return void 0;
|
|
80
|
+
if (value.status === "empty") {
|
|
81
|
+
return value.hasMore === false ? { events: [], hasMore: false } : void 0;
|
|
82
|
+
}
|
|
83
|
+
if (value.status === "unavailable") {
|
|
84
|
+
return value.reason === "organization_unbound" ? { events: [], hasMore: false } : void 0;
|
|
85
|
+
}
|
|
86
|
+
if (value.status !== "leased" || typeof value.hasMore !== "boolean") return void 0;
|
|
87
|
+
if (!Array.isArray(value.events) || value.events.length === 0 || value.events.length > MAX_FEEDBACK_EVENTS) {
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
const events = value.events.map(parseEvent);
|
|
91
|
+
if (events.some((event) => event === void 0)) return void 0;
|
|
92
|
+
const parsedEvents = events;
|
|
93
|
+
if (new Set(parsedEvents.map((event) => event.eventId)).size !== parsedEvents.length) {
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
return { events: parsedEvents, hasMore: value.hasMore };
|
|
97
|
+
}
|
|
98
|
+
function renderFeedback(lease) {
|
|
99
|
+
const lines = [];
|
|
100
|
+
const deliveries = [];
|
|
101
|
+
let pointCount = 0;
|
|
102
|
+
for (const event of lease.events) {
|
|
103
|
+
const line = `[prim] response \u2192 created Decision (dec_${event.shortId}): ${event.intent}`;
|
|
104
|
+
const extra = Array.from(line).length + (lines.length === 0 ? 0 : 1);
|
|
105
|
+
if (pointCount + extra > MAX_FEEDBACK_MESSAGE_CODE_POINTS) break;
|
|
106
|
+
lines.push(line);
|
|
107
|
+
deliveries.push({ eventId: event.eventId, leaseVersion: event.leaseVersion });
|
|
108
|
+
pointCount += extra;
|
|
109
|
+
}
|
|
110
|
+
if (lines.length === 0) return void 0;
|
|
111
|
+
return { systemMessage: lines.join("\n"), deliveries };
|
|
112
|
+
}
|
|
113
|
+
async function leaseDecisionFeedback(input, dependencies = {}) {
|
|
114
|
+
try {
|
|
115
|
+
const response = await (dependencies.client ?? getClient()).post(
|
|
116
|
+
LEASE_PATH,
|
|
117
|
+
{
|
|
118
|
+
protocolVersion: FEEDBACK_PROTOCOL_VERSION,
|
|
119
|
+
workspaceId: input.workspaceId,
|
|
120
|
+
currentSessionId: input.currentSessionId
|
|
121
|
+
},
|
|
122
|
+
{ signal: input.signal, quietRefresh: true }
|
|
123
|
+
);
|
|
124
|
+
const parsed = parseFeedbackLease(response);
|
|
125
|
+
if (!parsed) {
|
|
126
|
+
dependencies.onError?.(
|
|
127
|
+
new Error("server returned an unsupported decision-feedback contract")
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
return parsed;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
dependencies.onError?.(error);
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function acknowledgeDecisionFeedback(input, dependencies = {}) {
|
|
137
|
+
if (input.deliveries.length === 0 || input.deliveries.length > MAX_FEEDBACK_EVENTS) return false;
|
|
138
|
+
try {
|
|
139
|
+
const response = await (dependencies.client ?? getClient()).post(
|
|
140
|
+
ACK_PATH,
|
|
141
|
+
{
|
|
142
|
+
protocolVersion: FEEDBACK_PROTOCOL_VERSION,
|
|
143
|
+
workspaceId: input.workspaceId,
|
|
144
|
+
deliveries: input.deliveries
|
|
145
|
+
},
|
|
146
|
+
{ signal: input.signal, quietRefresh: true }
|
|
147
|
+
);
|
|
148
|
+
const acknowledgedEventIds = isRecord(response) ? response.acknowledgedEventIds : void 0;
|
|
149
|
+
const expectedEventIds = new Set(input.deliveries.map((delivery) => delivery.eventId));
|
|
150
|
+
const valid = isRecord(response) && response.protocolVersion === FEEDBACK_PROTOCOL_VERSION && response.status === "acked" && Array.isArray(acknowledgedEventIds) && acknowledgedEventIds.length <= input.deliveries.length && acknowledgedEventIds.every(
|
|
151
|
+
(id) => typeof id === "string" && id.length > 0 && id.length <= MAX_EVENT_ID_CHARS && expectedEventIds.has(id)
|
|
152
|
+
) && new Set(acknowledgedEventIds).size === acknowledgedEventIds.length;
|
|
153
|
+
if (!valid) {
|
|
154
|
+
dependencies.onError?.(
|
|
155
|
+
new Error("server returned an unsupported decision-feedback acknowledgement")
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return valid;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
dependencies.onError?.(error);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function parseFeedbackCapability(value) {
|
|
165
|
+
if (!isRecord(value) || value.protocolVersion !== FEEDBACK_PROTOCOL_VERSION) return void 0;
|
|
166
|
+
if (value.status === "available") return { status: "available" };
|
|
167
|
+
if (value.status === "unavailable" && value.reason === "organization_unbound") {
|
|
168
|
+
return { status: "unavailable", reason: "organization_unbound" };
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
171
|
+
}
|
|
172
|
+
async function fetchFeedbackCapability(signal, client = getClient()) {
|
|
173
|
+
const result = parseFeedbackCapability(await client.get(STATUS_PATH, { signal }));
|
|
174
|
+
if (!result) throw new Error("server returned an unsupported decision-feedback contract");
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export {
|
|
179
|
+
FEEDBACK_DEADLINE_MS,
|
|
180
|
+
renderFeedback,
|
|
181
|
+
leaseDecisionFeedback,
|
|
182
|
+
acknowledgeDecisionFeedback,
|
|
183
|
+
fetchFeedbackCapability
|
|
184
|
+
};
|
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getSiteUrl
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-W6PAKEDO.js";
|
|
4
4
|
|
|
5
5
|
// src/journal.ts
|
|
6
6
|
import {
|
|
7
7
|
appendFileSync,
|
|
8
|
+
closeSync,
|
|
8
9
|
existsSync,
|
|
10
|
+
fstatSync,
|
|
9
11
|
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
opendirSync,
|
|
10
14
|
readFileSync,
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
readSync,
|
|
16
|
+
readdirSync
|
|
13
17
|
} from "fs";
|
|
14
18
|
import { homedir } from "os";
|
|
15
19
|
import { dirname, join } from "path";
|
|
16
20
|
var JOURNAL_DIR = join(homedir(), ".config", "prim", "moves");
|
|
17
21
|
var UNBOUND_BUCKET = "_unbound";
|
|
18
22
|
var JOURNAL_BASENAME = "journal.ndjson";
|
|
23
|
+
var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
|
|
24
|
+
var PENDING_STATS_MAX_FILES = 128;
|
|
19
25
|
function envSlug(apiUrl) {
|
|
20
26
|
const slug = apiUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
21
27
|
if (slug.length === 0 || slug === "." || slug === "..") {
|
|
@@ -48,11 +54,7 @@ function journalPath(orgId) {
|
|
|
48
54
|
function appendMove(move, orgId) {
|
|
49
55
|
appendMoveToPath(journalPath(orgId), move);
|
|
50
56
|
}
|
|
51
|
-
function
|
|
52
|
-
if (!existsSync(path)) {
|
|
53
|
-
return [];
|
|
54
|
-
}
|
|
55
|
-
const content = readFileSync(path, "utf-8");
|
|
57
|
+
function parseMoves(content) {
|
|
56
58
|
const moves = [];
|
|
57
59
|
for (const line of content.split("\n")) {
|
|
58
60
|
if (line.length === 0) {
|
|
@@ -65,6 +67,12 @@ function readMovesFromPath(path) {
|
|
|
65
67
|
}
|
|
66
68
|
return moves;
|
|
67
69
|
}
|
|
70
|
+
function readMovesFromPath(path) {
|
|
71
|
+
if (!existsSync(path)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return parseMoves(readFileSync(path, "utf-8"));
|
|
75
|
+
}
|
|
68
76
|
function listBuckets() {
|
|
69
77
|
const out = [];
|
|
70
78
|
const envDir = currentEnvDir();
|
|
@@ -83,60 +91,164 @@ function listBuckets() {
|
|
|
83
91
|
return out;
|
|
84
92
|
}
|
|
85
93
|
var FLUSHING_PREFIX = `${JOURNAL_BASENAME}.flushing.`;
|
|
94
|
+
function oldestCapturedAt(moves) {
|
|
95
|
+
let oldest;
|
|
96
|
+
for (const move of moves) {
|
|
97
|
+
if (!Number.isFinite(move.capturedAt)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
oldest = oldest === void 0 ? move.capturedAt : Math.min(oldest, move.capturedAt);
|
|
101
|
+
}
|
|
102
|
+
return oldest;
|
|
103
|
+
}
|
|
104
|
+
function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
|
|
105
|
+
const fd = openSync(path, "r");
|
|
106
|
+
try {
|
|
107
|
+
const initial = fstatSync(fd);
|
|
108
|
+
const target = Math.min(initial.size, Math.max(0, maxBytes));
|
|
109
|
+
const buffer = Buffer.allocUnsafe(target);
|
|
110
|
+
let sampledBytes = 0;
|
|
111
|
+
while (sampledBytes < target) {
|
|
112
|
+
const count = readSync(fd, buffer, sampledBytes, target - sampledBytes, sampledBytes);
|
|
113
|
+
if (count === 0) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
sampledBytes += count;
|
|
117
|
+
}
|
|
118
|
+
const stat = fstatSync(fd);
|
|
119
|
+
const sampled = stat.size > sampledBytes;
|
|
120
|
+
let content = buffer.subarray(0, sampledBytes).toString("utf-8");
|
|
121
|
+
if (sampled) {
|
|
122
|
+
const lastNewline = content.lastIndexOf("\n");
|
|
123
|
+
content = lastNewline === -1 ? "" : content.slice(0, lastNewline + 1);
|
|
124
|
+
}
|
|
125
|
+
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
126
|
+
const moves = parseMoves(content);
|
|
127
|
+
return {
|
|
128
|
+
sizeBytes: stat.size,
|
|
129
|
+
mtimeMs: stat.mtimeMs,
|
|
130
|
+
lineCount: sampled && stat.size > 0 ? Math.max(1, lines.length) : lines.length,
|
|
131
|
+
oldestCapturedAt: oldestCapturedAt(moves),
|
|
132
|
+
sampled,
|
|
133
|
+
sampledBytes
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
86
139
|
function parseFlushingPid(name) {
|
|
87
140
|
const segments = name.slice(FLUSHING_PREFIX.length).split(".");
|
|
88
141
|
const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
|
|
89
142
|
return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
|
|
90
143
|
}
|
|
91
|
-
function listFlushingInDir(dir, bucket) {
|
|
144
|
+
function listFlushingInDir(dir, bucket, options = {}) {
|
|
92
145
|
if (!existsSync(dir)) {
|
|
93
146
|
return [];
|
|
94
147
|
}
|
|
95
148
|
const out = [];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
|
|
150
|
+
const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
151
|
+
const entries = opendirSync(dir);
|
|
152
|
+
try {
|
|
153
|
+
let entry = entries.readSync();
|
|
154
|
+
while (entry && out.length < maxFiles) {
|
|
155
|
+
if (!entry.isFile() || !entry.name.startsWith(FLUSHING_PREFIX)) {
|
|
156
|
+
entry = entries.readSync();
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const path = join(dir, entry.name);
|
|
160
|
+
let sample;
|
|
161
|
+
try {
|
|
162
|
+
sample = sampleJournalFile(path, remainingBytes);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (err.code === "ENOENT") {
|
|
165
|
+
entry = entries.readSync();
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
remainingBytes = Math.max(0, remainingBytes - sample.sampledBytes);
|
|
171
|
+
out.push({
|
|
172
|
+
bucket,
|
|
173
|
+
path,
|
|
174
|
+
pid: parseFlushingPid(entry.name),
|
|
175
|
+
...sample
|
|
176
|
+
});
|
|
177
|
+
entry = entries.readSync();
|
|
99
178
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const lineCount = readFileSync(path, "utf-8").split("\n").filter((l) => l.length > 0).length;
|
|
103
|
-
out.push({
|
|
104
|
-
bucket,
|
|
105
|
-
path,
|
|
106
|
-
pid: parseFlushingPid(entry.name),
|
|
107
|
-
sizeBytes: stat.size,
|
|
108
|
-
mtimeMs: stat.mtimeMs,
|
|
109
|
-
lineCount
|
|
110
|
-
});
|
|
179
|
+
} finally {
|
|
180
|
+
entries.closeSync();
|
|
111
181
|
}
|
|
112
182
|
return out;
|
|
113
183
|
}
|
|
114
|
-
function listFlushing() {
|
|
184
|
+
function listFlushing(options = {}) {
|
|
115
185
|
const envDir = currentEnvDir();
|
|
116
186
|
if (!existsSync(envDir)) {
|
|
117
187
|
return [];
|
|
118
188
|
}
|
|
119
189
|
const out = [];
|
|
190
|
+
let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
|
|
191
|
+
const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
120
192
|
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
193
|
+
if (out.length >= maxFiles) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
121
196
|
if (entry.isDirectory()) {
|
|
122
|
-
|
|
197
|
+
const found = listFlushingInDir(join(envDir, entry.name), entry.name, {
|
|
198
|
+
sampleBytes: remainingBytes,
|
|
199
|
+
maxFiles: maxFiles - out.length
|
|
200
|
+
});
|
|
201
|
+
out.push(...found);
|
|
202
|
+
remainingBytes = Math.max(
|
|
203
|
+
0,
|
|
204
|
+
remainingBytes - found.reduce((count, file) => count + file.sampledBytes, 0)
|
|
205
|
+
);
|
|
123
206
|
}
|
|
124
207
|
}
|
|
125
208
|
return out;
|
|
126
209
|
}
|
|
127
|
-
function bucketStats() {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
210
|
+
function bucketStats(options = {}) {
|
|
211
|
+
const out = [];
|
|
212
|
+
let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
|
|
213
|
+
const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
214
|
+
for (const { bucket, path } of listBuckets()) {
|
|
215
|
+
if (out.length >= maxFiles) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
let sample;
|
|
219
|
+
try {
|
|
220
|
+
sample = sampleJournalFile(path, remainingBytes);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.code === "ENOENT") {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
remainingBytes = Math.max(0, remainingBytes - sample.sampledBytes);
|
|
228
|
+
out.push({
|
|
133
229
|
bucket,
|
|
134
230
|
path,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
231
|
+
...sample
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
function pendingJournalStats() {
|
|
237
|
+
const perKindBytes = Math.floor(JOURNAL_STATS_SAMPLE_BYTES / 2);
|
|
238
|
+
const perKindFiles = Math.floor(PENDING_STATS_MAX_FILES / 2);
|
|
239
|
+
const live = bucketStats({ sampleBytes: perKindBytes, maxFiles: perKindFiles });
|
|
240
|
+
const stranded = listFlushing({ sampleBytes: perKindBytes, maxFiles: perKindFiles });
|
|
241
|
+
const timestamps = [...live, ...stranded].map((entry) => entry.oldestCapturedAt).filter((value) => value !== void 0);
|
|
242
|
+
const liveSampled = live.some((entry) => entry.sampled) || live.length >= perKindFiles;
|
|
243
|
+
const strandedSampled = stranded.some((entry) => entry.sampled) || stranded.length >= perKindFiles;
|
|
244
|
+
return {
|
|
245
|
+
pendingCount: live.reduce((count, entry) => count + entry.lineCount, 0) + stranded.reduce((count, entry) => count + entry.lineCount, 0),
|
|
246
|
+
oldestPendingAt: timestamps.length > 0 ? Math.min(...timestamps) : void 0,
|
|
247
|
+
strandedCount: stranded.reduce((count, entry) => count + entry.lineCount, 0),
|
|
248
|
+
strandedFileCount: stranded.length,
|
|
249
|
+
sampled: liveSampled || strandedSampled,
|
|
250
|
+
strandedSampled
|
|
251
|
+
};
|
|
140
252
|
}
|
|
141
253
|
|
|
142
254
|
// src/binding.ts
|
|
@@ -248,6 +360,7 @@ export {
|
|
|
248
360
|
listBuckets,
|
|
249
361
|
listFlushing,
|
|
250
362
|
bucketStats,
|
|
363
|
+
pendingJournalStats,
|
|
251
364
|
SESSIONS_DIR,
|
|
252
365
|
resolveOrg
|
|
253
366
|
};
|
|
@@ -68,11 +68,35 @@ function stripAnsi(text) {
|
|
|
68
68
|
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// src/ingest-response.ts
|
|
72
|
+
var IngestAcknowledgementError = class extends Error {
|
|
73
|
+
constructor(message) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "IngestAcknowledgementError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function requireDurableIngestAcknowledgement(value, expected) {
|
|
79
|
+
if (typeof value !== "object" || value === null) {
|
|
80
|
+
throw new IngestAcknowledgementError("ingest response was not an object");
|
|
81
|
+
}
|
|
82
|
+
const response = value;
|
|
83
|
+
if (response.disposition !== "persisted") {
|
|
84
|
+
throw new IngestAcknowledgementError("ingest response did not confirm persistence");
|
|
85
|
+
}
|
|
86
|
+
if (response.acknowledged !== expected) {
|
|
87
|
+
throw new IngestAcknowledgementError(
|
|
88
|
+
`ingest acknowledged ${String(response.acknowledged)} of ${String(expected)} moves`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
71
94
|
export {
|
|
72
95
|
color,
|
|
73
96
|
dim,
|
|
74
97
|
bold,
|
|
75
98
|
colorForArea,
|
|
76
99
|
stripControlChars,
|
|
77
|
-
stripAnsi
|
|
100
|
+
stripAnsi,
|
|
101
|
+
requireDurableIngestAcknowledgement
|
|
78
102
|
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// src/hooks/decision-feedback-core.ts
|
|
2
|
+
function buildHookOutput(options) {
|
|
3
|
+
const output = {};
|
|
4
|
+
if (options.systemMessage) output.systemMessage = options.systemMessage;
|
|
5
|
+
if (options.additionalContext) {
|
|
6
|
+
output.hookSpecificOutput = {
|
|
7
|
+
hookEventName: "SessionStart",
|
|
8
|
+
additionalContext: options.additionalContext
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return output;
|
|
12
|
+
}
|
|
13
|
+
function writeHookOutput(output, stream = process.stdout) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
let settled = false;
|
|
16
|
+
const finish = (ok, retainErrorListener = false) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
settled = true;
|
|
19
|
+
if (!retainErrorListener) stream.off("error", onError);
|
|
20
|
+
resolve(ok);
|
|
21
|
+
};
|
|
22
|
+
const onError = () => {
|
|
23
|
+
if (settled) {
|
|
24
|
+
stream.off("error", onError);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
finish(false);
|
|
28
|
+
};
|
|
29
|
+
stream.once("error", onError);
|
|
30
|
+
try {
|
|
31
|
+
stream.write(
|
|
32
|
+
`${JSON.stringify(output)}
|
|
33
|
+
`,
|
|
34
|
+
(error) => error ? finish(false, true) : finish(true)
|
|
35
|
+
);
|
|
36
|
+
} catch {
|
|
37
|
+
finish(false);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function handoffHookOutput(output, acknowledge, stream = process.stdout) {
|
|
42
|
+
const handedOff = await writeHookOutput(output, stream);
|
|
43
|
+
if (handedOff && acknowledge) {
|
|
44
|
+
try {
|
|
45
|
+
await acknowledge();
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return handedOff;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
buildHookOutput,
|
|
54
|
+
handoffHookOutput
|
|
55
|
+
};
|
|
@@ -79,7 +79,7 @@ function getSiteUrl() {
|
|
|
79
79
|
}
|
|
80
80
|
return DEFAULT_API_URL;
|
|
81
81
|
}
|
|
82
|
-
async function
|
|
82
|
+
async function performTokenRefresh(options = {}) {
|
|
83
83
|
if (!existsSync(REFRESH_TOKEN_PATH)) {
|
|
84
84
|
return void 0;
|
|
85
85
|
}
|
|
@@ -91,9 +91,11 @@ async function refreshToken() {
|
|
|
91
91
|
const response = await fetch(`${siteUrl}/mcp/broker/refresh`, {
|
|
92
92
|
method: "POST",
|
|
93
93
|
headers: { "Content-Type": "application/json" },
|
|
94
|
-
body: JSON.stringify({ refresh_token: refreshTokenValue })
|
|
94
|
+
body: JSON.stringify({ refresh_token: refreshTokenValue }),
|
|
95
|
+
signal: options.signal
|
|
95
96
|
});
|
|
96
97
|
if (!response.ok) {
|
|
98
|
+
if (options.quiet) return void 0;
|
|
97
99
|
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
98
100
|
process.stderr.write(
|
|
99
101
|
`[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
|
|
@@ -112,6 +114,24 @@ async function refreshToken() {
|
|
|
112
114
|
saveTokenExpiry(data.access_token, data.expires_in);
|
|
113
115
|
return data.access_token;
|
|
114
116
|
}
|
|
117
|
+
var _refreshInFlight;
|
|
118
|
+
function refreshToken(options = {}) {
|
|
119
|
+
if (_refreshInFlight) {
|
|
120
|
+
return _refreshInFlight;
|
|
121
|
+
}
|
|
122
|
+
const attempt = performTokenRefresh(options).then((token) => {
|
|
123
|
+
if (token) {
|
|
124
|
+
_cachedToken = token;
|
|
125
|
+
}
|
|
126
|
+
return token;
|
|
127
|
+
}).finally(() => {
|
|
128
|
+
if (_refreshInFlight === attempt) {
|
|
129
|
+
_refreshInFlight = void 0;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
_refreshInFlight = attempt;
|
|
133
|
+
return attempt;
|
|
134
|
+
}
|
|
115
135
|
var HttpError = class extends Error {
|
|
116
136
|
status;
|
|
117
137
|
constructor(status, message) {
|
|
@@ -128,7 +148,10 @@ async function request(method, path, body, options) {
|
|
|
128
148
|
_cachedToken = getAuthToken();
|
|
129
149
|
}
|
|
130
150
|
if (_cachedToken && isTokenExpiringSoon()) {
|
|
131
|
-
const newToken = await refreshToken(
|
|
151
|
+
const newToken = await refreshToken({
|
|
152
|
+
signal: options?.signal,
|
|
153
|
+
quiet: options?.quietRefresh
|
|
154
|
+
});
|
|
132
155
|
if (newToken) {
|
|
133
156
|
_cachedToken = newToken;
|
|
134
157
|
}
|
|
@@ -147,9 +170,13 @@ async function request(method, path, body, options) {
|
|
|
147
170
|
signal: options?.signal
|
|
148
171
|
});
|
|
149
172
|
};
|
|
150
|
-
|
|
173
|
+
const tokenUsed = _cachedToken;
|
|
174
|
+
let res = await doFetch(tokenUsed);
|
|
151
175
|
if (res.status === 401) {
|
|
152
|
-
const newToken = await refreshToken(
|
|
176
|
+
const newToken = _cachedToken && _cachedToken !== tokenUsed ? _cachedToken : await refreshToken({
|
|
177
|
+
signal: options?.signal,
|
|
178
|
+
quiet: options?.quietRefresh
|
|
179
|
+
});
|
|
153
180
|
if (newToken) {
|
|
154
181
|
_cachedToken = newToken;
|
|
155
182
|
res = await doFetch(newToken);
|
|
@@ -179,7 +206,6 @@ export {
|
|
|
179
206
|
getTokenExpiresAt,
|
|
180
207
|
getAuthToken,
|
|
181
208
|
getSiteUrl,
|
|
182
|
-
refreshToken,
|
|
183
209
|
HttpError,
|
|
184
210
|
getClient
|
|
185
211
|
};
|