opencode-codex-memory 0.6.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -28
- package/dist/opencode.json +1 -1
- package/dist/src/citation.d.ts +9 -0
- package/dist/src/citation.js +68 -11
- package/dist/src/db.js +10 -0
- package/dist/src/host-client.d.ts +1 -0
- package/dist/src/host-client.js +1 -0
- package/dist/src/index.d.ts +12 -2
- package/dist/src/index.js +32 -5
- package/dist/src/llm.d.ts +6 -0
- package/dist/src/llm.js +21 -10
- package/dist/src/phase2.d.ts +2 -0
- package/dist/src/phase2.js +1 -1
- package/dist/src/rollout-input.d.ts +6 -0
- package/dist/src/rollout-input.js +111 -0
- package/dist/src/store.d.ts +17 -1
- package/dist/src/store.js +90 -4
- package/dist/src/v2/agents.d.ts +53 -0
- package/dist/src/v2/agents.js +204 -0
- package/dist/src/v2/citation-overlay.d.ts +7 -0
- package/dist/src/v2/citation-overlay.js +52 -0
- package/dist/src/v2/index.d.ts +7 -0
- package/dist/src/v2/index.js +10 -0
- package/dist/src/v2/injection.d.ts +14 -0
- package/dist/src/v2/injection.js +19 -0
- package/dist/src/v2/plugin.d.ts +7 -0
- package/dist/src/v2/plugin.js +482 -0
- package/dist/src/v2/service.d.ts +74 -0
- package/dist/src/v2/service.js +173 -0
- package/dist/src/v2/shim.d.ts +47 -0
- package/dist/src/v2/shim.js +581 -0
- package/dist/src/v2/status-rpc.d.ts +197 -0
- package/dist/src/v2/status-rpc.js +159 -0
- package/dist/src/v2/status.d.ts +3 -0
- package/dist/src/v2/status.js +83 -0
- package/dist/src/v2/tools.d.ts +33 -0
- package/dist/src/v2/tools.js +57 -0
- package/dist/src/v2/tui.d.ts +3 -0
- package/dist/src/v2/tui.js +750 -0
- package/opencode.json +1 -1
- package/package.json +38 -2
package/dist/src/store.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare class MemoryStore {
|
|
|
52
52
|
pruneStage1Outputs(maxUnusedDays: number): number;
|
|
53
53
|
upsertStage1Output(out: Omit<Stage1Output, "usage_count" | "last_usage">): boolean;
|
|
54
54
|
recordUsage(sessionIds: string[]): void;
|
|
55
|
+
/**
|
|
56
|
+
* Record citations from a durable assistant message exactly once. V2 can
|
|
57
|
+
* observe the same persisted text through session.text.ended and later
|
|
58
|
+
* context calls, and either observation may happen after a process restart.
|
|
59
|
+
*/
|
|
60
|
+
recordUsageOnce(sessionId: string, assistantMessageId: string, citedSessionIds: string[]): string[];
|
|
55
61
|
claimStage1Jobs(sessions: ClaimableSession[], excludeSession?: string, maxClaimed?: number): Stage1Claim[];
|
|
56
62
|
markStage1Succeeded(sessionId: string, ownershipToken: string, out: Omit<Stage1Output, "usage_count" | "last_usage">): void;
|
|
57
63
|
/** Extraction succeeded but produced nothing worth keeping: finish the job and drop any stale output. */
|
|
@@ -75,7 +81,9 @@ export declare class MemoryStore {
|
|
|
75
81
|
* already running, preserve its lease and advance only the input watermark.
|
|
76
82
|
*/
|
|
77
83
|
private enqueueGlobalConsolidation;
|
|
78
|
-
claimGlobalPhase2Job(
|
|
84
|
+
claimGlobalPhase2Job(opts?: {
|
|
85
|
+
bypassCooldown?: boolean;
|
|
86
|
+
}): Phase2ClaimResult;
|
|
79
87
|
heartbeatPhase2Job(ownershipToken: string): boolean;
|
|
80
88
|
/**
|
|
81
89
|
* Marks the phase-2 job done and records exactly which stage-1 snapshots the
|
|
@@ -95,6 +103,7 @@ export declare class MemoryStore {
|
|
|
95
103
|
last_error: string | null;
|
|
96
104
|
finished_at: number | null;
|
|
97
105
|
retry_at: number | null;
|
|
106
|
+
lease_until: number | null;
|
|
98
107
|
success_finished_at: number | null;
|
|
99
108
|
last_success_watermark: number | null;
|
|
100
109
|
} | null;
|
|
@@ -108,6 +117,13 @@ export declare class MemoryStore {
|
|
|
108
117
|
* Plugin dispose/reload: release the global phase-2 job without retry backoff
|
|
109
118
|
* so the next process can reclaim immediately. Ownership-token guarded.
|
|
110
119
|
*/
|
|
120
|
+
/**
|
|
121
|
+
* Boot-time sweep: a `running` global row whose owning pid is dead can never
|
|
122
|
+
* be finished by anyone; release it so the next pass can reclaim instead of
|
|
123
|
+
* waiting the full lease out. Rows from live pids (a peer instance) and rows
|
|
124
|
+
* without a pid tag (older schema) are left alone.
|
|
125
|
+
*/
|
|
126
|
+
releaseOrphanedPhase2Job(): boolean;
|
|
111
127
|
releasePhase2OnShutdown(ownershipToken: string): void;
|
|
112
128
|
/**
|
|
113
129
|
* Phase 2 input set, mirroring codex get_phase2_input_selection:
|
package/dist/src/store.js
CHANGED
|
@@ -12,6 +12,26 @@ export const PRUNE_BATCH_SIZE = 200;
|
|
|
12
12
|
function newId() {
|
|
13
13
|
return crypto.randomUUID();
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Phase-2 worker ids embed the owning pid so a later boot can tell a live
|
|
17
|
+
* peer's lease from one orphaned by a hard kill (host auto-update/restart).
|
|
18
|
+
*/
|
|
19
|
+
const PHASE2_WORKER_PREFIX = `pid:${process.pid}:`;
|
|
20
|
+
function phase2WorkerId() {
|
|
21
|
+
return `${PHASE2_WORKER_PREFIX}${crypto.randomUUID()}`;
|
|
22
|
+
}
|
|
23
|
+
function pidAlive(pid) {
|
|
24
|
+
if (pid === process.pid)
|
|
25
|
+
return true;
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
// EPERM: exists but not ours. Only ESRCH proves it is gone.
|
|
32
|
+
return err.code !== "ESRCH";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
15
35
|
function now() {
|
|
16
36
|
return Date.now();
|
|
17
37
|
}
|
|
@@ -98,6 +118,39 @@ export class MemoryStore {
|
|
|
98
118
|
stmt.run(ts, id);
|
|
99
119
|
}).immediate();
|
|
100
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Record citations from a durable assistant message exactly once. V2 can
|
|
123
|
+
* observe the same persisted text through session.text.ended and later
|
|
124
|
+
* context calls, and either observation may happen after a process restart.
|
|
125
|
+
*/
|
|
126
|
+
recordUsageOnce(sessionId, assistantMessageId, citedSessionIds) {
|
|
127
|
+
const ids = [...new Set(citedSessionIds)];
|
|
128
|
+
if (!sessionId || !assistantMessageId || ids.length === 0)
|
|
129
|
+
return [];
|
|
130
|
+
const fresh = [];
|
|
131
|
+
const insert = this.db.prepare(`INSERT OR IGNORE INTO memory_citation_usage
|
|
132
|
+
(session_id, assistant_message_id, cited_session_id, recorded_at)
|
|
133
|
+
VALUES (?, ?, ?, ?)`);
|
|
134
|
+
const update = this.db.prepare("UPDATE memory_stage1_outputs SET usage_count = usage_count + 1, last_usage = ? WHERE session_id = ?");
|
|
135
|
+
const ts = now();
|
|
136
|
+
this.db.transaction(() => {
|
|
137
|
+
for (const citedSessionId of ids) {
|
|
138
|
+
const result = insert.run(sessionId, assistantMessageId, citedSessionId, ts);
|
|
139
|
+
if (result.changes === 0)
|
|
140
|
+
continue;
|
|
141
|
+
const updated = update.run(ts, citedSessionId);
|
|
142
|
+
if (updated.changes > 0) {
|
|
143
|
+
fresh.push(citedSessionId);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
this.db
|
|
147
|
+
.prepare("DELETE FROM memory_citation_usage WHERE session_id = ? AND assistant_message_id = ? AND cited_session_id = ?")
|
|
148
|
+
.run(sessionId, assistantMessageId, citedSessionId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}).immediate();
|
|
152
|
+
return fresh;
|
|
153
|
+
}
|
|
101
154
|
claimStage1Jobs(sessions, excludeSession, maxClaimed) {
|
|
102
155
|
const workerId = newId();
|
|
103
156
|
// Cap per-pass claims at codex's max_rollouts_per_startup (max_claimed,
|
|
@@ -286,8 +339,8 @@ export class MemoryStore {
|
|
|
286
339
|
END`)
|
|
287
340
|
.run(DEFAULT_RETRY_REMAINING, inputWatermark);
|
|
288
341
|
}
|
|
289
|
-
claimGlobalPhase2Job() {
|
|
290
|
-
const workerId =
|
|
342
|
+
claimGlobalPhase2Job(opts = {}) {
|
|
343
|
+
const workerId = phase2WorkerId();
|
|
291
344
|
const ownershipToken = newId();
|
|
292
345
|
const tNow = nowSec();
|
|
293
346
|
const lease = tNow + PHASE2_LEASE_SECONDS;
|
|
@@ -309,7 +362,10 @@ export class MemoryStore {
|
|
|
309
362
|
}
|
|
310
363
|
// codex: cooldown after a clean success (last_error IS NULL AND
|
|
311
364
|
// finished_at within the window); failures fall through to retry_at.
|
|
312
|
-
if (
|
|
365
|
+
if (!opts.bypassCooldown &&
|
|
366
|
+
row.last_error == null &&
|
|
367
|
+
row.finished_at != null &&
|
|
368
|
+
tNow - row.finished_at < PHASE2_COOLDOWN_MS / 1000) {
|
|
313
369
|
return { type: "skipped_cooldown" };
|
|
314
370
|
}
|
|
315
371
|
// codex gates on retry_at regardless of status and never exhausts
|
|
@@ -378,7 +434,7 @@ export class MemoryStore {
|
|
|
378
434
|
*/
|
|
379
435
|
phase2JobSnapshot() {
|
|
380
436
|
const row = this.db
|
|
381
|
-
.prepare(`SELECT status, finished_at, last_error, retry_at, last_success_watermark FROM memory_jobs
|
|
437
|
+
.prepare(`SELECT status, finished_at, last_error, retry_at, lease_until, last_success_watermark FROM memory_jobs
|
|
382
438
|
WHERE kind='memory_consolidate_global' AND job_key='global'`)
|
|
383
439
|
.get();
|
|
384
440
|
if (!row)
|
|
@@ -398,6 +454,7 @@ export class MemoryStore {
|
|
|
398
454
|
last_error: row.last_error,
|
|
399
455
|
finished_at: row.finished_at,
|
|
400
456
|
retry_at: row.retry_at,
|
|
457
|
+
lease_until: row.lease_until,
|
|
401
458
|
// Codex preserves last_success_watermark across later attempts, while the
|
|
402
459
|
// job finished_at describes only the latest attempt. Never label a failure
|
|
403
460
|
// timestamp as a success finish time.
|
|
@@ -447,6 +504,34 @@ export class MemoryStore {
|
|
|
447
504
|
* Plugin dispose/reload: release the global phase-2 job without retry backoff
|
|
448
505
|
* so the next process can reclaim immediately. Ownership-token guarded.
|
|
449
506
|
*/
|
|
507
|
+
/**
|
|
508
|
+
* Boot-time sweep: a `running` global row whose owning pid is dead can never
|
|
509
|
+
* be finished by anyone; release it so the next pass can reclaim instead of
|
|
510
|
+
* waiting the full lease out. Rows from live pids (a peer instance) and rows
|
|
511
|
+
* without a pid tag (older schema) are left alone.
|
|
512
|
+
*/
|
|
513
|
+
releaseOrphanedPhase2Job() {
|
|
514
|
+
const row = this.db
|
|
515
|
+
.prepare(`SELECT worker_id FROM memory_jobs
|
|
516
|
+
WHERE kind='memory_consolidate_global' AND job_key='global' AND status='running'`)
|
|
517
|
+
.get();
|
|
518
|
+
const m = row?.worker_id?.match(/^pid:(\d+):/);
|
|
519
|
+
if (!m)
|
|
520
|
+
return false;
|
|
521
|
+
if (pidAlive(Number(m[1])))
|
|
522
|
+
return false;
|
|
523
|
+
const res = this.db
|
|
524
|
+
.prepare(`UPDATE memory_jobs SET
|
|
525
|
+
status = 'pending',
|
|
526
|
+
last_error = ?,
|
|
527
|
+
retry_at = NULL,
|
|
528
|
+
finished_at = ?,
|
|
529
|
+
lease_until = NULL,
|
|
530
|
+
ownership_token = NULL
|
|
531
|
+
WHERE kind='memory_consolidate_global' AND job_key='global' AND status='running' AND worker_id=?`)
|
|
532
|
+
.run(`owner pid ${m[1]} exited before finishing`, nowSec(), row.worker_id);
|
|
533
|
+
return res.changes > 0;
|
|
534
|
+
}
|
|
450
535
|
releasePhase2OnShutdown(ownershipToken) {
|
|
451
536
|
this.db
|
|
452
537
|
.prepare(`UPDATE memory_jobs SET
|
|
@@ -515,6 +600,7 @@ export class MemoryStore {
|
|
|
515
600
|
this.db.transaction(() => {
|
|
516
601
|
this.db.run("DELETE FROM memory_stage1_outputs");
|
|
517
602
|
this.db.run("DELETE FROM memory_jobs");
|
|
603
|
+
this.db.run("DELETE FROM memory_citation_usage");
|
|
518
604
|
this.db
|
|
519
605
|
.prepare(`INSERT INTO memory_jobs
|
|
520
606
|
(kind, job_key, status, finished_at, last_error, retry_remaining, last_success_watermark)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export declare const MEMORIZE_AGENT_ID = "memorize";
|
|
2
|
+
export declare const MEMORIZE_EXTRACT_AGENT_ID = "memorize-extract";
|
|
3
|
+
export declare const MEMORIZE_SYSTEM = "You are a memory consolidation agent. Read the workspace diff file and update MEMORY.md, memory_summary.md, and skills/ under the memory workspace only. Do not read or edit project source files outside that memory root. Keep memory_summary.md under 10000 chars (2500 tokens). Prune stale entries. Do not access the network.";
|
|
4
|
+
export declare const MEMORIZE_EXTRACT_SYSTEM = "You are a memory extraction agent. The session transcript is provided inline in the prompt. Extract raw_memory, rollout_summary, and rollout_slug as JSON. Exclude AGENTS.md/instruction content. Redact secrets.";
|
|
5
|
+
export declare const MEMORIZE_DESCRIPTION = "Memory consolidation agent (opencode-codex-memory)";
|
|
6
|
+
export declare const MEMORIZE_EXTRACT_DESCRIPTION = "Memory extraction agent (opencode-codex-memory)";
|
|
7
|
+
export interface V2AgentDefinition {
|
|
8
|
+
description: string;
|
|
9
|
+
mode: "subagent";
|
|
10
|
+
hidden?: boolean;
|
|
11
|
+
system: string;
|
|
12
|
+
permissions: {
|
|
13
|
+
action: string;
|
|
14
|
+
resource: string;
|
|
15
|
+
effect: "allow" | "deny" | "ask";
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
/** V2-native memorize definition (V2 action names: edit covers write/patch). */
|
|
19
|
+
export declare function buildMemorizeAgent(): V2AgentDefinition;
|
|
20
|
+
/**
|
|
21
|
+
* Hidden extraction definition. Unused at runtime (extraction goes through
|
|
22
|
+
* generate.text), kept for parity with the V1 bundle and the health
|
|
23
|
+
* snapshot. deny-all: V2 has no StructuredOutput-capture concept, and the
|
|
24
|
+
* sessionless generate path needs no tools at all.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildMemorizeExtractAgent(): V2AgentDefinition;
|
|
27
|
+
interface AgentEditorLike {
|
|
28
|
+
get(id: string): {
|
|
29
|
+
description?: string;
|
|
30
|
+
mode?: string;
|
|
31
|
+
system?: string;
|
|
32
|
+
permissions?: unknown;
|
|
33
|
+
} | undefined;
|
|
34
|
+
update(id: string, update: (agent: Record<string, unknown>) => void): void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* V2 agent definition converted back to the V1 permission-map shape so the
|
|
38
|
+
* shared agent-health snapshot (surfaced by memory_inspect) keeps working.
|
|
39
|
+
*/
|
|
40
|
+
export declare function toV1AgentDefinition(def: V2AgentDefinition, agentId?: string): Record<string, unknown>;
|
|
41
|
+
export declare function shippedV1AgentForHealth(): Record<string, unknown>;
|
|
42
|
+
/**
|
|
43
|
+
* Ensure both sub-agents exist. A user-defined agent of the same name
|
|
44
|
+
* always wins (mirrors injectAgentDefinitions). Records health for
|
|
45
|
+
* memory_inspect in both cases.
|
|
46
|
+
*/
|
|
47
|
+
export declare function ensureV2Agents(ctx: {
|
|
48
|
+
agent: {
|
|
49
|
+
transform(cb: (editor: AgentEditorLike) => void): Promise<unknown>;
|
|
50
|
+
get(input: unknown): Promise<unknown>;
|
|
51
|
+
};
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode2 agent provisioning.
|
|
3
|
+
*
|
|
4
|
+
* V1 ships memorize/memorize-extract via the config hook (opencode.json
|
|
5
|
+
* `agent` map). V2 has no config hook, but agent.transform's update() creates
|
|
6
|
+
* a missing agent in the active plugin location, so setup() ensures them here
|
|
7
|
+
* instead. Helper sessions must run in that same location because V2 agent
|
|
8
|
+
* registration is location-scoped.
|
|
9
|
+
*
|
|
10
|
+
* Both agents ship for parity, with the V1 prompts verbatim. `memorize` does
|
|
11
|
+
* consolidation work; `memorize-extract` is hidden and unused — extraction
|
|
12
|
+
* runs through generate.text (inherently tool-less, no session, no agent),
|
|
13
|
+
* and V1 skips injecting unused agents for the same reason. Keeping the
|
|
14
|
+
* hidden definition preserves the health snapshot both agents report into.
|
|
15
|
+
*/
|
|
16
|
+
import path from "path";
|
|
17
|
+
import { memoryRoot } from "../paths.js";
|
|
18
|
+
import { pluginOptions } from "../options.js";
|
|
19
|
+
import { recordAgentConfig } from "../agent-health.js";
|
|
20
|
+
export const MEMORIZE_AGENT_ID = "memorize";
|
|
21
|
+
export const MEMORIZE_EXTRACT_AGENT_ID = "memorize-extract";
|
|
22
|
+
export const MEMORIZE_SYSTEM = "You are a memory consolidation agent. Read the workspace diff file and update MEMORY.md, memory_summary.md, and skills/ under the memory workspace only. Do not read or edit project source files outside that memory root. Keep memory_summary.md under 10000 chars (2500 tokens). Prune stale entries. Do not access the network.";
|
|
23
|
+
export const MEMORIZE_EXTRACT_SYSTEM = "You are a memory extraction agent. The session transcript is provided inline in the prompt. Extract raw_memory, rollout_summary, and rollout_slug as JSON. Exclude AGENTS.md/instruction content. Redact secrets.";
|
|
24
|
+
export const MEMORIZE_DESCRIPTION = "Memory consolidation agent (opencode-codex-memory)";
|
|
25
|
+
export const MEMORIZE_EXTRACT_DESCRIPTION = "Memory extraction agent (opencode-codex-memory)";
|
|
26
|
+
/** V2-native memorize definition (V2 action names: edit covers write/patch). */
|
|
27
|
+
export function buildMemorizeAgent() {
|
|
28
|
+
return {
|
|
29
|
+
description: MEMORIZE_DESCRIPTION,
|
|
30
|
+
mode: "subagent",
|
|
31
|
+
system: MEMORIZE_SYSTEM,
|
|
32
|
+
permissions: [
|
|
33
|
+
{ action: "*", resource: "*", effect: "deny" },
|
|
34
|
+
{ action: "read", resource: path.join(memoryRoot(), "*"), effect: "allow" },
|
|
35
|
+
{ action: "edit", resource: path.join(memoryRoot(), "*"), effect: "allow" },
|
|
36
|
+
{ action: "glob", resource: path.join(memoryRoot(), "*"), effect: "allow" },
|
|
37
|
+
{ action: "grep", resource: path.join(memoryRoot(), "*"), effect: "allow" },
|
|
38
|
+
// Memories live outside every project: without this grant the wildcard
|
|
39
|
+
// deny blocks consolidation from touching the memory workspace (same
|
|
40
|
+
// role as external_directory in the V1 definition).
|
|
41
|
+
{ action: "external_directory", resource: path.join(memoryRoot(), "*"), effect: "allow" },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Hidden extraction definition. Unused at runtime (extraction goes through
|
|
47
|
+
* generate.text), kept for parity with the V1 bundle and the health
|
|
48
|
+
* snapshot. deny-all: V2 has no StructuredOutput-capture concept, and the
|
|
49
|
+
* sessionless generate path needs no tools at all.
|
|
50
|
+
*/
|
|
51
|
+
export function buildMemorizeExtractAgent() {
|
|
52
|
+
return {
|
|
53
|
+
description: MEMORIZE_EXTRACT_DESCRIPTION,
|
|
54
|
+
mode: "subagent",
|
|
55
|
+
hidden: true,
|
|
56
|
+
system: MEMORIZE_EXTRACT_SYSTEM,
|
|
57
|
+
permissions: [{ action: "*", resource: "*", effect: "deny" }],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* V2 agent definition converted back to the V1 permission-map shape so the
|
|
62
|
+
* shared agent-health snapshot (surfaced by memory_inspect) keeps working.
|
|
63
|
+
*/
|
|
64
|
+
export function toV1AgentDefinition(def, agentId) {
|
|
65
|
+
const permission = {};
|
|
66
|
+
for (const rule of def.permissions) {
|
|
67
|
+
if (rule.action === "*") {
|
|
68
|
+
permission["*"] = rule.effect;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (rule.action === "external_directory") {
|
|
72
|
+
const cur = (permission.external_directory ?? {});
|
|
73
|
+
cur[rule.resource] = rule.effect;
|
|
74
|
+
permission.external_directory = cur;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (rule.action === "edit") {
|
|
78
|
+
// V1 names write/patch separately from edit.
|
|
79
|
+
permission.edit = rule.effect;
|
|
80
|
+
permission.write = rule.effect;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
permission[rule.action] = rule.effect;
|
|
84
|
+
}
|
|
85
|
+
if (agentId === MEMORIZE_EXTRACT_AGENT_ID) {
|
|
86
|
+
// The V1 bundle allows the synthetic StructuredOutput capture tool so
|
|
87
|
+
// json_schema extraction works; V2 needs no tools for the sessionless
|
|
88
|
+
// generate path, but health validates the V1 bundle semantics.
|
|
89
|
+
permission.StructuredOutput = "allow";
|
|
90
|
+
}
|
|
91
|
+
return { mode: def.mode, prompt: def.system, description: def.description, permission };
|
|
92
|
+
}
|
|
93
|
+
export function shippedV1AgentForHealth() {
|
|
94
|
+
return {
|
|
95
|
+
[MEMORIZE_AGENT_ID]: toV1AgentDefinition(buildMemorizeAgent(), MEMORIZE_AGENT_ID),
|
|
96
|
+
[MEMORIZE_EXTRACT_AGENT_ID]: toV1AgentDefinition(buildMemorizeExtractAgent(), MEMORIZE_EXTRACT_AGENT_ID),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Ensure both sub-agents exist. A user-defined agent of the same name
|
|
101
|
+
* always wins (mirrors injectAgentDefinitions). Records health for
|
|
102
|
+
* memory_inspect in both cases.
|
|
103
|
+
*/
|
|
104
|
+
export async function ensureV2Agents(ctx) {
|
|
105
|
+
const shipped = [
|
|
106
|
+
[MEMORIZE_AGENT_ID, buildMemorizeAgent()],
|
|
107
|
+
[MEMORIZE_EXTRACT_AGENT_ID, buildMemorizeExtractAgent()],
|
|
108
|
+
];
|
|
109
|
+
if (!pluginOptions.generate_memories) {
|
|
110
|
+
recordAgentConfig({ agent: {} }, false, shippedV1AgentForHealth());
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const effective = {};
|
|
114
|
+
const missing = [];
|
|
115
|
+
for (const [id, def] of shipped) {
|
|
116
|
+
let existing = null;
|
|
117
|
+
try {
|
|
118
|
+
const res = (await ctx.agent.get({ agentID: id }));
|
|
119
|
+
existing = res && typeof res === "object" && "data" in res ? res.data : res;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
existing = null;
|
|
123
|
+
}
|
|
124
|
+
if (existing && typeof existing === "object") {
|
|
125
|
+
const cur = existing;
|
|
126
|
+
if (isShippedIncomplete(id, cur, def)) {
|
|
127
|
+
missing.push([id, def]);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
effective[id] = {
|
|
131
|
+
mode: cur.mode,
|
|
132
|
+
prompt: cur.system,
|
|
133
|
+
description: cur.description,
|
|
134
|
+
permission: v2PermissionsToV1Map(cur.permissions),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
missing.push([id, def]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (missing.length > 0) {
|
|
143
|
+
await ctx.agent.transform((editor) => {
|
|
144
|
+
for (const [id, def] of missing) {
|
|
145
|
+
editor.update(id, (agent) => {
|
|
146
|
+
agent.description = def.description;
|
|
147
|
+
agent.mode = def.mode;
|
|
148
|
+
if (def.hidden !== undefined)
|
|
149
|
+
agent.hidden = def.hidden;
|
|
150
|
+
agent.system = def.system;
|
|
151
|
+
agent.permissions = def.permissions.map((r) => ({ ...r }));
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
for (const [id, def] of missing) {
|
|
156
|
+
effective[id] = toV1AgentDefinition(def, id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
recordAgentConfig({ agent: effective }, true, shippedV1AgentForHealth());
|
|
160
|
+
}
|
|
161
|
+
function isShippedIncomplete(id, existing, shipped) {
|
|
162
|
+
const system = typeof existing.system === "string"
|
|
163
|
+
? existing.system
|
|
164
|
+
: typeof existing.prompt === "string"
|
|
165
|
+
? existing.prompt
|
|
166
|
+
: "";
|
|
167
|
+
if (system !== shipped.system)
|
|
168
|
+
return false;
|
|
169
|
+
if (typeof existing.description === "string" && existing.description !== shipped.description)
|
|
170
|
+
return false;
|
|
171
|
+
if (id !== MEMORIZE_AGENT_ID)
|
|
172
|
+
return false;
|
|
173
|
+
const perms = existing.permissions;
|
|
174
|
+
if (!Array.isArray(perms))
|
|
175
|
+
return true;
|
|
176
|
+
return !perms.some((rule) => rule && typeof rule === "object" && rule.action === "external_directory");
|
|
177
|
+
}
|
|
178
|
+
function v2PermissionsToV1Map(permissions) {
|
|
179
|
+
const out = {};
|
|
180
|
+
if (!Array.isArray(permissions))
|
|
181
|
+
return out;
|
|
182
|
+
for (const rule of permissions) {
|
|
183
|
+
if (typeof rule?.action !== "string")
|
|
184
|
+
continue;
|
|
185
|
+
if (rule.action === "*") {
|
|
186
|
+
out["*"] = rule.effect;
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (rule.action === "external_directory" && typeof rule.resource === "string") {
|
|
190
|
+
const cur = (out.external_directory ?? {});
|
|
191
|
+
cur[rule.resource] = rule.effect;
|
|
192
|
+
out.external_directory = cur;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (rule.action === "edit") {
|
|
196
|
+
out.edit = rule.effect;
|
|
197
|
+
out.write = rule.effect;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
out[rule.action] = rule.effect;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2-only citation instructions. V1 `read_path.md` keeps the XML contract;
|
|
3
|
+
* the TUI renders fenced `memory-citation` blocks, so the V2 context hook
|
|
4
|
+
* overlays this section onto the shared inject prompt.
|
|
5
|
+
*/
|
|
6
|
+
export declare const V2_CITATION_INSTRUCTIONS = "Memory citation requirements:\n\n- If ANY relevant memory files were used: append exactly one fenced code\n block with the language tag `memory-citation` as the VERY LAST content of\n the final reply. Normal responses should include the answer first, then\n append the block at the end. The host renders this block natively.\n- Use this exact structure for programmatic parsing:\n````\n```memory-citation\nMEMORY.md:234-236|note=build command for the api service\nrollout_summaries/2026-02-17T21-23-02-ln3m-example.md:10-12|note=weekly report format\nsessions: ses_abc123 ses_def456\n```\n````\n- Do not wrap it in `<memory-citation>` XML tags; the fenced form replaces\n that older format.\n- Citation entry lines are for rendering:\n - one citation entry per line\n - format: `<file>:<line_start>-<line_end>|note=<how memory was used>`\n - use file paths relative to the memory base path (for example, `MEMORY.md`,\n `rollout_summaries/...`, `skills/...`)\n - only cite files actually used under the memory base path (do not cite\n workspace files as memory citations)\n - if you used `MEMORY.md` and then a rollout summary/skill file, cite both\n - list entries in order of importance (most important first)\n - `note` should be short, single-line, and use simple characters only (avoid\n unusual symbols, no newlines)\n- The final `sessions:` line is for us to track which past sessions you find\n useful:\n - one line, space-separated session ids after `sessions:`\n - session ids look like `ses_...` and appear in rollout summary files and\n MEMORY.md as `session_id:`\n - include unique ids only; do not repeat ids\n - omit the `sessions:` line if no session ids are available\n - do not include file paths or notes on this line\n - for every citation entry, try to find and cite the corresponding session id\n- Never include memory citations inside pull-request messages.\n- Never cite blank lines; double-check ranges.\n\n";
|
|
7
|
+
export declare function overlayV2CitationInstructions(prompt: string): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V2-only citation instructions. V1 `read_path.md` keeps the XML contract;
|
|
3
|
+
* the TUI renders fenced `memory-citation` blocks, so the V2 context hook
|
|
4
|
+
* overlays this section onto the shared inject prompt.
|
|
5
|
+
*/
|
|
6
|
+
export const V2_CITATION_INSTRUCTIONS = `Memory citation requirements:
|
|
7
|
+
|
|
8
|
+
- If ANY relevant memory files were used: append exactly one fenced code
|
|
9
|
+
block with the language tag \`memory-citation\` as the VERY LAST content of
|
|
10
|
+
the final reply. Normal responses should include the answer first, then
|
|
11
|
+
append the block at the end. The host renders this block natively.
|
|
12
|
+
- Use this exact structure for programmatic parsing:
|
|
13
|
+
\`\`\`\`
|
|
14
|
+
\`\`\`memory-citation
|
|
15
|
+
MEMORY.md:234-236|note=build command for the api service
|
|
16
|
+
rollout_summaries/2026-02-17T21-23-02-ln3m-example.md:10-12|note=weekly report format
|
|
17
|
+
sessions: ses_abc123 ses_def456
|
|
18
|
+
\`\`\`
|
|
19
|
+
\`\`\`\`
|
|
20
|
+
- Do not wrap it in \`<memory-citation>\` XML tags; the fenced form replaces
|
|
21
|
+
that older format.
|
|
22
|
+
- Citation entry lines are for rendering:
|
|
23
|
+
- one citation entry per line
|
|
24
|
+
- format: \`<file>:<line_start>-<line_end>|note=<how memory was used>\`
|
|
25
|
+
- use file paths relative to the memory base path (for example, \`MEMORY.md\`,
|
|
26
|
+
\`rollout_summaries/...\`, \`skills/...\`)
|
|
27
|
+
- only cite files actually used under the memory base path (do not cite
|
|
28
|
+
workspace files as memory citations)
|
|
29
|
+
- if you used \`MEMORY.md\` and then a rollout summary/skill file, cite both
|
|
30
|
+
- list entries in order of importance (most important first)
|
|
31
|
+
- \`note\` should be short, single-line, and use simple characters only (avoid
|
|
32
|
+
unusual symbols, no newlines)
|
|
33
|
+
- The final \`sessions:\` line is for us to track which past sessions you find
|
|
34
|
+
useful:
|
|
35
|
+
- one line, space-separated session ids after \`sessions:\`
|
|
36
|
+
- session ids look like \`ses_...\` and appear in rollout summary files and
|
|
37
|
+
MEMORY.md as \`session_id:\`
|
|
38
|
+
- include unique ids only; do not repeat ids
|
|
39
|
+
- omit the \`sessions:\` line if no session ids are available
|
|
40
|
+
- do not include file paths or notes on this line
|
|
41
|
+
- for every citation entry, try to find and cite the corresponding session id
|
|
42
|
+
- Never include memory citations inside pull-request messages.
|
|
43
|
+
- Never cite blank lines; double-check ranges.
|
|
44
|
+
|
|
45
|
+
`;
|
|
46
|
+
export function overlayV2CitationInstructions(prompt) {
|
|
47
|
+
const start = prompt.indexOf("Memory citation requirements:");
|
|
48
|
+
const end = prompt.indexOf("Updating memories:");
|
|
49
|
+
if (start === -1 || end === -1 || end <= start)
|
|
50
|
+
return `${prompt.trimEnd()}\n\n${V2_CITATION_INSTRUCTIONS}`;
|
|
51
|
+
return prompt.slice(0, start) + V2_CITATION_INSTRUCTIONS + prompt.slice(end);
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode2 entry point (re-exported through the package main for
|
|
3
|
+
* single-entry dual-host support: V1 reads server(), V2 reads id+setup()).
|
|
4
|
+
*/
|
|
5
|
+
import { Plugin } from "@opencode/plugin";
|
|
6
|
+
import { setup } from "./plugin.js";
|
|
7
|
+
export default Plugin.define({
|
|
8
|
+
id: "opencode-codex-memory",
|
|
9
|
+
setup,
|
|
10
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ledger of memory text injected into model requests by the V2
|
|
3
|
+
* `context` hook. Token counts are the same chars/4 estimate used for the
|
|
4
|
+
* injection cap, so they are comparable to `memory_inspect`'s
|
|
5
|
+
* `summary_tokens_est`, not provider-billed tokens.
|
|
6
|
+
*/
|
|
7
|
+
export interface InjectionTotals {
|
|
8
|
+
tokens: number;
|
|
9
|
+
requests: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function recordInjection(sessionID: string, tokens: number): void;
|
|
12
|
+
export declare function injectionTotals(): InjectionTotals;
|
|
13
|
+
export declare function sessionInjection(sessionID: string | null | undefined): InjectionTotals;
|
|
14
|
+
export declare function resetInjectionStats(): void;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const perSession = new Map();
|
|
2
|
+
let global = { tokens: 0, requests: 0 };
|
|
3
|
+
export function recordInjection(sessionID, tokens) {
|
|
4
|
+
global = { tokens: global.tokens + tokens, requests: global.requests + 1 };
|
|
5
|
+
const current = perSession.get(sessionID) ?? { tokens: 0, requests: 0 };
|
|
6
|
+
perSession.set(sessionID, { tokens: current.tokens + tokens, requests: current.requests + 1 });
|
|
7
|
+
}
|
|
8
|
+
export function injectionTotals() {
|
|
9
|
+
return { ...global };
|
|
10
|
+
}
|
|
11
|
+
export function sessionInjection(sessionID) {
|
|
12
|
+
if (!sessionID)
|
|
13
|
+
return { tokens: 0, requests: 0 };
|
|
14
|
+
return { ...(perSession.get(sessionID) ?? { tokens: 0, requests: 0 }) };
|
|
15
|
+
}
|
|
16
|
+
export function resetInjectionStats() {
|
|
17
|
+
perSession.clear();
|
|
18
|
+
global = { tokens: 0, requests: 0 };
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type V2Context } from "./shim.js";
|
|
2
|
+
/** Test seam: wait for hook-launched work to settle. */
|
|
3
|
+
export declare function waitForV2BackgroundTasks(): Promise<void>;
|
|
4
|
+
/** Test seam: reset module state between tests. */
|
|
5
|
+
export declare function resetV2ModuleStateForTest(): void;
|
|
6
|
+
export declare function markV2TurnSeen(sessionId: string): boolean;
|
|
7
|
+
export declare function setup(ctx: V2Context): Promise<(() => void | Promise<void>) | void>;
|