@lore-co/opencode 0.1.17 → 0.1.18
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/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +815 -41
- package/dist/index.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -5
package/dist/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { homedir } from "node:os";
|
|
2
3
|
import { isAbsolute, relative } from "node:path";
|
|
3
|
-
import { GenericAgentAdapter, injectSharedMemory, } from "@lore-co/adapter-generic";
|
|
4
|
-
import { redactSensitiveText, stripLoreInjectedContext, } from "@lore-co/core";
|
|
4
|
+
import { GenericAgentAdapter, normalizeHostScope, toHostTask, injectSharedMemory, } from "@lore-co/adapter-generic";
|
|
5
|
+
import { canonicalJson, formatExplanation, redactUnknown, redactSensitiveText, stripLoreInjectedContext, } from "@lore-co/core";
|
|
6
|
+
import { ReliabilityStore, classifyRetryFailure, createIntegrationInvocationAttempt, recordLocalGuardMetric, recordLocalRetrievalMetric, } from "@lore-co/cli/reliability-store";
|
|
7
|
+
import { writeInvocationAttemptBounded, writeInvocationCompletionBounded, } from "@lore-co/cli/invocation-health-writer";
|
|
8
|
+
import { SignedSnapshotError, verifySignedSnapshot, } from "@lore-co/cli/signed-snapshot";
|
|
9
|
+
import { CONTEXT_CACHE_MAX_STALE_MS, isContextTransportFailure, selectCachedContext, } from "@lore-co/cli/context-fallback";
|
|
5
10
|
import { boundedTimeout, createBoundedFetch, resolveLoreCredentials, withTimeout, } from "./config.js";
|
|
6
11
|
import { canonicalRepositoryScope, repositoryScopeFromWorktree, } from "./repository.js";
|
|
12
|
+
import { OPENCODE_PLUGIN_VERSION } from "./version.js";
|
|
7
13
|
export { DEFAULT_TIMEOUT_MS, MAX_CONFIG_BYTES, MAX_RESPONSE_BYTES, MAX_TIMEOUT_MS, MIN_TIMEOUT_MS, boundedTimeout, createBoundedFetch, resolveLoreCredentials, } from "./config.js";
|
|
8
14
|
export { canonicalRepositoryScope, repositoryScopeFromWorktree, } from "./repository.js";
|
|
15
|
+
export { OPENCODE_PLUGIN_VERSION } from "./version.js";
|
|
16
|
+
export { OPENCODE_RELIABILITY_INTEGRATION, } from "@lore-co/core";
|
|
9
17
|
export const MAX_CONTEXT_BYTES = 12_000;
|
|
10
18
|
export const MAX_MESSAGE_BYTES = 100_000;
|
|
11
19
|
export const MAX_SESSIONS = 256;
|
|
@@ -16,6 +24,9 @@ const MAX_METADATA_BYTES = 500;
|
|
|
16
24
|
const MAX_PARTS_PER_MESSAGE = 200;
|
|
17
25
|
const MAX_MESSAGE_SCAN = 20;
|
|
18
26
|
const CONNECTOR = "lore-opencode-plugin";
|
|
27
|
+
const TRUST_KEY_REFRESH_TTL_MS = 60 * 60_000;
|
|
28
|
+
const TRUST_KEY_REFRESH_STATE_KEY = "snapshot-trust-keys";
|
|
29
|
+
const CONTEXT_NOTICE_COOLDOWN_MS = 15 * 60_000;
|
|
19
30
|
function utf8Length(value) {
|
|
20
31
|
return Buffer.byteLength(value, "utf8");
|
|
21
32
|
}
|
|
@@ -101,6 +112,83 @@ function contextText(value) {
|
|
|
101
112
|
}
|
|
102
113
|
return truncateUtf8(redactSensitiveText(value).text.trim(), MAX_CONTEXT_BYTES);
|
|
103
114
|
}
|
|
115
|
+
function objectValue(value) {
|
|
116
|
+
return typeof value === "object" &&
|
|
117
|
+
value !== null &&
|
|
118
|
+
!Array.isArray(value)
|
|
119
|
+
? value
|
|
120
|
+
: null;
|
|
121
|
+
}
|
|
122
|
+
function contextRetrievalMetadata(value, payload) {
|
|
123
|
+
const metadata = objectValue(value);
|
|
124
|
+
const freshness = objectValue(metadata?.freshness);
|
|
125
|
+
const policy = objectValue(metadata?.policy);
|
|
126
|
+
const reasons = Array.isArray(metadata?.reasons)
|
|
127
|
+
? metadata.reasons.filter((reason) => typeof reason === "string")
|
|
128
|
+
: [];
|
|
129
|
+
const source = metadata?.source;
|
|
130
|
+
const fallback = metadata?.fallback;
|
|
131
|
+
const live = source === "live" &&
|
|
132
|
+
freshness?.state === "fresh" &&
|
|
133
|
+
policy?.source === "live" &&
|
|
134
|
+
policy.loadedAt === payload.issuedAt &&
|
|
135
|
+
Date.now() < Date.parse(payload.validUntil);
|
|
136
|
+
const cached = source === "cache" &&
|
|
137
|
+
fallback === "cached_context" &&
|
|
138
|
+
metadata?.status === "degraded" &&
|
|
139
|
+
(freshness?.state === "fresh" || freshness?.state === "stale") &&
|
|
140
|
+
policy?.source === "cache" &&
|
|
141
|
+
policy.loadedAt === payload.asOf &&
|
|
142
|
+
reasons.includes("cached_context");
|
|
143
|
+
if (metadata?.contractVersion !== "reliability-v1" ||
|
|
144
|
+
metadata.operation !== "retrieval" ||
|
|
145
|
+
metadata.requestId !== payload.requestId ||
|
|
146
|
+
freshness?.asOf !== payload.asOf ||
|
|
147
|
+
freshness.validUntil !== payload.validUntil ||
|
|
148
|
+
policy?.version !== payload.policyVersion ||
|
|
149
|
+
policy.validUntil !== payload.validUntil ||
|
|
150
|
+
(!live && !cached)) {
|
|
151
|
+
return { valid: false };
|
|
152
|
+
}
|
|
153
|
+
if (cached) {
|
|
154
|
+
const ageMs = Math.max(0, Date.now() - Date.parse(payload.asOf));
|
|
155
|
+
const state = Date.now() < Date.parse(payload.validUntil) ? "fresh" : "stale";
|
|
156
|
+
return {
|
|
157
|
+
valid: true,
|
|
158
|
+
notice: `Lore used verified ${state} cached context (${ageLabel(ageMs)} old) because fresh context was unavailable.`,
|
|
159
|
+
metricOutcome: "cached_fallback",
|
|
160
|
+
cacheAgeMs: ageMs,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (fallback === "live_lexical" &&
|
|
164
|
+
metadata?.status === "degraded" &&
|
|
165
|
+
reasons.includes("lexical_fallback")) {
|
|
166
|
+
return {
|
|
167
|
+
valid: true,
|
|
168
|
+
notice: "Lore used live lexical retrieval because semantic retrieval was unavailable.",
|
|
169
|
+
metricOutcome: "live_lexical_fallback",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const valid = fallback === "none" &&
|
|
173
|
+
metadata?.status === "ok" &&
|
|
174
|
+
reasons.length > 0;
|
|
175
|
+
return {
|
|
176
|
+
valid,
|
|
177
|
+
...(valid ? { metricOutcome: "live_primary" } : {}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function ageLabel(ageMs) {
|
|
181
|
+
if (ageMs < 60_000) {
|
|
182
|
+
return "less than a minute";
|
|
183
|
+
}
|
|
184
|
+
if (ageMs < 60 * 60_000) {
|
|
185
|
+
return `${Math.floor(ageMs / 60_000)}m`;
|
|
186
|
+
}
|
|
187
|
+
if (ageMs < 24 * 60 * 60_000) {
|
|
188
|
+
return `${Math.floor(ageMs / (60 * 60_000))}h`;
|
|
189
|
+
}
|
|
190
|
+
return `${Math.floor(ageMs / (24 * 60 * 60_000))}d`;
|
|
191
|
+
}
|
|
104
192
|
function scopeFromContext(context, repositoryScope) {
|
|
105
193
|
const repo = boundedScopeValue(repositoryScope, MAX_SCOPE_BYTES);
|
|
106
194
|
const relativePath = relative(context.worktree, context.directory);
|
|
@@ -179,7 +267,12 @@ export function formatGuardBlockMessage(verdict) {
|
|
|
179
267
|
const shown = required.length > 0 ? required : verdict.items;
|
|
180
268
|
const lines = [
|
|
181
269
|
"Lore Guard: this action conflicts with a required rule.",
|
|
182
|
-
...shown.
|
|
270
|
+
...shown.flatMap((item) => [
|
|
271
|
+
`- ${item.content}`,
|
|
272
|
+
` ${item.explanation === undefined
|
|
273
|
+
? `Source: ${guardItemSource(item)}.`
|
|
274
|
+
: formatExplanation(item.explanation)}`,
|
|
275
|
+
]),
|
|
183
276
|
verdict.confirm === undefined
|
|
184
277
|
? "Ask the user to confirm before retrying."
|
|
185
278
|
: `Ask the user to confirm; they can approve with: ${verdict.confirm.command}`,
|
|
@@ -193,10 +286,9 @@ export function formatGuardAssistContext(verdict) {
|
|
|
193
286
|
const lines = ["Relevant Lore context:"];
|
|
194
287
|
for (const item of verdict.items) {
|
|
195
288
|
lines.push(`- ${item.content}`);
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
lines.push(`- ${guardItemSource(item)}`);
|
|
289
|
+
lines.push(` ${item.explanation === undefined
|
|
290
|
+
? `Source: ${guardItemSource(item)}.`
|
|
291
|
+
: formatExplanation(item.explanation)}`);
|
|
200
292
|
}
|
|
201
293
|
if (verdict.conflicts.length > 0) {
|
|
202
294
|
lines.push("", "Conflicting Lore context (do not silently pick a winner):");
|
|
@@ -227,14 +319,18 @@ function pluginTimeout(options, env) {
|
|
|
227
319
|
class LoreOpenCodeRuntime {
|
|
228
320
|
#context;
|
|
229
321
|
#adapter;
|
|
322
|
+
#store;
|
|
323
|
+
#workspaceId;
|
|
230
324
|
#repositoryScope;
|
|
231
325
|
#timeoutMs;
|
|
232
326
|
#sessions = new Map();
|
|
233
|
-
constructor(context, adapter, timeoutMs, repositoryScope) {
|
|
327
|
+
constructor(context, adapter, timeoutMs, repositoryScope, store, workspaceId) {
|
|
234
328
|
this.#context = context;
|
|
235
329
|
this.#adapter = adapter;
|
|
236
330
|
this.#timeoutMs = timeoutMs;
|
|
237
331
|
this.#repositoryScope = repositoryScope;
|
|
332
|
+
this.#store = store;
|
|
333
|
+
this.#workspaceId = workspaceId;
|
|
238
334
|
}
|
|
239
335
|
hooks() {
|
|
240
336
|
return {
|
|
@@ -260,7 +356,11 @@ class LoreOpenCodeRuntime {
|
|
|
260
356
|
blockMessage = await this.#guardBeforeTool(input, output);
|
|
261
357
|
}
|
|
262
358
|
catch {
|
|
263
|
-
|
|
359
|
+
const state = this.#session(input.sessionID);
|
|
360
|
+
if (state !== undefined) {
|
|
361
|
+
state.guardContext =
|
|
362
|
+
"Lore Guard is unavailable; this action is proceeding without a live policy decision.";
|
|
363
|
+
}
|
|
264
364
|
return;
|
|
265
365
|
}
|
|
266
366
|
if (blockMessage !== undefined) {
|
|
@@ -276,6 +376,7 @@ class LoreOpenCodeRuntime {
|
|
|
276
376
|
}
|
|
277
377
|
else if (event.type === "session.deleted") {
|
|
278
378
|
this.#deleteSession(event.properties.info.id);
|
|
379
|
+
await this.#clearPendingAssistant(event.properties.info.id);
|
|
279
380
|
}
|
|
280
381
|
}
|
|
281
382
|
catch {
|
|
@@ -326,6 +427,447 @@ class LoreOpenCodeRuntime {
|
|
|
326
427
|
return undefined;
|
|
327
428
|
}
|
|
328
429
|
}
|
|
430
|
+
#contextNotice(state, notice) {
|
|
431
|
+
if (notice === undefined) {
|
|
432
|
+
return "";
|
|
433
|
+
}
|
|
434
|
+
if (notice.includes("cached context")) {
|
|
435
|
+
return notice;
|
|
436
|
+
}
|
|
437
|
+
const now = Date.now();
|
|
438
|
+
if (state.contextNotice?.text === notice &&
|
|
439
|
+
now - state.contextNotice.at < CONTEXT_NOTICE_COOLDOWN_MS) {
|
|
440
|
+
return "";
|
|
441
|
+
}
|
|
442
|
+
state.contextNotice = { text: notice, at: now };
|
|
443
|
+
return notice;
|
|
444
|
+
}
|
|
445
|
+
#pendingStateKey(sessionId) {
|
|
446
|
+
return `opencode:pending:${sessionId}`;
|
|
447
|
+
}
|
|
448
|
+
async #loadPendingAssistant(sessionId) {
|
|
449
|
+
const stored = await this.#store?.readState(this.#pendingStateKey(sessionId));
|
|
450
|
+
const value = objectValue(stored);
|
|
451
|
+
if (value === null ||
|
|
452
|
+
typeof value.id !== "string" ||
|
|
453
|
+
typeof value.content !== "string" ||
|
|
454
|
+
typeof value.timestamp !== "string" ||
|
|
455
|
+
typeof value.completedAt !== "number" ||
|
|
456
|
+
typeof value.fingerprint !== "string") {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
id: value.id,
|
|
461
|
+
content: value.content,
|
|
462
|
+
timestamp: value.timestamp,
|
|
463
|
+
completedAt: value.completedAt,
|
|
464
|
+
fingerprint: value.fingerprint,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async #savePendingAssistant(sessionId, pending) {
|
|
468
|
+
await this.#store?.writeState(this.#pendingStateKey(sessionId), pending);
|
|
469
|
+
}
|
|
470
|
+
async #clearPendingAssistant(sessionId) {
|
|
471
|
+
await this.#store?.deleteState(this.#pendingStateKey(sessionId));
|
|
472
|
+
}
|
|
473
|
+
async #snapshotKeys(refresh = false) {
|
|
474
|
+
const store = this.#store;
|
|
475
|
+
const workspaceId = this.#workspaceId;
|
|
476
|
+
const adapter = this.#adapter;
|
|
477
|
+
if (store === undefined || workspaceId === undefined) {
|
|
478
|
+
throw new Error("Lore connector has no workspace identity; reconnect Lore");
|
|
479
|
+
}
|
|
480
|
+
if (!refresh) {
|
|
481
|
+
const cached = await store.readPublicTrustKeys();
|
|
482
|
+
const refreshState = objectValue(await store.readState(TRUST_KEY_REFRESH_STATE_KEY));
|
|
483
|
+
const refreshedAt = typeof refreshState?.refreshedAt === "string"
|
|
484
|
+
? Date.parse(refreshState.refreshedAt)
|
|
485
|
+
: Number.NaN;
|
|
486
|
+
if (cached.length > 0 &&
|
|
487
|
+
Number.isFinite(refreshedAt) &&
|
|
488
|
+
Date.now() - refreshedAt < TRUST_KEY_REFRESH_TTL_MS &&
|
|
489
|
+
refreshedAt <= Date.now() + 5 * 60_000) {
|
|
490
|
+
return { workspaceId, keys: cached };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
if (adapter?.getSnapshotPublicKeys === undefined) {
|
|
494
|
+
throw new Error("Lore adapter does not support snapshot keys");
|
|
495
|
+
}
|
|
496
|
+
const keySet = await adapter.getSnapshotPublicKeys();
|
|
497
|
+
if (keySet.workspaceId !== workspaceId) {
|
|
498
|
+
throw new Error("Lore snapshot keys belong to a different workspace");
|
|
499
|
+
}
|
|
500
|
+
const keys = keySet.keys.map((key) => ({
|
|
501
|
+
...key,
|
|
502
|
+
workspaceId,
|
|
503
|
+
}));
|
|
504
|
+
await store.writePublicTrustKeys(keys);
|
|
505
|
+
await store.writeState(TRUST_KEY_REFRESH_STATE_KEY, {
|
|
506
|
+
refreshedAt: new Date().toISOString(),
|
|
507
|
+
});
|
|
508
|
+
return { workspaceId, keys };
|
|
509
|
+
}
|
|
510
|
+
async #cachedSnapshotKeys() {
|
|
511
|
+
const store = this.#store;
|
|
512
|
+
const workspaceId = this.#workspaceId;
|
|
513
|
+
if (store === undefined || workspaceId === undefined) {
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const [keys, refreshState] = await Promise.all([
|
|
517
|
+
store.readPublicTrustKeys(),
|
|
518
|
+
store.readState(TRUST_KEY_REFRESH_STATE_KEY),
|
|
519
|
+
]);
|
|
520
|
+
const refreshedAt = typeof objectValue(refreshState)?.refreshedAt === "string"
|
|
521
|
+
? Date.parse(String(objectValue(refreshState)?.refreshedAt))
|
|
522
|
+
: Number.NaN;
|
|
523
|
+
if (keys.length === 0 ||
|
|
524
|
+
!Number.isFinite(refreshedAt) ||
|
|
525
|
+
Date.now() - refreshedAt >= TRUST_KEY_REFRESH_TTL_MS ||
|
|
526
|
+
refreshedAt > Date.now() + 5 * 60_000) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
return { workspaceId, keys };
|
|
530
|
+
}
|
|
531
|
+
async #cachedContext(delivery) {
|
|
532
|
+
const store = this.#store;
|
|
533
|
+
const workspaceId = this.#workspaceId;
|
|
534
|
+
const keys = await this.#cachedSnapshotKeys();
|
|
535
|
+
if (store === undefined || workspaceId === undefined || keys === null) {
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
const candidates = [];
|
|
539
|
+
for (const record of (await store.listContextSnapshots()).records) {
|
|
540
|
+
const value = objectValue(record.value);
|
|
541
|
+
if (value === null || typeof value.compactJws !== "string") {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
try {
|
|
545
|
+
candidates.push({
|
|
546
|
+
cacheKey: record.cacheKey,
|
|
547
|
+
compactJws: value.compactJws,
|
|
548
|
+
payload: verifySignedSnapshot(value.compactJws, {
|
|
549
|
+
workspaceId,
|
|
550
|
+
kind: "context",
|
|
551
|
+
keys,
|
|
552
|
+
allowExpired: true,
|
|
553
|
+
}).payload,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
// One invalid cache entry must not hide another verified candidate.
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const task = redactUnknown(toHostTask(delivery, "opencode")).value;
|
|
561
|
+
const taskValue = objectValue(task);
|
|
562
|
+
if (taskValue === null ||
|
|
563
|
+
typeof taskValue.task !== "string" ||
|
|
564
|
+
!objectValue(taskValue.scope)) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
const fallbackTask = {
|
|
568
|
+
workspaceId,
|
|
569
|
+
task: taskValue.task,
|
|
570
|
+
scope: taskValue.scope,
|
|
571
|
+
...(Array.isArray(taskValue.files)
|
|
572
|
+
? { files: taskValue.files.filter((item) => typeof item === "string") }
|
|
573
|
+
: {}),
|
|
574
|
+
...(Array.isArray(taskValue.components)
|
|
575
|
+
? {
|
|
576
|
+
components: taskValue.components.filter((item) => typeof item === "string"),
|
|
577
|
+
}
|
|
578
|
+
: {}),
|
|
579
|
+
...(Array.isArray(taskValue.symbols)
|
|
580
|
+
? {
|
|
581
|
+
symbols: taskValue.symbols.filter((item) => typeof item === "string"),
|
|
582
|
+
}
|
|
583
|
+
: {}),
|
|
584
|
+
};
|
|
585
|
+
const selected = selectCachedContext(candidates, fallbackTask);
|
|
586
|
+
if (selected === null) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
context: contextText(selected.context),
|
|
591
|
+
notice: `Lore used verified ${selected.freshness.state} cached context (${ageLabel(selected.freshness.ageMs)} old) because live retrieval was unavailable.`,
|
|
592
|
+
metricOutcome: "cached_fallback",
|
|
593
|
+
cacheAgeMs: selected.freshness.ageMs,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async #verifySnapshot(compactJws, kind, allowExpired = false) {
|
|
597
|
+
const workspaceId = this.#workspaceId;
|
|
598
|
+
if (workspaceId === undefined) {
|
|
599
|
+
throw new Error("Lore connector has no workspace identity; reconnect Lore");
|
|
600
|
+
}
|
|
601
|
+
const verifyWith = (keys) => verifySignedSnapshot(compactJws, {
|
|
602
|
+
workspaceId,
|
|
603
|
+
kind,
|
|
604
|
+
keys,
|
|
605
|
+
allowExpired,
|
|
606
|
+
}).payload;
|
|
607
|
+
try {
|
|
608
|
+
return verifyWith(await this.#snapshotKeys());
|
|
609
|
+
}
|
|
610
|
+
catch (error) {
|
|
611
|
+
if (!(error instanceof SignedSnapshotError) ||
|
|
612
|
+
error.code !== "UNTRUSTED_KEY") {
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
return verifyWith(await this.#snapshotKeys(true));
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
#captureCommitted(response, externalEventId, serverEventId) {
|
|
619
|
+
const acknowledgement = response.capture.acknowledgement;
|
|
620
|
+
return (acknowledgement?.state === "committed_server" &&
|
|
621
|
+
acknowledgement.durability === "workspace_database" &&
|
|
622
|
+
acknowledgement.replayPending === false &&
|
|
623
|
+
acknowledgement.idempotencyKey === externalEventId &&
|
|
624
|
+
acknowledgement.eventId === serverEventId);
|
|
625
|
+
}
|
|
626
|
+
async #persistContextSnapshot(compactJws, cacheKey, expectedRequest, eventId, receiptId) {
|
|
627
|
+
const payload = await this.#verifySnapshot(compactJws, "context", true);
|
|
628
|
+
const request = objectValue(payload.request);
|
|
629
|
+
if (request === null ||
|
|
630
|
+
canonicalJson(request) !== canonicalJson(expectedRequest) ||
|
|
631
|
+
payload.eventId !== eventId ||
|
|
632
|
+
payload.receiptId !== receiptId ||
|
|
633
|
+
typeof payload.context !== "string" ||
|
|
634
|
+
!Array.isArray(payload.memories) ||
|
|
635
|
+
!Array.isArray(payload.hits) ||
|
|
636
|
+
objectValue(payload.packing) === null) {
|
|
637
|
+
throw new Error("Lore context snapshot does not bind its request and delivery");
|
|
638
|
+
}
|
|
639
|
+
if (Date.now() - Date.parse(payload.validUntil) >
|
|
640
|
+
CONTEXT_CACHE_MAX_STALE_MS) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
await this.#store
|
|
644
|
+
?.writeContextSnapshot(cacheKey, { compactJws })
|
|
645
|
+
.catch(() => undefined);
|
|
646
|
+
return payload;
|
|
647
|
+
}
|
|
648
|
+
async #enqueueCapture(kind, eventId, payload) {
|
|
649
|
+
if (this.#store === undefined) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
return (await this.#store.enqueue({
|
|
653
|
+
kind,
|
|
654
|
+
idempotencyKey: eventId,
|
|
655
|
+
payload,
|
|
656
|
+
})).entry;
|
|
657
|
+
}
|
|
658
|
+
async #flushCapture() {
|
|
659
|
+
const store = this.#store;
|
|
660
|
+
const adapter = this.#adapter;
|
|
661
|
+
if (store === undefined || adapter === undefined) {
|
|
662
|
+
return null;
|
|
663
|
+
}
|
|
664
|
+
const claimed = await store.claimNext({
|
|
665
|
+
workerId: `opencode:${process.pid}`,
|
|
666
|
+
kinds: ["opencode-turn", "opencode-observation"],
|
|
667
|
+
});
|
|
668
|
+
if (claimed === null || claimed.claim === undefined) {
|
|
669
|
+
return null;
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const payload = objectValue(claimed.payload);
|
|
673
|
+
if (payload === null) {
|
|
674
|
+
throw Object.assign(new Error("Invalid OpenCode outbox payload"), {
|
|
675
|
+
incompatible: true,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
let turn;
|
|
679
|
+
let signedContext;
|
|
680
|
+
let retrievalNotice;
|
|
681
|
+
if (claimed.kind === "opencode-turn") {
|
|
682
|
+
if (adapter.processTurnReliable === undefined) {
|
|
683
|
+
throw Object.assign(new Error("Lore adapter has no reliable turn operation"), { incompatible: true });
|
|
684
|
+
}
|
|
685
|
+
const turnInput = payload;
|
|
686
|
+
turn = await adapter.processTurnReliable(turnInput, claimed.idempotencyKey);
|
|
687
|
+
if (!this.#captureCommitted(turn, claimed.idempotencyKey, turn.turn.event.id)) {
|
|
688
|
+
throw Object.assign(new Error("Lore turn has no matching durable acknowledgement"), { incompatible: true });
|
|
689
|
+
}
|
|
690
|
+
const contextSnapshot = await this.#persistContextSnapshot(turn.contextSnapshot, claimed.idempotencyKey, objectValue(redactUnknown({
|
|
691
|
+
connector: turnInput.connector,
|
|
692
|
+
eventId: turnInput.eventId,
|
|
693
|
+
sessionId: turnInput.sessionId,
|
|
694
|
+
task: {
|
|
695
|
+
agent: "opencode",
|
|
696
|
+
scope: normalizeHostScope(turnInput),
|
|
697
|
+
task: typeof turnInput.currentUser === "string"
|
|
698
|
+
? turnInput.currentUser
|
|
699
|
+
: (turnInput.currentUser?.content ??
|
|
700
|
+
turnInput.currentUserPrompt ??
|
|
701
|
+
""),
|
|
702
|
+
},
|
|
703
|
+
}).value) ?? {}, turn.turn.event.id, turn.turn.receipt.id);
|
|
704
|
+
if (contextSnapshot === null) {
|
|
705
|
+
retrievalNotice =
|
|
706
|
+
"Lore context cache is too old to use; continuing without injected context.";
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
const metadata = contextRetrievalMetadata(turn.retrieval, contextSnapshot);
|
|
710
|
+
if (metadata.valid) {
|
|
711
|
+
signedContext = contextText(contextSnapshot.context);
|
|
712
|
+
retrievalNotice = metadata.notice;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
retrievalNotice =
|
|
716
|
+
"Lore context could not be trusted; continuing without injected context.";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
if (adapter.observeEventReliable === undefined) {
|
|
722
|
+
throw Object.assign(new Error("Lore adapter has no reliable observation operation"), { incompatible: true });
|
|
723
|
+
}
|
|
724
|
+
const observed = await adapter.observeEventReliable(payload);
|
|
725
|
+
if (!this.#captureCommitted(observed, claimed.idempotencyKey, observed.observation.event.id)) {
|
|
726
|
+
throw Object.assign(new Error("Lore observation has no matching durable acknowledgement"), { incompatible: true });
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
await store.acknowledgeClaim(claimed.claim.id);
|
|
730
|
+
return {
|
|
731
|
+
idempotencyKey: claimed.idempotencyKey,
|
|
732
|
+
...(turn === undefined ? {} : { turn }),
|
|
733
|
+
...(signedContext === undefined ? {} : { signedContext }),
|
|
734
|
+
...(retrievalNotice === undefined ? {} : { retrievalNotice }),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
const classified = classifyRetryFailure(error);
|
|
739
|
+
const failure = objectValue(error);
|
|
740
|
+
const httpStatus = typeof failure?.httpStatus === "number"
|
|
741
|
+
? failure.httpStatus
|
|
742
|
+
: typeof failure?.status === "number"
|
|
743
|
+
? failure.status
|
|
744
|
+
: undefined;
|
|
745
|
+
await store.failClaim({
|
|
746
|
+
claimId: claimed.claim.id,
|
|
747
|
+
classification: error instanceof SignedSnapshotError
|
|
748
|
+
? "incompatible"
|
|
749
|
+
: classified === "retryable" &&
|
|
750
|
+
httpStatus !== 409 &&
|
|
751
|
+
!isContextTransportFailure(error)
|
|
752
|
+
? "incompatible"
|
|
753
|
+
: classified,
|
|
754
|
+
message: error instanceof Error ? error.message : "Lore upload failed",
|
|
755
|
+
});
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async #prepareContextDelivery(delivery) {
|
|
760
|
+
const adapter = this.#adapter;
|
|
761
|
+
if (adapter === undefined) {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
if (this.#store !== undefined && this.#workspaceId === undefined) {
|
|
765
|
+
return {
|
|
766
|
+
context: "",
|
|
767
|
+
notice: "Lore context is temporarily unavailable; continuing without injected context.",
|
|
768
|
+
metricOutcome: "failed",
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
if (this.#store !== undefined &&
|
|
772
|
+
this.#workspaceId !== undefined &&
|
|
773
|
+
adapter.prepareDeliveryReliable !== undefined) {
|
|
774
|
+
try {
|
|
775
|
+
const response = await adapter.prepareDeliveryReliable(delivery);
|
|
776
|
+
const expectedRequest = redactUnknown({
|
|
777
|
+
connector: delivery.connector,
|
|
778
|
+
eventId: delivery.eventId,
|
|
779
|
+
sessionId: delivery.sessionId,
|
|
780
|
+
task: toHostTask(delivery, "opencode"),
|
|
781
|
+
}).value;
|
|
782
|
+
const contextSnapshot = await this.#persistContextSnapshot(response.snapshot, delivery.eventId, objectValue(expectedRequest) ?? {}, response.delivery.event.id, response.delivery.receipt.id);
|
|
783
|
+
if (contextSnapshot === null) {
|
|
784
|
+
return {
|
|
785
|
+
context: "",
|
|
786
|
+
notice: "Lore context cache is too old to use; continuing without injected context.",
|
|
787
|
+
metricOutcome: "failed",
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const metadata = contextRetrievalMetadata(response.reliability, contextSnapshot);
|
|
791
|
+
if (!metadata.valid) {
|
|
792
|
+
return {
|
|
793
|
+
context: "",
|
|
794
|
+
notice: "Lore context could not be trusted; continuing without injected context.",
|
|
795
|
+
metricOutcome: "failed",
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
return {
|
|
799
|
+
context: contextText(contextSnapshot.context),
|
|
800
|
+
...(metadata.notice === undefined
|
|
801
|
+
? {}
|
|
802
|
+
: { notice: metadata.notice }),
|
|
803
|
+
...(metadata.metricOutcome === undefined
|
|
804
|
+
? {}
|
|
805
|
+
: { metricOutcome: metadata.metricOutcome }),
|
|
806
|
+
...(metadata.cacheAgeMs === undefined
|
|
807
|
+
? {}
|
|
808
|
+
: { cacheAgeMs: metadata.cacheAgeMs }),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
if (isContextTransportFailure(error)) {
|
|
813
|
+
const cached = await this.#cachedContext(delivery).catch(() => null);
|
|
814
|
+
if (cached !== null) {
|
|
815
|
+
return cached;
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
context: "",
|
|
819
|
+
notice: "Lore context is temporarily unavailable; continuing without injected context.",
|
|
820
|
+
metricOutcome: "failed",
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
context: "",
|
|
825
|
+
notice: "Lore context could not be trusted; continuing without injected context.",
|
|
826
|
+
metricOutcome: "failed",
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
const response = await adapter.prepareDelivery(delivery);
|
|
832
|
+
return { context: contextText(response.context) };
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
return {
|
|
836
|
+
context: "",
|
|
837
|
+
notice: "Lore context is temporarily unavailable; continuing without injected context.",
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
async #recordPreparedContextMetric(prepared) {
|
|
842
|
+
if (this.#store === undefined ||
|
|
843
|
+
prepared?.metricOutcome === undefined) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
await recordLocalRetrievalMetric(this.#store, {
|
|
847
|
+
outcome: prepared.metricOutcome,
|
|
848
|
+
...(prepared.cacheAgeMs === undefined
|
|
849
|
+
? {}
|
|
850
|
+
: { cacheAgeMs: prepared.cacheAgeMs }),
|
|
851
|
+
}).catch(() => undefined);
|
|
852
|
+
}
|
|
853
|
+
async #checkGuard(input) {
|
|
854
|
+
const adapter = this.#adapter;
|
|
855
|
+
if (adapter === undefined) {
|
|
856
|
+
return undefined;
|
|
857
|
+
}
|
|
858
|
+
if (this.#store !== undefined &&
|
|
859
|
+
this.#workspaceId !== undefined &&
|
|
860
|
+
adapter.guardCheckReliable !== undefined) {
|
|
861
|
+
const response = await adapter.guardCheckReliable(input);
|
|
862
|
+
const payload = await this.#verifySnapshot(response.policySnapshot, "guard_policy");
|
|
863
|
+
if (payload.policyVersion !== response.policyVersion) {
|
|
864
|
+
throw new Error("Lore Guard snapshot does not bind the returned policy");
|
|
865
|
+
}
|
|
866
|
+
await this.#store.writePolicySnapshot("current", response.policySnapshot);
|
|
867
|
+
return response.check;
|
|
868
|
+
}
|
|
869
|
+
return adapter.guardCheck?.(input);
|
|
870
|
+
}
|
|
329
871
|
async #captureAndDeliver(input, output) {
|
|
330
872
|
if (this.#adapter === undefined) {
|
|
331
873
|
return;
|
|
@@ -359,7 +901,8 @@ class LoreOpenCodeRuntime {
|
|
|
359
901
|
}
|
|
360
902
|
}
|
|
361
903
|
async #processChatMessage(state, sessionId, messageId, content, timestamp, fingerprint, input) {
|
|
362
|
-
|
|
904
|
+
const adapter = this.#adapter;
|
|
905
|
+
if (!state.active || adapter === undefined) {
|
|
363
906
|
return;
|
|
364
907
|
}
|
|
365
908
|
if (state.lastChat?.fingerprint === fingerprint) {
|
|
@@ -375,10 +918,16 @@ class LoreOpenCodeRuntime {
|
|
|
375
918
|
state.context = "";
|
|
376
919
|
const scope = scopeFromContext(this.#context, this.#repositoryScope);
|
|
377
920
|
const metadata = metadataFromChat(input, this.#context);
|
|
378
|
-
|
|
921
|
+
await this.#flushCapture();
|
|
922
|
+
const pendingAssistant = state.pendingAssistant ??
|
|
923
|
+
(await this.#loadPendingAssistant(sessionId));
|
|
924
|
+
if (state.pendingAssistant === undefined &&
|
|
925
|
+
pendingAssistant !== undefined) {
|
|
926
|
+
state.pendingAssistant = pendingAssistant;
|
|
927
|
+
}
|
|
379
928
|
if (pendingAssistant !== undefined) {
|
|
380
929
|
const eventId = stableEventId("turn", sessionId, pendingAssistant.fingerprint, fingerprint);
|
|
381
|
-
const
|
|
930
|
+
const turnInput = {
|
|
382
931
|
connector: CONNECTOR,
|
|
383
932
|
eventId,
|
|
384
933
|
sessionId,
|
|
@@ -402,12 +951,69 @@ class LoreOpenCodeRuntime {
|
|
|
402
951
|
previousAssistantMessageId: pendingAssistant.id,
|
|
403
952
|
currentUserMessageId: messageId,
|
|
404
953
|
},
|
|
405
|
-
}
|
|
406
|
-
|
|
954
|
+
};
|
|
955
|
+
let context = "";
|
|
956
|
+
let pendingDurable = false;
|
|
957
|
+
if (this.#store !== undefined &&
|
|
958
|
+
adapter.processTurnReliable !== undefined) {
|
|
959
|
+
try {
|
|
960
|
+
await this.#enqueueCapture("opencode-turn", eventId, turnInput);
|
|
961
|
+
pendingDurable = true;
|
|
962
|
+
await this.#clearPendingAssistant(sessionId);
|
|
963
|
+
const flushed = await this.#flushCapture();
|
|
964
|
+
if (flushed?.idempotencyKey === eventId &&
|
|
965
|
+
flushed.turn !== undefined) {
|
|
966
|
+
await recordLocalRetrievalMetric(this.#store, {
|
|
967
|
+
outcome: flushed.retrievalNotice?.includes("lexical")
|
|
968
|
+
? "live_lexical_fallback"
|
|
969
|
+
: "live_primary",
|
|
970
|
+
}).catch(() => undefined);
|
|
971
|
+
context = [
|
|
972
|
+
this.#contextNotice(state, flushed.retrievalNotice),
|
|
973
|
+
flushed.signedContext,
|
|
974
|
+
]
|
|
975
|
+
.filter((value) => value !== undefined && value !== "")
|
|
976
|
+
.join("\n\n");
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
const prepared = await this.#prepareContextDelivery({
|
|
980
|
+
connector: CONNECTOR,
|
|
981
|
+
eventId: stableEventId("delivery", sessionId, eventId, content),
|
|
982
|
+
sessionId,
|
|
983
|
+
scope,
|
|
984
|
+
task: content,
|
|
985
|
+
limit: 10,
|
|
986
|
+
});
|
|
987
|
+
await this.#recordPreparedContextMetric(prepared);
|
|
988
|
+
context = [
|
|
989
|
+
"Lore saved this capture locally; server upload is pending.",
|
|
990
|
+
this.#contextNotice(state, prepared?.notice),
|
|
991
|
+
prepared?.context,
|
|
992
|
+
]
|
|
993
|
+
.filter((value) => value !== undefined && value !== "")
|
|
994
|
+
.join("\n\n");
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch {
|
|
998
|
+
context =
|
|
999
|
+
"Lore could not acknowledge this capture locally. Run lore doctor.";
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
const processed = await this.#attempt(() => adapter.processTurn(turnInput, eventId));
|
|
1004
|
+
if (processed === undefined || !state.active) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
pendingDurable = true;
|
|
1008
|
+
context = contextText(processed.context.text);
|
|
1009
|
+
}
|
|
1010
|
+
if (!state.active) {
|
|
407
1011
|
return;
|
|
408
1012
|
}
|
|
409
|
-
const context = contextText(processed.context.text);
|
|
410
1013
|
state.context = context;
|
|
1014
|
+
if (!pendingDurable) {
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
411
1017
|
state.lastChat = { fingerprint, context };
|
|
412
1018
|
state.lastConsumedAssistant = {
|
|
413
1019
|
completedAt: pendingAssistant.completedAt,
|
|
@@ -438,16 +1044,52 @@ class LoreOpenCodeRuntime {
|
|
|
438
1044
|
task: content,
|
|
439
1045
|
limit: 10,
|
|
440
1046
|
};
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
1047
|
+
let observed;
|
|
1048
|
+
let preparedContext;
|
|
1049
|
+
let captureNotice = "";
|
|
1050
|
+
if (this.#store !== undefined &&
|
|
1051
|
+
adapter.observeEventReliable !== undefined) {
|
|
1052
|
+
try {
|
|
1053
|
+
await this.#enqueueCapture("opencode-observation", observationId, observation);
|
|
1054
|
+
}
|
|
1055
|
+
catch {
|
|
1056
|
+
captureNotice =
|
|
1057
|
+
"Lore could not acknowledge this capture locally. Run lore doctor.";
|
|
1058
|
+
}
|
|
1059
|
+
const [flushed, prepared] = await Promise.all([
|
|
1060
|
+
this.#flushCapture(),
|
|
1061
|
+
this.#prepareContextDelivery(delivery),
|
|
1062
|
+
]);
|
|
1063
|
+
await this.#recordPreparedContextMetric(prepared);
|
|
1064
|
+
observed =
|
|
1065
|
+
flushed?.idempotencyKey === observationId ? flushed : undefined;
|
|
1066
|
+
preparedContext = [
|
|
1067
|
+
this.#contextNotice(state, prepared?.notice),
|
|
1068
|
+
prepared?.context,
|
|
1069
|
+
]
|
|
1070
|
+
.filter((value) => value !== undefined && value !== "")
|
|
1071
|
+
.join("\n\n");
|
|
1072
|
+
if (observed === undefined && captureNotice === "") {
|
|
1073
|
+
captureNotice =
|
|
1074
|
+
"Lore saved this capture locally; server upload is pending.";
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
else {
|
|
1078
|
+
const [legacyObserved, prepared] = await Promise.all([
|
|
1079
|
+
this.#attempt(() => adapter.observeEvent(observation)),
|
|
1080
|
+
this.#attempt(() => adapter.prepareDelivery(delivery)),
|
|
1081
|
+
]);
|
|
1082
|
+
observed = legacyObserved;
|
|
1083
|
+
preparedContext = contextText(prepared?.context);
|
|
1084
|
+
}
|
|
445
1085
|
if (!state.active) {
|
|
446
1086
|
return;
|
|
447
1087
|
}
|
|
448
|
-
const context =
|
|
1088
|
+
const context = [captureNotice, preparedContext]
|
|
1089
|
+
.filter((value) => value !== undefined && value !== "")
|
|
1090
|
+
.join("\n\n");
|
|
449
1091
|
state.context = context;
|
|
450
|
-
if (observed !== undefined &&
|
|
1092
|
+
if (observed !== undefined && preparedContext !== undefined) {
|
|
451
1093
|
state.lastChat = { fingerprint, context };
|
|
452
1094
|
}
|
|
453
1095
|
}
|
|
@@ -479,7 +1121,8 @@ class LoreOpenCodeRuntime {
|
|
|
479
1121
|
}
|
|
480
1122
|
async #guardBeforeTool(input, output) {
|
|
481
1123
|
const adapter = this.#adapter;
|
|
482
|
-
if (adapter?.guardCheck === undefined
|
|
1124
|
+
if (adapter?.guardCheck === undefined &&
|
|
1125
|
+
adapter?.guardCheckReliable === undefined) {
|
|
483
1126
|
return undefined;
|
|
484
1127
|
}
|
|
485
1128
|
const sessionId = boundedIdentifier(input.sessionID);
|
|
@@ -500,11 +1143,21 @@ class LoreOpenCodeRuntime {
|
|
|
500
1143
|
const now = Date.now();
|
|
501
1144
|
if (state.guardMode?.mode === "off" &&
|
|
502
1145
|
now - state.guardMode.checkedAt < GUARD_MODE_TTL_MS) {
|
|
1146
|
+
if (this.#store !== undefined) {
|
|
1147
|
+
await recordLocalGuardMetric(this.#store, {
|
|
1148
|
+
outcome: "reused_decision",
|
|
1149
|
+
}).catch(() => undefined);
|
|
1150
|
+
}
|
|
503
1151
|
return undefined;
|
|
504
1152
|
}
|
|
505
1153
|
const key = guardKey(trigger);
|
|
506
1154
|
const lastSeen = state.guardKeys?.get(key);
|
|
507
1155
|
if (lastSeen !== undefined && now - lastSeen < GUARD_KEY_COOLDOWN_MS) {
|
|
1156
|
+
if (this.#store !== undefined) {
|
|
1157
|
+
await recordLocalGuardMetric(this.#store, {
|
|
1158
|
+
outcome: "reused_decision",
|
|
1159
|
+
}).catch(() => undefined);
|
|
1160
|
+
}
|
|
508
1161
|
return undefined;
|
|
509
1162
|
}
|
|
510
1163
|
const scope = scopeFromContext(this.#context, this.#repositoryScope);
|
|
@@ -517,18 +1170,36 @@ class LoreOpenCodeRuntime {
|
|
|
517
1170
|
return relativePath.startsWith("..") ? file : relativePath;
|
|
518
1171
|
})
|
|
519
1172
|
.map((file) => truncateUtf8(file, MAX_PATH_BYTES));
|
|
520
|
-
const
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
? {}
|
|
530
|
-
|
|
531
|
-
|
|
1173
|
+
const startedAt = Date.now();
|
|
1174
|
+
let verdict;
|
|
1175
|
+
try {
|
|
1176
|
+
verdict = await withTimeout(this.#timeoutMs, () => this.#checkGuard({
|
|
1177
|
+
connector: CONNECTOR,
|
|
1178
|
+
sessionId,
|
|
1179
|
+
action: trigger.action,
|
|
1180
|
+
tool: input.tool,
|
|
1181
|
+
...(scope.repo === undefined ? {} : { repo: scope.repo }),
|
|
1182
|
+
...(scope.path === undefined ? {} : { path: scope.path }),
|
|
1183
|
+
...(files === undefined ? {} : { files }),
|
|
1184
|
+
...(trigger.command === undefined
|
|
1185
|
+
? {}
|
|
1186
|
+
: { command: truncateUtf8(trigger.command, MAX_PATH_BYTES) }),
|
|
1187
|
+
}));
|
|
1188
|
+
}
|
|
1189
|
+
catch (error) {
|
|
1190
|
+
if (this.#store !== undefined) {
|
|
1191
|
+
await recordLocalGuardMetric(this.#store, {
|
|
1192
|
+
outcome: "failed",
|
|
1193
|
+
}).catch(() => undefined);
|
|
1194
|
+
}
|
|
1195
|
+
throw error;
|
|
1196
|
+
}
|
|
1197
|
+
if (this.#store !== undefined) {
|
|
1198
|
+
await recordLocalGuardMetric(this.#store, {
|
|
1199
|
+
outcome: verdict === undefined ? "failed" : "live_check",
|
|
1200
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
1201
|
+
}).catch(() => undefined);
|
|
1202
|
+
}
|
|
532
1203
|
if (verdict === undefined || !state.active) {
|
|
533
1204
|
return undefined;
|
|
534
1205
|
}
|
|
@@ -644,13 +1315,15 @@ class LoreOpenCodeRuntime {
|
|
|
644
1315
|
fingerprint === state.pendingAssistant.fingerprint))) {
|
|
645
1316
|
return;
|
|
646
1317
|
}
|
|
647
|
-
|
|
1318
|
+
const pendingAssistant = {
|
|
648
1319
|
id: assistantId,
|
|
649
1320
|
content: assistantContent,
|
|
650
1321
|
timestamp: isoTimestamp(completedAt),
|
|
651
1322
|
completedAt,
|
|
652
1323
|
fingerprint,
|
|
653
1324
|
};
|
|
1325
|
+
await this.#savePendingAssistant(sessionId, pendingAssistant);
|
|
1326
|
+
state.pendingAssistant = pendingAssistant;
|
|
654
1327
|
}
|
|
655
1328
|
#deleteSession(sessionId) {
|
|
656
1329
|
const state = this.#sessions.get(sessionId);
|
|
@@ -666,18 +1339,40 @@ class LoreOpenCodeRuntime {
|
|
|
666
1339
|
this.#sessions.delete(sessionId);
|
|
667
1340
|
}
|
|
668
1341
|
}
|
|
1342
|
+
function effectiveHome(dependencies) {
|
|
1343
|
+
return (dependencies.home ??
|
|
1344
|
+
(dependencies.env ?? process.env).HOME ??
|
|
1345
|
+
homedir());
|
|
1346
|
+
}
|
|
669
1347
|
async function createAdapter(dependencies, timeoutMs) {
|
|
670
1348
|
if (dependencies.adapter !== undefined) {
|
|
671
|
-
|
|
1349
|
+
if (dependencies.workspaceId === undefined) {
|
|
1350
|
+
return {
|
|
1351
|
+
adapter: dependencies.adapter,
|
|
1352
|
+
store: undefined,
|
|
1353
|
+
invocationWorkspaceKey: undefined,
|
|
1354
|
+
workspaceId: undefined,
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
const store = new ReliabilityStore(dependencies.workspaceId, {
|
|
1358
|
+
home: effectiveHome(dependencies),
|
|
1359
|
+
});
|
|
1360
|
+
await store.initialize();
|
|
1361
|
+
return {
|
|
1362
|
+
adapter: dependencies.adapter,
|
|
1363
|
+
store,
|
|
1364
|
+
invocationWorkspaceKey: dependencies.workspaceId,
|
|
1365
|
+
workspaceId: dependencies.workspaceId,
|
|
1366
|
+
};
|
|
672
1367
|
}
|
|
673
1368
|
try {
|
|
674
1369
|
const env = dependencies.env ?? process.env;
|
|
675
1370
|
const credentials = await resolveLoreCredentials({
|
|
676
1371
|
env,
|
|
677
|
-
|
|
1372
|
+
home: effectiveHome(dependencies),
|
|
678
1373
|
});
|
|
679
1374
|
const fetchImplementation = dependencies.fetch ?? globalThis.fetch;
|
|
680
|
-
|
|
1375
|
+
const adapter = new GenericAgentAdapter({
|
|
681
1376
|
id: "opencode",
|
|
682
1377
|
baseUrl: credentials.apiUrl,
|
|
683
1378
|
headers: {
|
|
@@ -685,20 +1380,99 @@ async function createAdapter(dependencies, timeoutMs) {
|
|
|
685
1380
|
},
|
|
686
1381
|
fetch: createBoundedFetch(fetchImplementation, timeoutMs),
|
|
687
1382
|
});
|
|
1383
|
+
let workspaceId = credentials.workspaceId;
|
|
1384
|
+
let keySet;
|
|
1385
|
+
let legacyServer = false;
|
|
1386
|
+
if (workspaceId === undefined) {
|
|
1387
|
+
try {
|
|
1388
|
+
keySet = await adapter.getSnapshotPublicKeys();
|
|
1389
|
+
workspaceId = keySet.workspaceId;
|
|
1390
|
+
}
|
|
1391
|
+
catch (error) {
|
|
1392
|
+
legacyServer =
|
|
1393
|
+
objectValue(error)?.status === 404;
|
|
1394
|
+
// Captures can still queue under a credential-scoped fallback.
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
if (legacyServer) {
|
|
1398
|
+
return {
|
|
1399
|
+
adapter,
|
|
1400
|
+
store: undefined,
|
|
1401
|
+
invocationWorkspaceKey: undefined,
|
|
1402
|
+
workspaceId: undefined,
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
const credentialStoreKey = `credential-${createHash("sha256")
|
|
1406
|
+
.update(`${credentials.apiUrl}\0${credentials.token}`)
|
|
1407
|
+
.digest("hex")
|
|
1408
|
+
.slice(0, 32)}`;
|
|
1409
|
+
const storeKey = workspaceId ?? credentialStoreKey;
|
|
1410
|
+
const store = new ReliabilityStore(storeKey, {
|
|
1411
|
+
home: effectiveHome(dependencies),
|
|
1412
|
+
});
|
|
1413
|
+
await store.initialize();
|
|
1414
|
+
if (workspaceId !== undefined) {
|
|
1415
|
+
const credentialStore = new ReliabilityStore(credentialStoreKey, {
|
|
1416
|
+
home: effectiveHome(dependencies),
|
|
1417
|
+
});
|
|
1418
|
+
await credentialStore.transferPendingTo(store);
|
|
1419
|
+
}
|
|
1420
|
+
if (keySet !== undefined && workspaceId !== undefined) {
|
|
1421
|
+
await store.writePublicTrustKeys(keySet.keys.map((key) => ({
|
|
1422
|
+
...key,
|
|
1423
|
+
workspaceId,
|
|
1424
|
+
})));
|
|
1425
|
+
await store.writeState(TRUST_KEY_REFRESH_STATE_KEY, {
|
|
1426
|
+
refreshedAt: new Date().toISOString(),
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
adapter,
|
|
1431
|
+
store,
|
|
1432
|
+
invocationWorkspaceKey: storeKey,
|
|
1433
|
+
workspaceId,
|
|
1434
|
+
};
|
|
688
1435
|
}
|
|
689
1436
|
catch {
|
|
690
|
-
return
|
|
1437
|
+
return {
|
|
1438
|
+
adapter: undefined,
|
|
1439
|
+
store: undefined,
|
|
1440
|
+
invocationWorkspaceKey: undefined,
|
|
1441
|
+
workspaceId: undefined,
|
|
1442
|
+
};
|
|
691
1443
|
}
|
|
692
1444
|
}
|
|
693
1445
|
export function createLoreOpenCodePlugin(dependencies = {}) {
|
|
694
1446
|
return async (context, options) => {
|
|
695
1447
|
const env = dependencies.env ?? process.env;
|
|
696
1448
|
const timeoutMs = pluginTimeout(options, env);
|
|
697
|
-
const
|
|
1449
|
+
const runtime = await createAdapter(dependencies, timeoutMs);
|
|
1450
|
+
const invocation = runtime.invocationWorkspaceKey === undefined
|
|
1451
|
+
? null
|
|
1452
|
+
: createIntegrationInvocationAttempt("plugin/opencode", { runtimeVersion: OPENCODE_PLUGIN_VERSION });
|
|
1453
|
+
const invocationRecorded = runtime.invocationWorkspaceKey === undefined || invocation === null
|
|
1454
|
+
? false
|
|
1455
|
+
: await writeInvocationAttemptBounded(runtime.invocationWorkspaceKey, invocation, { home: effectiveHome(dependencies) }).catch(() => false);
|
|
698
1456
|
const repositoryScope = dependencies.repositoryScope === undefined
|
|
699
1457
|
? await repositoryScopeFromWorktree(context.worktree)
|
|
700
1458
|
: canonicalRepositoryScope(dependencies.repositoryScope);
|
|
701
|
-
|
|
1459
|
+
try {
|
|
1460
|
+
const hooks = new LoreOpenCodeRuntime(context, runtime.adapter, timeoutMs, repositoryScope, runtime.store, runtime.workspaceId).hooks();
|
|
1461
|
+
if (runtime.invocationWorkspaceKey !== undefined &&
|
|
1462
|
+
invocation !== null &&
|
|
1463
|
+
invocationRecorded) {
|
|
1464
|
+
await writeInvocationCompletionBounded(runtime.invocationWorkspaceKey, invocation, { success: true }, { home: effectiveHome(dependencies) }).catch(() => undefined);
|
|
1465
|
+
}
|
|
1466
|
+
return hooks;
|
|
1467
|
+
}
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
if (runtime.invocationWorkspaceKey !== undefined &&
|
|
1470
|
+
invocation !== null &&
|
|
1471
|
+
invocationRecorded) {
|
|
1472
|
+
await writeInvocationCompletionBounded(runtime.invocationWorkspaceKey, invocation, { success: false, failureCode: "runtime_error" }, { home: effectiveHome(dependencies) }).catch(() => undefined);
|
|
1473
|
+
}
|
|
1474
|
+
throw error;
|
|
1475
|
+
}
|
|
702
1476
|
};
|
|
703
1477
|
}
|
|
704
1478
|
export const LoreOpenCodePlugin = createLoreOpenCodePlugin();
|