@mojoaar/opencode-mimir 0.24.0 → 0.25.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/dist/index.js CHANGED
@@ -18,6 +18,11 @@ export const MAX_TRANSCRIPT = 32_000;
18
18
  /** A compaction of nothing is not worth a model call. */
19
19
  export const MIN_TRANSCRIPT = 500;
20
20
  const LEARN_HINT = 'This session was just compacted. Keep what was decided, what broke and any convention, and skip what Mimir already has.';
21
+ const LEARN_NOW_HINT = 'The user asked for this session to be kept now. Keep what was decided, what broke and any convention, and skip what Mimir already has.';
22
+ const NOTHING_TO_SEND = 'Nothing worth handing over yet.';
23
+ const SENT = 'Mimir is writing what this session settled.';
24
+ const QUEUE_OFF = 'Mimir took it, but its queue is not running.';
25
+ const UNREACHABLE = 'Mimir is unreachable, so nothing was sent.';
21
26
  /** Message text is shaped differently by each provider, so read what we can and skip the rest. */
22
27
  export function transcript(messages) {
23
28
  const lines = [];
@@ -82,9 +87,15 @@ export default Plugin.define({
82
87
  // The reminder rides the message count. A shrink means a compaction just
83
88
  // happened, which is the worst moment to lose a detail, so it nudges early.
84
89
  const counts = new Map();
90
+ // The command below is handed no messages, only a session id, so the hook
91
+ // keeps the newest transcript for it. One string per live session, at most
92
+ // 32 KB, dropped once it has been sent.
93
+ const snapshots = new Map();
85
94
  const nudge = await ctx.session.hook('context', async (event) => {
86
95
  const state = counts.get(event.sessionID) ?? { turns: 0, nudgedAt: 0 };
87
96
  const turns = event.messages?.length ?? 0;
97
+ if (turns > state.turns)
98
+ snapshots.set(event.sessionID, transcript(event.messages ?? []));
88
99
  const due = turns < state.turns || turns - state.nudgedAt >= NUDGE_EVERY;
89
100
  counts.set(event.sessionID, { turns, nudgedAt: due ? turns : state.nudgedAt });
90
101
  if (due)
@@ -109,12 +120,38 @@ export default Plugin.define({
109
120
  if (!sent?.ok)
110
121
  compacted.delete(event.sessionID);
111
122
  });
123
+ // A manual handover, for the sessions that end before they ever compact.
124
+ const command = await ctx.command.transform((editor) => {
125
+ editor.add({
126
+ name: 'mimir-learn',
127
+ description: 'Hand this session to Mimir now.',
128
+ execute: async ({ sessionID }) => {
129
+ const text = snapshots.get(sessionID) ?? '';
130
+ let notice = NOTHING_TO_SEND;
131
+ if (text.length >= MIN_TRANSCRIPT) {
132
+ const sent = await callTool('memory_learn', { repo: repo ?? undefined, transcript: text, hint: LEARN_NOW_HINT }, { ...config, timeoutMs: 4000 }).catch(() => null);
133
+ const queued = sent?.ok ? sent.result.queued : undefined;
134
+ if (!sent?.ok)
135
+ notice = UNREACHABLE;
136
+ else if (queued === false)
137
+ notice = QUEUE_OFF;
138
+ else {
139
+ notice = SENT;
140
+ // Kept on failure above, so a retry has something to send.
141
+ snapshots.delete(sessionID);
142
+ }
143
+ }
144
+ await ctx.session.synthetic({ sessionID, text: notice }).catch(() => null);
145
+ },
146
+ });
147
+ });
112
148
  const tools = await registerTools(ctx, config, repo);
113
149
  return async () => {
114
150
  await tools.dispose();
115
151
  await session.dispose();
116
152
  await nudge.dispose();
117
153
  await compaction.dispose();
154
+ await command.dispose();
118
155
  };
119
156
  },
120
157
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojoaar/opencode-mimir",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Mimir long-term memory for OpenCode sessions",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/index.ts CHANGED
@@ -23,6 +23,12 @@ export const MAX_TRANSCRIPT = 32_000
23
23
  export const MIN_TRANSCRIPT = 500
24
24
  const LEARN_HINT =
25
25
  'This session was just compacted. Keep what was decided, what broke and any convention, and skip what Mimir already has.'
26
+ const LEARN_NOW_HINT =
27
+ 'The user asked for this session to be kept now. Keep what was decided, what broke and any convention, and skip what Mimir already has.'
28
+ const NOTHING_TO_SEND = 'Nothing worth handing over yet.'
29
+ const SENT = 'Mimir is writing what this session settled.'
30
+ const QUEUE_OFF = 'Mimir took it, but its queue is not running.'
31
+ const UNREACHABLE = 'Mimir is unreachable, so nothing was sent.'
26
32
 
27
33
  /** Message text is shaped differently by each provider, so read what we can and skip the rest. */
28
34
  export function transcript(messages: unknown[]): string {
@@ -91,9 +97,14 @@ export default Plugin.define({
91
97
  // The reminder rides the message count. A shrink means a compaction just
92
98
  // happened, which is the worst moment to lose a detail, so it nudges early.
93
99
  const counts = new Map<string, { turns: number; nudgedAt: number }>()
100
+ // The command below is handed no messages, only a session id, so the hook
101
+ // keeps the newest transcript for it. One string per live session, at most
102
+ // 32 KB, dropped once it has been sent.
103
+ const snapshots = new Map<string, string>()
94
104
  const nudge = await ctx.session.hook('context', async (event) => {
95
105
  const state = counts.get(event.sessionID) ?? { turns: 0, nudgedAt: 0 }
96
106
  const turns = event.messages?.length ?? 0
107
+ if (turns > state.turns) snapshots.set(event.sessionID, transcript(event.messages ?? []))
97
108
  const due = turns < state.turns || turns - state.nudgedAt >= NUDGE_EVERY
98
109
  counts.set(event.sessionID, { turns, nudgedAt: due ? turns : state.nudgedAt })
99
110
 
@@ -123,6 +134,34 @@ export default Plugin.define({
123
134
  if (!sent?.ok) compacted.delete(event.sessionID)
124
135
  })
125
136
 
137
+ // A manual handover, for the sessions that end before they ever compact.
138
+ const command = await ctx.command.transform((editor) => {
139
+ editor.add({
140
+ name: 'mimir-learn',
141
+ description: 'Hand this session to Mimir now.',
142
+ execute: async ({ sessionID }) => {
143
+ const text = snapshots.get(sessionID) ?? ''
144
+ let notice = NOTHING_TO_SEND
145
+ if (text.length >= MIN_TRANSCRIPT) {
146
+ const sent = await callTool(
147
+ 'memory_learn',
148
+ { repo: repo ?? undefined, transcript: text, hint: LEARN_NOW_HINT },
149
+ { ...config, timeoutMs: 4000 },
150
+ ).catch(() => null)
151
+ const queued = sent?.ok ? (sent.result as { queued?: boolean }).queued : undefined
152
+ if (!sent?.ok) notice = UNREACHABLE
153
+ else if (queued === false) notice = QUEUE_OFF
154
+ else {
155
+ notice = SENT
156
+ // Kept on failure above, so a retry has something to send.
157
+ snapshots.delete(sessionID)
158
+ }
159
+ }
160
+ await ctx.session.synthetic({ sessionID, text: notice }).catch(() => null)
161
+ },
162
+ })
163
+ })
164
+
126
165
  const tools = await registerTools(ctx, config, repo)
127
166
 
128
167
  return async () => {
@@ -130,6 +169,7 @@ export default Plugin.define({
130
169
  await session.dispose()
131
170
  await nudge.dispose()
132
171
  await compaction.dispose()
172
+ await command.dispose()
133
173
  }
134
174
  },
135
175
  })