@pcircle/memesh 4.7.3 → 4.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/AGENTS.md +3 -1
  4. package/README.de.md +42 -16
  5. package/README.md +46 -19
  6. package/README.zh-TW.md +42 -17
  7. package/dashboard/dist/index.html +1 -1
  8. package/dist/core/agent-message-storage.d.ts +76 -0
  9. package/dist/core/agent-message-storage.d.ts.map +1 -0
  10. package/dist/core/agent-message-storage.js +359 -0
  11. package/dist/core/agent-message-storage.js.map +1 -0
  12. package/dist/core/agent-messaging.d.ts +224 -0
  13. package/dist/core/agent-messaging.d.ts.map +1 -0
  14. package/dist/core/agent-messaging.js +881 -0
  15. package/dist/core/agent-messaging.js.map +1 -0
  16. package/dist/core/agent-router.d.ts +194 -0
  17. package/dist/core/agent-router.d.ts.map +1 -0
  18. package/dist/core/agent-router.js +1138 -0
  19. package/dist/core/agent-router.js.map +1 -0
  20. package/dist/core/briefing.d.ts.map +1 -1
  21. package/dist/core/briefing.js +8 -1
  22. package/dist/core/briefing.js.map +1 -1
  23. package/dist/core/config.d.ts.map +1 -1
  24. package/dist/core/config.js.map +1 -1
  25. package/dist/core/doctor.d.ts +19 -0
  26. package/dist/core/doctor.d.ts.map +1 -1
  27. package/dist/core/doctor.js +176 -2
  28. package/dist/core/doctor.js.map +1 -1
  29. package/dist/core/dreamer.d.ts +8 -5
  30. package/dist/core/dreamer.d.ts.map +1 -1
  31. package/dist/core/dreamer.js +88 -3
  32. package/dist/core/dreamer.js.map +1 -1
  33. package/dist/core/product-improvements.d.ts +61 -0
  34. package/dist/core/product-improvements.d.ts.map +1 -0
  35. package/dist/core/product-improvements.js +216 -0
  36. package/dist/core/product-improvements.js.map +1 -0
  37. package/dist/core/repo-state.d.ts +11 -0
  38. package/dist/core/repo-state.d.ts.map +1 -0
  39. package/dist/core/repo-state.js +70 -0
  40. package/dist/core/repo-state.js.map +1 -0
  41. package/dist/core/schema-export.d.ts.map +1 -1
  42. package/dist/core/schema-export.js +70 -0
  43. package/dist/core/schema-export.js.map +1 -1
  44. package/dist/core/task-state.d.ts.map +1 -1
  45. package/dist/core/task-state.js +3 -3
  46. package/dist/core/task-state.js.map +1 -1
  47. package/dist/core/updater.d.ts +2 -0
  48. package/dist/core/updater.d.ts.map +1 -1
  49. package/dist/core/updater.js +11 -5
  50. package/dist/core/updater.js.map +1 -1
  51. package/dist/core/work-topology.d.ts.map +1 -1
  52. package/dist/core/work-topology.js +1 -0
  53. package/dist/core/work-topology.js.map +1 -1
  54. package/dist/host-adapters/acp-client.d.ts +147 -0
  55. package/dist/host-adapters/acp-client.d.ts.map +1 -0
  56. package/dist/host-adapters/acp-client.js +793 -0
  57. package/dist/host-adapters/acp-client.js.map +1 -0
  58. package/dist/host-adapters/claude-channel.d.ts +75 -0
  59. package/dist/host-adapters/claude-channel.d.ts.map +1 -0
  60. package/dist/host-adapters/claude-channel.js +255 -0
  61. package/dist/host-adapters/claude-channel.js.map +1 -0
  62. package/dist/host-adapters/codex-app-server.d.ts +83 -0
  63. package/dist/host-adapters/codex-app-server.d.ts.map +1 -0
  64. package/dist/host-adapters/codex-app-server.js +360 -0
  65. package/dist/host-adapters/codex-app-server.js.map +1 -0
  66. package/dist/host-adapters/codex-cli-queue.d.ts +17 -0
  67. package/dist/host-adapters/codex-cli-queue.d.ts.map +1 -0
  68. package/dist/host-adapters/codex-cli-queue.js +85 -0
  69. package/dist/host-adapters/codex-cli-queue.js.map +1 -0
  70. package/dist/host-runtime/acp.d.ts +57 -0
  71. package/dist/host-runtime/acp.d.ts.map +1 -0
  72. package/dist/host-runtime/acp.js +338 -0
  73. package/dist/host-runtime/acp.js.map +1 -0
  74. package/dist/host-runtime/claude.d.ts +41 -0
  75. package/dist/host-runtime/claude.d.ts.map +1 -0
  76. package/dist/host-runtime/claude.js +213 -0
  77. package/dist/host-runtime/claude.js.map +1 -0
  78. package/dist/host-runtime/codex-session.d.ts +24 -0
  79. package/dist/host-runtime/codex-session.d.ts.map +1 -0
  80. package/dist/host-runtime/codex-session.js +98 -0
  81. package/dist/host-runtime/codex-session.js.map +1 -0
  82. package/dist/host-runtime/codex.d.ts +33 -0
  83. package/dist/host-runtime/codex.d.ts.map +1 -0
  84. package/dist/host-runtime/codex.js +215 -0
  85. package/dist/host-runtime/codex.js.map +1 -0
  86. package/dist/host-runtime/config.d.ts +9 -0
  87. package/dist/host-runtime/config.d.ts.map +1 -0
  88. package/dist/host-runtime/config.js +91 -0
  89. package/dist/host-runtime/config.js.map +1 -0
  90. package/dist/host-runtime/router-client.d.ts +38 -0
  91. package/dist/host-runtime/router-client.d.ts.map +1 -0
  92. package/dist/host-runtime/router-client.js +463 -0
  93. package/dist/host-runtime/router-client.js.map +1 -0
  94. package/dist/host-runtime/router.d.ts +3 -0
  95. package/dist/host-runtime/router.d.ts.map +1 -0
  96. package/dist/host-runtime/router.js +43 -0
  97. package/dist/host-runtime/router.js.map +1 -0
  98. package/dist/mcp/server.js +2 -2
  99. package/dist/mcp/server.js.map +1 -1
  100. package/dist/skills-manifest.json +22 -17
  101. package/dist/storage/conflicts.d.ts.map +1 -1
  102. package/dist/storage/conflicts.js +8 -1
  103. package/dist/storage/conflicts.js.map +1 -1
  104. package/dist/storage/schema.d.ts +1 -1
  105. package/dist/storage/schema.d.ts.map +1 -1
  106. package/dist/storage/schema.js +255 -0
  107. package/dist/storage/schema.js.map +1 -1
  108. package/dist/transports/agent-messaging.d.ts +11 -0
  109. package/dist/transports/agent-messaging.d.ts.map +1 -0
  110. package/dist/transports/agent-messaging.js +273 -0
  111. package/dist/transports/agent-messaging.js.map +1 -0
  112. package/dist/transports/cli/cli.d.ts +6 -1
  113. package/dist/transports/cli/cli.d.ts.map +1 -1
  114. package/dist/transports/cli/cli.js +392 -17
  115. package/dist/transports/cli/cli.js.map +1 -1
  116. package/dist/transports/http/server.d.ts.map +1 -1
  117. package/dist/transports/http/server.js +31 -2
  118. package/dist/transports/http/server.js.map +1 -1
  119. package/dist/transports/mcp/handlers.d.ts +152 -1
  120. package/dist/transports/mcp/handlers.d.ts.map +1 -1
  121. package/dist/transports/mcp/handlers.js +108 -3
  122. package/dist/transports/mcp/handlers.js.map +1 -1
  123. package/dist/transports/schemas.d.ts +106 -1
  124. package/dist/transports/schemas.d.ts.map +1 -1
  125. package/dist/transports/schemas.js +86 -0
  126. package/dist/transports/schemas.js.map +1 -1
  127. package/docs/platforms/README.md +167 -0
  128. package/docs/platforms/agent-messaging.md +290 -0
  129. package/hooks/hooks.json +11 -0
  130. package/llms-install.md +101 -25
  131. package/package.json +14 -5
  132. package/scripts/hooks/_generated/repo-state.js +77 -0
  133. package/scripts/hooks/_generated/schema.js +255 -0
  134. package/scripts/hooks/_generated/task-state.js +3 -3
  135. package/scripts/hooks/_generated/work-topology.js +1 -0
  136. package/scripts/hooks/_shared.js +77 -97
  137. package/scripts/hooks/auto-update-runner.mjs +243 -0
  138. package/scripts/hooks/session-start.js +12 -2
  139. package/scripts/hooks/session-summary.js +7 -6
  140. package/skills/memesh/SKILL.md +40 -1
@@ -69,6 +69,235 @@ CREATE TABLE IF NOT EXISTS memesh_metadata (
69
69
  value TEXT NOT NULL
70
70
  );
71
71
 
72
+ -- Local agent messaging core. Messages are immutable, deliveries identify the
73
+ -- authorized recipient, events are the wakeup/catch-up surface, cursors keep
74
+ -- the internal sequence opaque, and receipts are append-only facts that remain
75
+ -- separate from delivery existence.
76
+ CREATE TABLE IF NOT EXISTS agent_messages (
77
+ message_id TEXT PRIMARY KEY,
78
+ project TEXT NOT NULL,
79
+ sender TEXT NOT NULL,
80
+ sender_host TEXT,
81
+ recipient TEXT NOT NULL,
82
+ content_type TEXT NOT NULL,
83
+ correlation_id TEXT,
84
+ reply_to_message_id TEXT,
85
+ privacy TEXT NOT NULL,
86
+ payload_json TEXT NOT NULL,
87
+ -- A tombstone replaces only a terminal payload. The original JSON bytes
88
+ -- are never reconstructed from these fields; they retain enough identity to
89
+ -- audit the erasure while the routing and lifecycle tables remain intact.
90
+ payload_sha256 TEXT,
91
+ payload_original_bytes INTEGER,
92
+ payload_tombstoned_at TIMESTAMP,
93
+ provenance_json TEXT NOT NULL,
94
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS agent_message_deliveries (
98
+ delivery_id TEXT PRIMARY KEY,
99
+ message_id TEXT NOT NULL,
100
+ project TEXT NOT NULL,
101
+ recipient TEXT NOT NULL,
102
+ target_kind TEXT NOT NULL DEFAULT 'principal' CHECK (target_kind IN ('principal', 'session')),
103
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
104
+ FOREIGN KEY (message_id) REFERENCES agent_messages(message_id) ON DELETE CASCADE,
105
+ UNIQUE(message_id, project, recipient)
106
+ );
107
+
108
+ CREATE TABLE IF NOT EXISTS agent_message_events (
109
+ event_sequence INTEGER PRIMARY KEY AUTOINCREMENT,
110
+ event_id TEXT NOT NULL UNIQUE,
111
+ message_id TEXT NOT NULL,
112
+ delivery_id TEXT NOT NULL,
113
+ project TEXT NOT NULL,
114
+ recipient TEXT NOT NULL,
115
+ event_kind TEXT NOT NULL,
116
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
117
+ FOREIGN KEY (message_id) REFERENCES agent_messages(message_id) ON DELETE CASCADE,
118
+ FOREIGN KEY (delivery_id) REFERENCES agent_message_deliveries(delivery_id) ON DELETE CASCADE
119
+ );
120
+
121
+ CREATE TABLE IF NOT EXISTS agent_message_idempotency (
122
+ project TEXT NOT NULL,
123
+ sender TEXT NOT NULL,
124
+ idempotency_key TEXT NOT NULL,
125
+ request_hash TEXT NOT NULL,
126
+ message_id TEXT NOT NULL UNIQUE,
127
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
128
+ FOREIGN KEY (message_id) REFERENCES agent_messages(message_id) ON DELETE CASCADE,
129
+ PRIMARY KEY(project, sender, idempotency_key)
130
+ );
131
+
132
+ CREATE TABLE IF NOT EXISTS agent_message_cursors (
133
+ cursor_token TEXT PRIMARY KEY,
134
+ project TEXT NOT NULL,
135
+ recipient TEXT NOT NULL,
136
+ event_sequence INTEGER NOT NULL,
137
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
138
+ );
139
+
140
+ CREATE TABLE IF NOT EXISTS agent_message_receipts (
141
+ receipt_id TEXT PRIMARY KEY,
142
+ message_id TEXT NOT NULL,
143
+ project TEXT NOT NULL,
144
+ recipient TEXT NOT NULL,
145
+ receipt_kind TEXT NOT NULL,
146
+ actor TEXT NOT NULL,
147
+ idempotency_key TEXT NOT NULL,
148
+ request_hash TEXT NOT NULL,
149
+ detail_json TEXT NOT NULL,
150
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
151
+ FOREIGN KEY (message_id) REFERENCES agent_messages(message_id) ON DELETE CASCADE,
152
+ UNIQUE(project, recipient, message_id, receipt_kind, idempotency_key)
153
+ );
154
+
155
+ -- Host-native push identity and lifecycle. A principal is stable, a session
156
+ -- instance is ephemeral, and every connection to that session gets a strictly
157
+ -- increasing generation. The activation checkpoint is intentionally created
158
+ -- at first principal registration: principal-targeted history at or before it
159
+ -- is durable inbox history, but is never replayed as a first-time host push.
160
+ CREATE TABLE IF NOT EXISTS agent_principals (
161
+ project TEXT NOT NULL,
162
+ principal_id TEXT NOT NULL,
163
+ activation_event_sequence INTEGER NOT NULL,
164
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
165
+ PRIMARY KEY(project, principal_id)
166
+ );
167
+
168
+ CREATE TABLE IF NOT EXISTS agent_session_instances (
169
+ project TEXT NOT NULL,
170
+ session_instance_id TEXT NOT NULL,
171
+ principal_id TEXT NOT NULL,
172
+ adapter_kind TEXT NOT NULL,
173
+ last_generation INTEGER NOT NULL DEFAULT 0,
174
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
175
+ PRIMARY KEY(project, session_instance_id),
176
+ FOREIGN KEY (project, principal_id) REFERENCES agent_principals(project, principal_id)
177
+ );
178
+
179
+ CREATE TABLE IF NOT EXISTS agent_session_connections (
180
+ connection_id TEXT PRIMARY KEY,
181
+ project TEXT NOT NULL,
182
+ principal_id TEXT NOT NULL,
183
+ session_instance_id TEXT NOT NULL,
184
+ generation INTEGER NOT NULL,
185
+ adapter_kind TEXT NOT NULL,
186
+ router_instance_id TEXT NOT NULL,
187
+ lease_expires_at_ms INTEGER NOT NULL,
188
+ connected_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
189
+ disconnected_at TIMESTAMP,
190
+ disconnect_reason TEXT,
191
+ UNIQUE(project, session_instance_id, generation),
192
+ FOREIGN KEY (project, session_instance_id)
193
+ REFERENCES agent_session_instances(project, session_instance_id)
194
+ );
195
+
196
+ CREATE TABLE IF NOT EXISTS agent_presence_facts (
197
+ presence_fact_id TEXT PRIMARY KEY,
198
+ project TEXT NOT NULL,
199
+ principal_id TEXT NOT NULL,
200
+ session_instance_id TEXT NOT NULL,
201
+ connection_id TEXT NOT NULL,
202
+ generation INTEGER NOT NULL,
203
+ presence_kind TEXT NOT NULL CHECK (presence_kind IN ('connected', 'heartbeat', 'disconnected', 'superseded')),
204
+ detail_json TEXT NOT NULL,
205
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
206
+ FOREIGN KEY (connection_id) REFERENCES agent_session_connections(connection_id)
207
+ );
208
+
209
+ -- Dispatch is at-least-once across a crash boundary. delivery_id is the stable
210
+ -- adapter dispatch key; attempt_id identifies one invocation. A host_accept is
211
+ -- a separate fact and never implies an agent acknowledgement or workflow
212
+ -- outcome.
213
+ CREATE TABLE IF NOT EXISTS agent_dispatch_attempts (
214
+ attempt_id TEXT PRIMARY KEY,
215
+ delivery_id TEXT NOT NULL,
216
+ project TEXT NOT NULL,
217
+ principal_id TEXT NOT NULL,
218
+ session_instance_id TEXT NOT NULL,
219
+ connection_id TEXT NOT NULL,
220
+ generation INTEGER NOT NULL,
221
+ router_instance_id TEXT NOT NULL,
222
+ attempt_number INTEGER NOT NULL,
223
+ result TEXT NOT NULL DEFAULT 'started' CHECK (result IN ('started', 'adapter_returned', 'adapter_rejected', 'adapter_failed', 'stale_generation')),
224
+ failure_code TEXT,
225
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
226
+ completed_at TIMESTAMP,
227
+ UNIQUE(delivery_id, attempt_number),
228
+ FOREIGN KEY (delivery_id) REFERENCES agent_message_deliveries(delivery_id) ON DELETE CASCADE,
229
+ FOREIGN KEY (connection_id) REFERENCES agent_session_connections(connection_id)
230
+ );
231
+
232
+ CREATE TABLE IF NOT EXISTS agent_host_accepts (
233
+ host_accept_id TEXT PRIMARY KEY,
234
+ attempt_id TEXT NOT NULL UNIQUE,
235
+ delivery_id TEXT NOT NULL UNIQUE,
236
+ adapter_kind TEXT NOT NULL,
237
+ receipt_json TEXT NOT NULL,
238
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
239
+ FOREIGN KEY (attempt_id) REFERENCES agent_dispatch_attempts(attempt_id),
240
+ FOREIGN KEY (delivery_id) REFERENCES agent_message_deliveries(delivery_id) ON DELETE CASCADE
241
+ );
242
+
243
+ CREATE TABLE IF NOT EXISTS agent_ack_facts (
244
+ ack_fact_id TEXT PRIMARY KEY,
245
+ delivery_id TEXT NOT NULL,
246
+ host_accept_id TEXT NOT NULL,
247
+ actor TEXT NOT NULL,
248
+ idempotency_key TEXT NOT NULL,
249
+ request_hash TEXT NOT NULL,
250
+ detail_json TEXT NOT NULL,
251
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
252
+ UNIQUE(delivery_id, actor, idempotency_key),
253
+ FOREIGN KEY (delivery_id) REFERENCES agent_message_deliveries(delivery_id) ON DELETE CASCADE,
254
+ FOREIGN KEY (host_accept_id) REFERENCES agent_host_accepts(host_accept_id)
255
+ );
256
+
257
+ CREATE TABLE IF NOT EXISTS agent_workflow_facts (
258
+ workflow_fact_id TEXT PRIMARY KEY,
259
+ delivery_id TEXT NOT NULL,
260
+ actor TEXT NOT NULL,
261
+ workflow_state TEXT NOT NULL,
262
+ idempotency_key TEXT NOT NULL,
263
+ request_hash TEXT NOT NULL,
264
+ detail_json TEXT NOT NULL,
265
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
266
+ UNIQUE(delivery_id, actor, idempotency_key),
267
+ FOREIGN KEY (delivery_id) REFERENCES agent_message_deliveries(delivery_id) ON DELETE CASCADE
268
+ );
269
+
270
+ CREATE TABLE IF NOT EXISTS agent_retention_facts (
271
+ retention_fact_id TEXT PRIMARY KEY,
272
+ message_id TEXT NOT NULL,
273
+ actor TEXT NOT NULL,
274
+ retention_state TEXT NOT NULL,
275
+ idempotency_key TEXT NOT NULL,
276
+ request_hash TEXT NOT NULL,
277
+ detail_json TEXT NOT NULL,
278
+ created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
279
+ UNIQUE(message_id, actor, idempotency_key),
280
+ FOREIGN KEY (message_id) REFERENCES agent_messages(message_id) ON DELETE CASCADE
281
+ );
282
+
283
+ CREATE INDEX IF NOT EXISTS idx_agent_message_events_recipient_sequence
284
+ ON agent_message_events(project, recipient, event_sequence);
285
+ CREATE INDEX IF NOT EXISTS idx_agent_message_deliveries_scope
286
+ ON agent_message_deliveries(project, recipient, message_id);
287
+ CREATE INDEX IF NOT EXISTS idx_agent_message_receipts_scope
288
+ ON agent_message_receipts(project, recipient, message_id, created_at);
289
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_message_cursors_unique_scope_sequence
290
+ ON agent_message_cursors(project, recipient, event_sequence);
291
+ CREATE INDEX IF NOT EXISTS idx_agent_principals_activation
292
+ ON agent_principals(project, activation_event_sequence);
293
+ CREATE INDEX IF NOT EXISTS idx_agent_session_connections_active
294
+ ON agent_session_connections(project, principal_id, session_instance_id, lease_expires_at_ms)
295
+ WHERE disconnected_at IS NULL;
296
+ CREATE INDEX IF NOT EXISTS idx_agent_dispatch_attempts_delivery
297
+ ON agent_dispatch_attempts(delivery_id, attempt_number);
298
+ CREATE INDEX IF NOT EXISTS idx_agent_workflow_facts_delivery_created
299
+ ON agent_workflow_facts(delivery_id, created_at, workflow_fact_id);
300
+
72
301
  -- Proof that a capture hook actually RAN. Nothing else in this schema can
73
302
  -- give it: every other signal is "a row was written", and "the hook ran and
74
303
  -- found nothing worth saving" is the healthy case that produces no row at
@@ -148,6 +377,32 @@ export function migrateEntitiesSchema(db) {
148
377
  addColumn('title', "ALTER TABLE entities ADD COLUMN title TEXT");
149
378
  db.exec(`CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status);
150
379
  CREATE INDEX IF NOT EXISTS idx_entities_namespace ON entities(namespace);`);
380
+ const deliveryTableExists = db
381
+ .prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'agent_message_deliveries'")
382
+ .get();
383
+ if (!deliveryTableExists)
384
+ return;
385
+ const deliveryColumns = new Set(db.prepare("PRAGMA table_info(agent_message_deliveries)").all().map((column) => column.name));
386
+ if (!deliveryColumns.has('target_kind')) {
387
+ safeAlter(db, "ALTER TABLE agent_message_deliveries ADD COLUMN target_kind TEXT NOT NULL DEFAULT 'principal' " +
388
+ "CHECK (target_kind IN ('principal', 'session'))");
389
+ }
390
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_message_deliveries_target
391
+ ON agent_message_deliveries(project, target_kind, recipient, message_id);`);
392
+ const messageColumns = new Set(db.prepare("PRAGMA table_info(agent_messages)").all().map((column) => column.name));
393
+ const addMessageColumn = (column, sql) => {
394
+ if (messageColumns.has(column))
395
+ return;
396
+ safeAlter(db, sql);
397
+ messageColumns.add(column);
398
+ };
399
+ addMessageColumn('payload_sha256', 'ALTER TABLE agent_messages ADD COLUMN payload_sha256 TEXT');
400
+ addMessageColumn('payload_original_bytes', 'ALTER TABLE agent_messages ADD COLUMN payload_original_bytes INTEGER');
401
+ addMessageColumn('payload_tombstoned_at', 'ALTER TABLE agent_messages ADD COLUMN payload_tombstoned_at TIMESTAMP');
402
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_messages_tombstone
403
+ ON agent_messages(payload_tombstoned_at, created_at);
404
+ CREATE INDEX IF NOT EXISTS idx_agent_workflow_facts_delivery_created
405
+ ON agent_workflow_facts(delivery_id, created_at, workflow_fact_id);`);
151
406
  }
152
407
  export function ensureTagsUniqueIndex(db) {
153
408
  try {
@@ -72,7 +72,7 @@ const FIELD_LABELS = {
72
72
  goal: 'Goal',
73
73
  next: 'Next',
74
74
  blocked: 'Blocked',
75
- done: 'Just finished',
75
+ done: 'Had just finished',
76
76
  };
77
77
  function ageInDays(updatedAt, now) {
78
78
  if (!updatedAt)
@@ -87,8 +87,8 @@ export function taskStateLines(state, project, now = new Date()) {
87
87
  if (isEmptyTaskState(state))
88
88
  return [];
89
89
  const days = ageInDays(state.updated_at, now);
90
- const age = days === null ? '' : days === 0 ? ' (today)' : days === 1 ? ' (yesterday)' : ` (${days} days ago)`;
91
- const lines = [`Where "${project}" was left off${age}:`];
90
+ const age = days === null ? 'at some point' : days === 0 ? 'today' : days === 1 ? 'yesterday' : `${days} days ago`;
91
+ const lines = [`Stated about "${project}" ${age}, and not revisited since:`];
92
92
  for (const field of TASK_STATE_FIELDS) {
93
93
  const value = state[field];
94
94
  if (value)
@@ -13,6 +13,7 @@ export const WORK_LAYER_TYPES = new Set([
13
13
  'milestone',
14
14
  'pattern',
15
15
  'technical_pattern',
16
+ 'product_improvement',
16
17
  'goal',
17
18
  'plan',
18
19
  'task-state',
@@ -1,6 +1,5 @@
1
1
  import { appendFileSync, chmodSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { spawn } from 'child_process';
3
- import { createRequire } from 'module';
4
3
  import { MemeshDatabase } from './_generated/sqlite.js';
5
4
  import { dirname, join } from 'path';
6
5
  import { fileURLToPath, pathToFileURL } from 'url';
@@ -36,6 +35,7 @@ import {
36
35
  } from './_generated/core-paths.js';
37
36
  import { autoCaptureDecision } from './_generated/capture-flag.js';
38
37
  export { assembleTopologyBlock, buildReferenceContext, extractCitedMemoryIds, DEFAULT_TOPOLOGY_BUDGET, SNIPPET_FETCH_CHARS, TOPOLOGY_CANDIDATE_CAP } from './_generated/work-topology.js';
38
+ export { readRepoState, repoStateLines } from './_generated/repo-state.js';
39
39
  export { matchingGuards, guardFromMetadata } from './_generated/guards.js';
40
40
  export { writeCitationRule, citationRulePath, CITATION_RULE_BODY } from './_generated/citation-rule.js';
41
41
  import { guardFromMetadata as guardFromMetadataLocal } from './_generated/guards.js';
@@ -128,8 +128,6 @@ import {
128
128
 
129
129
  export { homeDir, memeshDir, getDbPath, getMemeshDirFromDbPath, getProjectName, redactSecrets, slugFromRemoteUrl };
130
130
 
131
- const require = createRequire(import.meta.url);
132
-
133
131
  /**
134
132
  * Resolve the package root from a hook file's `import.meta.url`.
135
133
  *
@@ -285,9 +283,13 @@ const VALID_AUTO_UPDATE_POLICIES = new Set(['off', 'patch', 'minor', 'major']);
285
283
  */
286
284
  export function resolveAutoUpdatePolicy(env = process.env) {
287
285
  const envVal = env.MEMESH_AUTO_UPDATE;
288
- if (typeof envVal === 'string') {
286
+ if (envVal !== undefined) {
287
+ if (typeof envVal !== 'string') return 'off';
289
288
  const lowered = envVal.toLowerCase();
290
289
  if (VALID_AUTO_UPDATE_POLICIES.has(lowered)) return lowered;
290
+ // An explicit but malformed higher-priority value must not uncover a
291
+ // permissive config value and authorise an unattended global mutation.
292
+ return 'off';
291
293
  }
292
294
  const cfg = readHookConfig(env);
293
295
  if (typeof cfg.autoUpdate === 'string') {
@@ -827,84 +829,28 @@ export function decideAutoUpdateHook(currentVersion, cache, policy) {
827
829
 
828
830
  export function logAutoUpdate(line) {
829
831
  try {
830
- const dir = getMemeshDirFromDbPath();
832
+ const dir = memeshDir();
831
833
  ensurePrivateDir(dir);
832
834
  const path = join(dir, 'auto-update.log');
833
835
  appendFileSync(path, `[${new Date().toISOString()}] ${line}\n`);
834
836
  try { chmodSync(path, 0o600); } catch { /* non-POSIX */ }
837
+ return true;
835
838
  } catch {
836
- // Logging is best-effort.
837
- }
838
- }
839
-
840
- export const AUTO_UPDATE_LOCK_TTL_MS = 10 * 60 * 1000;
841
-
842
- export function tryAcquireAutoUpdateLock(version) {
843
- try {
844
- const dir = memeshDir();
845
- ensurePrivateDir(dir);
846
- const lockPath = join(dir, 'auto-update.lock');
847
- const fs = require('fs');
848
- const myToken = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
849
- const payload = `${myToken}\n${process.pid}\n${Date.now()}\n${version}\n`;
850
- try {
851
- // O_CREAT|O_EXCL is the POSIX atomic create primitive — only one process wins.
852
- const O_FLAGS = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL;
853
- const fd = fs.openSync(lockPath, O_FLAGS, 0o600);
854
- try { fs.writeFileSync(fd, payload); } finally { fs.closeSync(fd); }
855
- return { acquired: true, lockPath };
856
- } catch (err) {
857
- if (err?.code !== 'EEXIST') throw err;
858
- }
859
- let stat;
860
- try { stat = fs.statSync(lockPath); } catch { return { acquired: false, lockPath }; }
861
- if (Date.now() - stat.mtimeMs <= AUTO_UPDATE_LOCK_TTL_MS) {
862
- return { acquired: false, lockPath };
863
- }
864
- const tempPath = `${lockPath}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
865
- try {
866
- fs.writeFileSync(tempPath, payload, { mode: 0o600 });
867
- try { fs.unlinkSync(lockPath); } catch (e2) {
868
- if (e2?.code !== 'ENOENT') {
869
- try { fs.unlinkSync(tempPath); } catch { /* best-effort */ }
870
- return { acquired: false, lockPath };
871
- }
872
- }
873
- fs.renameSync(tempPath, lockPath);
874
- } catch {
875
- try { fs.unlinkSync(tempPath); } catch { /* best-effort */ }
876
- return { acquired: false, lockPath };
877
- }
878
- // The "race" here is the lock primitive itself: we just renamed our
879
- // temp file onto lockPath, then immediately read back to see whether
880
- // our token won. If a concurrent caller's rename landed in the
881
- // window between our rename and our read, their token is now there
882
- // and we correctly conclude we did NOT acquire the lock. That race
883
- // outcome IS the contract — last-writer-wins lock-file pattern, not
884
- // a TOCTOU defect. (CodeQL js/file-system-race #82 is dismissed on
885
- // the security dashboard with this rationale; CodeQL does not honor
886
- // inline suppression comments, so dismissal is the correct route.)
887
- let recorded;
888
- try { recorded = fs.readFileSync(lockPath, 'utf8'); } catch { return { acquired: false, lockPath }; }
889
- return recorded.split('\n')[0] === myToken
890
- ? { acquired: true, lockPath }
891
- : { acquired: false, lockPath };
892
- } catch {
893
- return { acquired: false, lockPath: null };
839
+ return false;
894
840
  }
895
841
  }
896
842
 
897
843
  /**
898
- * Spawn `npm install -g @pcircle/memesh@<version>` detached so the upgrade
899
- * can finish after this hook returns. Never blocks the session.
844
+ * Dispatch the detached updater runner after proving that this is an
845
+ * npm-global install and that its durable outcome log is writable. The runner
846
+ * owns the single-flight lock and emits the only terminal SUCCESS/FAILED line.
900
847
  *
901
848
  * @param {string} version - Target version to install
902
- * @param {boolean} deprecationOverride - Whether this is a security-override install
903
849
  * @param {object|null} installChannelMod - Preloaded dist/core/install-channel.js module
904
- * @returns {{ state: 'spawned'|'in-progress'|'channel'|'failed' }}
850
+ * @returns {Promise<{ state: 'dispatched'|'channel'|'failed' }>}
905
851
  */
906
- export function spawnAutoUpdate(version, deprecationOverride, installChannelMod) {
907
- let lock = null;
852
+ export async function spawnAutoUpdate(version, installChannelMod) {
853
+ let fd = -1;
908
854
  try {
909
855
  const pluginRoot = resolvePluginRoot(import.meta.url);
910
856
  let channel = 'unknown';
@@ -914,42 +860,76 @@ export function spawnAutoUpdate(version, deprecationOverride, installChannelMod)
914
860
  } catch { /* best-effort */ }
915
861
  }
916
862
  if (channel !== 'npm-global') {
917
- logAutoUpdate(
918
- `auto-update SKIPPED: install channel '${channel}' does not support self-update via npm install -g`
919
- );
863
+ const line = `auto-update SKIPPED: install channel '${channel}' does not support self-update via npm install -g`;
864
+ if (!logAutoUpdate(line)) {
865
+ try { process.stderr.write(`[memesh] ${line}\n`); } catch { /* stderr unavailable */ }
866
+ }
920
867
  return { state: 'channel' };
921
868
  }
922
- lock = tryAcquireAutoUpdateLock(version);
923
- if (!lock.acquired) {
924
- logAutoUpdate(
925
- `auto-update SKIPPED: another session already holds ${lock.lockPath ?? 'auto-update.lock'} for this upgrade`
926
- );
927
- return { state: 'in-progress' };
928
- }
929
- const dir = getMemeshDirFromDbPath();
869
+
870
+ const dir = memeshDir();
930
871
  ensurePrivateDir(dir);
931
872
  const logPath = join(dir, 'auto-update.log');
932
- let fd = -1;
933
- try { fd = openSync(logPath, 'a', 0o600); } catch { fd = -1; }
934
- const stdio = fd >= 0 ? ['ignore', fd, fd] : 'ignore';
873
+ const lockPath = join(dir, 'auto-update.lock');
874
+ const runnerPath = join(pluginRoot, 'scripts', 'hooks', 'auto-update-runner.mjs');
875
+ if (!existsSync(runnerPath)) {
876
+ const line = `auto-update FAILED: runner missing at ${runnerPath}`;
877
+ if (!logAutoUpdate(line)) {
878
+ try { process.stderr.write(`[memesh] ${line}\n`); } catch { /* stderr unavailable */ }
879
+ }
880
+ return { state: 'failed' };
881
+ }
882
+
883
+ try {
884
+ fd = openSync(logPath, 'a', 0o600);
885
+ try { chmodSync(logPath, 0o600); } catch { /* non-POSIX */ }
886
+ } catch (err) {
887
+ try {
888
+ process.stderr.write(
889
+ `[memesh] auto-update FAILED: durable log unavailable at ${logPath} (${err?.message ?? err})\n`,
890
+ );
891
+ } catch { /* stderr unavailable */ }
892
+ return { state: 'failed' };
893
+ }
894
+
935
895
  const child = spawn(
936
- 'npm',
937
- ['install', '-g', `@pcircle/memesh@${version}`],
938
- // windowsHide avoids a flashing console window on Windows; harmless on POSIX.
939
- { detached: true, stdio, env: process.env, windowsHide: true },
896
+ process.execPath,
897
+ [runnerPath, version, lockPath],
898
+ { detached: true, stdio: ['ignore', fd, fd], env: process.env, windowsHide: true },
940
899
  );
941
- child.unref();
900
+
901
+ return await new Promise((resolve) => {
902
+ let settled = false;
903
+ const finish = (state, line) => {
904
+ if (settled) return;
905
+ settled = true;
906
+ if (fd >= 0) {
907
+ try { closeSync(fd); } catch { /* already closed */ }
908
+ fd = -1;
909
+ }
910
+ if (!logAutoUpdate(line)) {
911
+ try { process.stderr.write(`[memesh] ${line}\n`); } catch { /* stderr unavailable */ }
912
+ }
913
+ resolve({ state });
914
+ };
915
+ child.once('spawn', () => {
916
+ child.unref();
917
+ finish(
918
+ 'dispatched',
919
+ `auto-update DISPATCHED: target=${version} worker_pid=${child.pid ?? 'unknown'} lock=${lockPath}`,
920
+ );
921
+ });
922
+ child.once('error', (err) => {
923
+ finish('failed', `auto-update FAILED: target=${version} stage=dispatch error=${err?.message ?? err}`);
924
+ });
925
+ });
926
+ } catch (err) {
942
927
  if (fd >= 0) {
943
- try { closeSync(fd); } catch { /* ignore */ }
928
+ try { closeSync(fd); } catch { /* already closed */ }
944
929
  }
945
- logAutoUpdate(
946
- `auto-update spawn: target=${version}${deprecationOverride ? ' (deprecation-override)' : ''} pid=${child.pid ?? 'unknown'} lock=${lock.lockPath}`
947
- );
948
- return { state: 'spawned' };
949
- } catch (err) {
950
- logAutoUpdate(`auto-update spawn FAILED: ${err?.message ?? err}`);
951
- if (lock?.acquired && lock.lockPath) {
952
- try { require('fs').unlinkSync(lock.lockPath); } catch { /* best-effort */ }
930
+ const line = `auto-update FAILED: target=${version} stage=dispatch error=${err?.message ?? err}`;
931
+ if (!logAutoUpdate(line)) {
932
+ try { process.stderr.write(`[memesh] ${line}\n`); } catch { /* stderr unavailable */ }
953
933
  }
954
934
  return { state: 'failed' };
955
935
  }