@memtensor/memos-local-openclaw-plugin 1.0.8-beta.3 → 1.0.8-beta.5

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/index.ts CHANGED
@@ -2339,48 +2339,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2339
2339
 
2340
2340
  // ─── Service lifecycle ───
2341
2341
 
2342
- api.registerService({
2343
- id: "memos-local-openclaw-plugin",
2344
- start: async () => {
2345
- if (hubServer) {
2346
- const hubUrl = await hubServer.start();
2347
- api.logger.info(`memos-local: hub started at ${hubUrl}`);
2348
- }
2342
+ let serviceStarted = false;
2349
2343
 
2350
- // Auto-connect to Hub in client mode (handles both existing token and auto-join via teamToken)
2351
- if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
2352
- try {
2353
- const session = await connectToHub(store, ctx.config, ctx.log);
2354
- api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
2355
- } catch (err) {
2356
- api.logger.warn(`memos-local: Hub connection failed: ${err}`);
2357
- }
2358
- }
2344
+ const startServiceCore = async () => {
2345
+ if (serviceStarted) return;
2346
+ serviceStarted = true;
2359
2347
 
2348
+ if (hubServer) {
2349
+ const hubUrl = await hubServer.start();
2350
+ api.logger.info(`memos-local: hub started at ${hubUrl}`);
2351
+ }
2352
+
2353
+ if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
2360
2354
  try {
2361
- const viewerUrl = await viewer.start();
2362
- api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2363
- api.logger.info(`╔══════════════════════════════════════════╗`);
2364
- api.logger.info(`║ MemOS Memory Viewer ║`);
2365
- api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
2366
- api.logger.info(`║ Open in browser to manage memories ║`);
2367
- api.logger.info(`╚══════════════════════════════════════════╝`);
2368
- api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
2369
- api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
2370
- skillEvolver.recoverOrphanedTasks().then((count) => {
2371
- if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
2372
- }).catch((err) => {
2373
- api.logger.warn(`memos-local: skill recovery failed: ${err}`);
2374
- });
2355
+ const session = await connectToHub(store, ctx.config, ctx.log);
2356
+ api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
2375
2357
  } catch (err) {
2376
- api.logger.warn(`memos-local: viewer failed to start: ${err}`);
2377
- api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2358
+ api.logger.warn(`memos-local: Hub connection failed: ${err}`);
2378
2359
  }
2379
- telemetry.trackPluginStarted(
2380
- ctx.config.embedding?.provider ?? "local",
2381
- ctx.config.summarizer?.provider ?? "none",
2382
- );
2383
- },
2360
+ }
2361
+
2362
+ try {
2363
+ const viewerUrl = await viewer.start();
2364
+ api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2365
+ api.logger.info(`╔══════════════════════════════════════════╗`);
2366
+ api.logger.info(`║ MemOS Memory Viewer ║`);
2367
+ api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
2368
+ api.logger.info(`║ Open in browser to manage memories ║`);
2369
+ api.logger.info(`╚══════════════════════════════════════════╝`);
2370
+ api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
2371
+ api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
2372
+ skillEvolver.recoverOrphanedTasks().then((count) => {
2373
+ if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
2374
+ }).catch((err) => {
2375
+ api.logger.warn(`memos-local: skill recovery failed: ${err}`);
2376
+ });
2377
+ } catch (err) {
2378
+ api.logger.warn(`memos-local: viewer failed to start: ${err}`);
2379
+ api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
2380
+ }
2381
+ telemetry.trackPluginStarted(
2382
+ ctx.config.embedding?.provider ?? "local",
2383
+ ctx.config.summarizer?.provider ?? "none",
2384
+ );
2385
+ };
2386
+
2387
+ api.registerService({
2388
+ id: "memos-local-openclaw-plugin",
2389
+ start: async () => { await startServiceCore(); },
2384
2390
  stop: async () => {
2385
2391
  await worker.flush();
2386
2392
  await telemetry.shutdown();
@@ -2390,6 +2396,19 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2390
2396
  api.logger.info("memos-local: stopped");
2391
2397
  },
2392
2398
  });
2399
+
2400
+ // Fallback: OpenClaw may load this plugin via deferred reload after
2401
+ // startPluginServices has already run, so service.start() never fires.
2402
+ // Self-start the viewer after a grace period if it hasn't been started.
2403
+ const SELF_START_DELAY_MS = 3000;
2404
+ setTimeout(() => {
2405
+ if (!serviceStarted) {
2406
+ api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
2407
+ startServiceCore().catch((err) => {
2408
+ api.logger.warn(`memos-local: self-start failed: ${err}`);
2409
+ });
2410
+ }
2411
+ }, SELF_START_DELAY_MS);
2393
2412
  },
2394
2413
  };
2395
2414
 
@@ -3,7 +3,7 @@
3
3
  "name": "MemOS Local Memory",
4
4
  "description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
5
5
  "kind": "memory",
6
- "version": "1.0.8-beta.2",
6
+ "version": "1.0.8-beta.5",
7
7
  "skills": [
8
8
  "skill/memos-memory-guide"
9
9
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.8-beta.3",
3
+ "version": "1.0.8-beta.5",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/src/hub/server.ts CHANGED
@@ -714,9 +714,14 @@ export class HubServer {
714
714
 
715
715
  // Track which IDs are memories vs chunks
716
716
  const memoryIdSet = new Set(memFtsHits.map(({ hit }) => hit.id));
717
+ const ftsHitIdSet = new Set<string>();
718
+ for (const { hit } of ftsHits) ftsHitIdSet.add(hit.id);
719
+ for (const { hit } of memFtsHits) ftsHitIdSet.add(hit.id);
717
720
 
718
721
  // Two-stage retrieval: FTS candidates first, then embed + cosine rerank
719
722
  let mergedIds: string[];
723
+ /** Vector RRF channel: require min cosine similarity unless id is already an FTS hit. */
724
+ const MIN_VECTOR_SIM = 0.45;
720
725
  if (this.opts.embedder) {
721
726
  try {
722
727
  const [queryVec] = await this.opts.embedder.embed([query]);
@@ -739,8 +744,9 @@ export class HubServer {
739
744
  memoryIdSet.add(e.memoryId);
740
745
  }
741
746
 
742
- scored.sort((a, b) => b.score - a.score);
743
- const topScored = scored.slice(0, maxResults * 2);
747
+ const vecCandidates = scored.filter((s) => s.score >= MIN_VECTOR_SIM || ftsHitIdSet.has(s.id));
748
+ vecCandidates.sort((a, b) => b.score - a.score);
749
+ const topScored = vecCandidates.slice(0, maxResults * 2);
744
750
 
745
751
  const K = 60;
746
752
  const rrfScores = new Map<string, number>();
@@ -2724,6 +2724,11 @@ export class SqliteStore {
2724
2724
  this.db.prepare("UPDATE local_shared_tasks SET hub_task_id = '', hub_instance_id = '', visibility = 'public', group_id = NULL, synced_chunks = 0 WHERE task_id = ?").run(taskId);
2725
2725
  }
2726
2726
 
2727
+ /** Client UI: remove team_shared_chunks rows for all chunks linked to this task (list badge chunk fallback). */
2728
+ clearTeamSharedChunksForTask(taskId: string): void {
2729
+ this.db.prepare("DELETE FROM team_shared_chunks WHERE chunk_id IN (SELECT id FROM chunks WHERE task_id = ?)").run(taskId);
2730
+ }
2731
+
2727
2732
  clearAllTeamSharingState(): void {
2728
2733
  this.clearTeamSharedChunks();
2729
2734
  this.clearTeamSharedSkills();
@@ -44,6 +44,7 @@ html{overflow-y:scroll}
44
44
  [data-theme="light"] .auth-card{box-shadow:0 25px 50px -12px rgba(0,0,0,.08)}
45
45
  [data-theme="light"] .topbar{background:rgba(255,255,255,.92);border-bottom-color:var(--border);backdrop-filter:blur(8px)}
46
46
  [data-theme="light"] .session-item .count,[data-theme="light"] .session-tag{background:rgba(0,0,0,.05)}
47
+ [data-theme="light"] .owner-tag{background:rgba(99,102,241,.08);border-color:rgba(99,102,241,.18)}
47
48
  [data-theme="light"] .card-content pre{background:#f3f4f6;border-color:var(--border)}
48
49
  [data-theme="light"] .vscore-badge{background:rgba(79,70,229,.06);color:#4f46e5}
49
50
  [data-theme="light"] ::-webkit-scrollbar-thumb{background:rgba(0,0,0,.15)}
@@ -389,6 +390,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
389
390
  .role-tag.system{background:var(--amber-bg);color:var(--amber);border:1px solid rgba(245,158,11,.2)}
390
391
  .card-time{font-size:12px;color:var(--text-sec);display:flex;align-items:center;gap:8px}
391
392
  .session-tag{font-size:11px;font-family:ui-monospace,monospace;color:var(--text-muted);background:rgba(0,0,0,.2);padding:3px 8px;border-radius:6px;cursor:default}
393
+ .owner-tag{font-size:11px;font-weight:600;color:var(--pri);background:var(--pri-glow);padding:3px 9px;border-radius:8px;border:1px solid rgba(99,102,241,.15);cursor:default;white-space:nowrap}
392
394
  .card-summary{font-size:15px;font-weight:600;color:var(--text);margin-bottom:10px;line-height:1.5;letter-spacing:-.01em;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
393
395
  .card-content{font-size:13px;color:var(--text-sec);line-height:1.65;max-height:0;overflow:hidden;transition:max-height .3s ease}
394
396
  .card-content.show{max-height:600px;overflow-y:auto}
@@ -1818,7 +1820,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1818
1820
  </div>
1819
1821
  <div class="settings-field">
1820
1822
  <label data-i18n="settings.taskAutoFinalize">Task Auto-Finalize (hours)</label>
1821
- <input type="number" id="cfgTaskAutoFinalizeHours" placeholder="4" min="0" step="1" style="max-width:120px">
1823
+ <input type="number" id="cfgTaskAutoFinalizeHours" placeholder="4" min="0" step="1" style="max-width:120px" onkeydown="if(['-','e','E','+'].includes(event.key))event.preventDefault()" oninput="this.value=this.value.replace(/[^0-9]/g,'')">
1822
1824
  <div class="field-hint" data-i18n="settings.taskAutoFinalize.hint">Active tasks with no new messages beyond this duration will be automatically summarized and completed when the Tasks page is opened. Set to 0 to disable. Default: 4 hours.</div>
1823
1825
  </div>
1824
1826
  </div>
@@ -1901,18 +1903,20 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1901
1903
  </div>
1902
1904
  </div>
1903
1905
 
1904
- <div id="migrateActions" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap">
1906
+ <div id="migrateActions" style="display:flex;gap:10px 16px;align-items:center;flex-wrap:wrap">
1905
1907
  <button class="btn" onclick="migrateScan(true)" id="migrateScanBtn" style="background:var(--bg);border:1px solid var(--border);color:var(--text);font-weight:600;padding:7px 18px;cursor:pointer" data-i18n="migrate.scan">Scan Data Sources</button>
1906
- <button class="btn btn-primary" onclick="migrateStart()" id="migrateStartBtn" style="display:none" data-i18n="migrate.start">Start Import</button>
1907
- <span id="migrateConcurrencyRow" style="display:none;align-items:center;gap:6px">
1908
- <span style="font-size:11px;color:var(--text-muted)" data-i18n="migrate.concurrency.label">Concurrent agents</span>
1909
- <select id="migrateConcurrency" class="filter-select" style="min-width:auto;padding:3px 10px;font-size:11px">
1910
- <option value="1" selected>1</option>
1911
- <option value="2">2</option>
1912
- <option value="4">4</option>
1913
- <option value="8">8</option>
1914
- </select>
1915
- </span>
1908
+ <div id="migrateStartConcurrencyWrap" style="display:none;align-items:center;gap:12px;flex-wrap:wrap;flex-shrink:0">
1909
+ <button class="btn btn-primary" onclick="migrateStart()" id="migrateStartBtn" style="display:inline-flex" data-i18n="migrate.start">Start Import</button>
1910
+ <span id="migrateConcurrencyRow" style="display:flex;align-items:center;gap:8px;flex-shrink:0;min-width:max-content">
1911
+ <span style="font-size:11px;color:var(--text-muted);white-space:nowrap" data-i18n="migrate.concurrency.label">Concurrent agents</span>
1912
+ <select id="migrateConcurrency" class="filter-select" style="min-width:auto;padding:3px 28px 3px 10px;font-size:11px">
1913
+ <option value="1" selected>1</option>
1914
+ <option value="2">2</option>
1915
+ <option value="4">4</option>
1916
+ <option value="8">8</option>
1917
+ </select>
1918
+ </span>
1919
+ </div>
1916
1920
  <span id="migrateStatus" style="font-size:11px;color:var(--text-muted)"></span>
1917
1921
  </div>
1918
1922
  <div id="migrateConcurrencyWarn" style="display:none;margin-top:8px;padding:8px 12px;background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.2);border-radius:8px;font-size:11px;color:#f59e0b;line-height:1.5">
@@ -1945,7 +1949,7 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1945
1949
  <button class="btn btn-sm" id="ppStopBtn" onclick="ppStop()" style="display:none;background:rgba(239,68,68,.12);color:#ef4444;border:1px solid rgba(239,68,68,.3);font-size:12px;padding:5px 16px;font-weight:600" data-i18n="migrate.stop">\u25A0 Stop</button>
1946
1950
  <span style="display:inline-flex;align-items:center;gap:6px">
1947
1951
  <span style="font-size:11px;color:var(--text-muted)" data-i18n="pp.concurrency.label">Concurrent agents</span>
1948
- <select id="ppConcurrency" class="filter-select" style="min-width:auto;padding:3px 10px;font-size:11px">
1952
+ <select id="ppConcurrency" class="filter-select" style="min-width:auto;padding:3px 28px 3px 10px;font-size:11px">
1949
1953
  <option value="1" selected>1</option>
1950
1954
  <option value="2">2</option>
1951
1955
  <option value="4">4</option>
@@ -5352,6 +5356,42 @@ function openHubSkillDetailFromCache(cacheKey,idx){
5352
5356
 
5353
5357
  function escAttr(s){return String(s||'').replace(/&/g,'&amp;').replace(/'/g,'&#39;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
5354
5358
 
5359
+ function fmtAgentName(owner){
5360
+ if(!owner||owner==='public') return '';
5361
+ var s=String(owner);
5362
+ if(s.startsWith('agent:')) s=s.slice(6);
5363
+ return s;
5364
+ }
5365
+
5366
+ function fmtSessionDisplay(sid){
5367
+ if(!sid) return '';
5368
+ if(sid.startsWith('agent:')){
5369
+ var parts=sid.split(':');
5370
+ // agent:{agentId}:import → "📥 import"
5371
+ if(parts.length===3 && parts[2]==='import') return '\\u{1F4E5} import';
5372
+ // agent:{agentId}:session:{sessionId} → shortened sessionId
5373
+ if(parts.length>=4 && parts[2]==='session'){
5374
+ var sessId=parts.slice(3).join(':');
5375
+ return sessId.length>12?sessId.slice(0,6)+'..'+sessId.slice(-4):sessId;
5376
+ }
5377
+ // agent:{agentId}:{other} → show from second part (e.g. "work:main")
5378
+ return parts.slice(1).join(':');
5379
+ }
5380
+ // Legacy formats
5381
+ if(sid.startsWith('openclaw-import-')) return '\\u{1F4E5} '+sid.slice(16);
5382
+ if(sid.startsWith('openclaw-session-')){
5383
+ var id=sid.slice(17);
5384
+ return id.length>12?id.slice(0,6)+'..'+id.slice(-4):id;
5385
+ }
5386
+ if(sid.length>20) return sid.slice(0,8)+'..'+sid.slice(-6);
5387
+ return sid;
5388
+ }
5389
+
5390
+ function isImportedSession(sid){
5391
+ if(!sid) return false;
5392
+ return sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-')||/^agent:[^:]+:(import|session:)/.test(sid);
5393
+ }
5394
+
5355
5395
  /* ─── Unified Sharing Scope Selector ─── */
5356
5396
 
5357
5397
  function getScopeLabel(scope){
@@ -5494,9 +5534,9 @@ function openTaskScopeModal(){
5494
5534
  var isTeamShared=!!(task.sharingVisibility||task.hubTaskId);
5495
5535
  var cs=isTeamShared?'team':isLocalShared?'local':'private';
5496
5536
  openScopeSelectorModal('task',task.id,cs,function(s){
5497
- if(s==='team'){task.sharingVisibility='public';task.hubTaskId=task.hubTaskId||'shared';}
5498
- else if(s==='local'){task.sharingVisibility=null;task.owner='public';}
5499
- else{task.sharingVisibility=null;task.owner=task._origOwner||'agent:main';}
5537
+ if(s==='team'){task.sharingVisibility='public';task.hubTaskId=true;}
5538
+ else if(s==='local'){task.sharingVisibility=null;task.hubTaskId=false;task.owner='public';}
5539
+ else{task.sharingVisibility=null;task.hubTaskId=false;task.owner=task._origOwner||'agent:main';}
5500
5540
  renderTaskShareActions(task);
5501
5541
  updateTaskCardBadge(task.id,s);
5502
5542
  });
@@ -5513,7 +5553,7 @@ async function shareCurrentTask(){
5513
5553
  try{
5514
5554
  const r=await fetch('/api/sharing/tasks/share',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:currentTaskDetail.id,visibility:visibility})});
5515
5555
  const d=await r.json();
5516
- if(d.ok||d.shared){toast(t('toast.taskShared'),'success');currentTaskDetail.sharingVisibility=visibility;renderTaskShareActions(currentTaskDetail);} else {toast(d.error||t('toast.taskShareFail'),'error');}
5556
+ if(d.ok||d.shared){toast(t('toast.taskShared'),'success');currentTaskDetail.sharingVisibility=visibility;currentTaskDetail.hubTaskId=true;renderTaskShareActions(currentTaskDetail);} else {toast(d.error||t('toast.taskShareFail'),'error');}
5517
5557
  }catch(e){toast(t('toast.taskShareFail')+': '+e.message,'error');}
5518
5558
  }
5519
5559
 
@@ -5522,7 +5562,7 @@ async function unshareCurrentTask(){
5522
5562
  try{
5523
5563
  const r=await fetch('/api/sharing/tasks/unshare',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({taskId:currentTaskDetail.id})});
5524
5564
  const d=await r.json();
5525
- if(d.ok||d.unshared){toast(t('toast.taskUnshared'),'success');currentTaskDetail.sharingVisibility=null;renderTaskShareActions(currentTaskDetail);} else {toast(d.error||t('toast.taskUnshareFail'),'error');}
5565
+ if(d.ok||d.unshared){toast(t('toast.taskUnshared'),'success');currentTaskDetail.sharingVisibility=null;currentTaskDetail.hubTaskId=false;renderTaskShareActions(currentTaskDetail);} else {toast(d.error||t('toast.taskUnshareFail'),'error');}
5526
5566
  }catch(e){toast(t('toast.taskUnshareFail')+': '+e.message,'error');}
5527
5567
  }
5528
5568
 
@@ -7414,7 +7454,7 @@ async function saveGeneralConfig(){
7414
7454
  const vp=document.getElementById('cfgViewerPort').value.trim();
7415
7455
  if(vp) cfg.viewerPort=Number(vp);
7416
7456
  const tafh=document.getElementById('cfgTaskAutoFinalizeHours').value.trim();
7417
- cfg.taskAutoFinalizeHours=tafh!==''?Number(tafh):4;
7457
+ cfg.taskAutoFinalizeHours=tafh!==''?Math.max(0,Number(tafh)):4;
7418
7458
  cfg.telemetry={enabled:document.getElementById('cfgTelemetryEnabled').checked};
7419
7459
 
7420
7460
  await doSaveConfig(cfg, saveBtn, 'generalSaved');
@@ -8606,7 +8646,7 @@ function renderMemories(items){
8606
8646
  const id=m.id;
8607
8647
  const vscore=m._vscore?'<span class="vscore-badge">'+Math.round(m._vscore*100)+'%</span>':'';
8608
8648
  const sid=m.session_key||'';
8609
- const sidShort=sid.length>18?sid.slice(0,6)+'..'+sid.slice(-6):sid;
8649
+ const sidShort=fmtSessionDisplay(sid);
8610
8650
  const mc=m.merge_count||0;
8611
8651
  const cardTitle=esc(rawSummary||rawContent||'');
8612
8652
  const mergeBadge=mc>0?'<span class="merge-badge">\\u{1F504} '+t('card.evolved')+' '+mc+t('card.times')+'</span>':'';
@@ -8614,7 +8654,7 @@ function renderMemories(items){
8614
8654
  const ds=m.dedup_status||'active';
8615
8655
  const isInactive=ds==='merged'||ds==='duplicate';
8616
8656
  const dedupBadge=ds==='duplicate'?'<span class="dedup-badge duplicate">'+t('card.dedupDuplicate')+'</span>':ds==='merged'?'<span class="dedup-badge merged">'+t('card.dedupMerged')+'</span>':'';
8617
- const isImported=sid.startsWith('openclaw-import-')||sid.startsWith('openclaw-session-');
8657
+ const isImported=isImportedSession(sid);
8618
8658
  const importBadge=isImported?'<span class="import-badge">\u{1F990} '+t('card.imported')+'</span>':'';
8619
8659
  const ownerVal=m.owner||'agent:main';
8620
8660
  const isPublicMem=ownerVal==='public';
@@ -8648,8 +8688,10 @@ function renderMemories(items){
8648
8688
  }
8649
8689
  }catch(e){}
8650
8690
  }
8691
+ var ownerName=fmtAgentName(m.owner);
8692
+ var ownerBadge=ownerName?'<span class="owner-tag" title="'+esc(m.owner||'')+'">\\u{1F916} '+esc(ownerName)+'</span>':'';
8651
8693
  return '<div class="memory-card'+(isInactive?' dedup-inactive':'')+'">'+
8652
- '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span>'+memScopeBadge+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
8694
+ '<div class="card-header"><div class="meta"><span class="role-tag '+role+'">'+role+'</span>'+ownerBadge+memScopeBadge+importBadge+dedupBadge+mergeBadge+'</div><span class="card-time"><span class="session-tag" title="'+esc(sid)+'">'+esc(sidShort)+'</span> '+time+updatedAt+'</span></div>'+
8653
8695
  '<div class="card-summary">'+selectBoxHtml+cardTitle+'</div>'+
8654
8696
  (function(){
8655
8697
  if(mc<=0) return '';
@@ -8789,6 +8831,7 @@ async function showMemoryModal(chunkId){
8789
8831
  h+='<div class="mm-section"><div class="mm-section-label">'+t('admin.content')+'</div><pre class="mm-content">'+esc(m.content)+'</pre></div>';
8790
8832
  }
8791
8833
  h+='<div class="mm-meta">';
8834
+ if(m.owner) h+='<div class="mm-meta-chip"><strong>'+t('admin.owner')+'</strong><span>\\u{1F916} '+esc(m.owner)+'</span></div>';
8792
8835
  if(m.session_key) h+='<div class="mm-meta-chip"><strong>'+t('admin.session')+'</strong><span>'+esc(m.session_key)+'</span></div>';
8793
8836
  h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.created')+'</strong><span>'+fmtModalDate(m.created_at)+'</span></div>';
8794
8837
  if(m.updated_at) h+='<div class="mm-meta-chip"><strong>'+t('memory.detail.updated')+'</strong><span>'+fmtModalDate(m.updated_at)+'</span></div>';
@@ -8966,7 +9009,7 @@ async function migrateScan(showToast){
8966
9009
  const btn=document.getElementById('migrateScanBtn');
8967
9010
  btn.disabled=true;
8968
9011
  btn.textContent=t('migrate.scanning');
8969
- document.getElementById('migrateStartBtn').style.display='none';
9012
+ document.getElementById('migrateStartConcurrencyWrap').style.display='none';
8970
9013
  document.getElementById('migrateScanResult').style.display='none';
8971
9014
  document.getElementById('migrateConfigWarn').style.display='none';
8972
9015
  document.getElementById('migrateProgress').style.display='none';
@@ -8999,8 +9042,7 @@ async function migrateScan(showToast){
8999
9042
  const remaining=Math.max(0,(d.totalItems||0)-imported);
9000
9043
 
9001
9044
  if(d.totalItems>0 && d.configReady){
9002
- document.getElementById('migrateStartBtn').style.display='inline-flex';
9003
- document.getElementById('migrateConcurrencyRow').style.display='inline-flex';
9045
+ document.getElementById('migrateStartConcurrencyWrap').style.display='flex';
9004
9046
  if(d.hasImportedData){
9005
9047
  document.getElementById('migrateStartBtn').textContent=t('migrate.resume');
9006
9048
  }else{
@@ -9053,11 +9095,10 @@ async function migrateStart(){
9053
9095
 
9054
9096
  window._migrateRunning=true;
9055
9097
  _migrateStatusChecked=true;
9056
- document.getElementById('migrateStartBtn').style.display='none';
9098
+ document.getElementById('migrateStartConcurrencyWrap').style.display='none';
9057
9099
  document.getElementById('migrateScanBtn').disabled=true;
9058
9100
  var hintEl=document.getElementById('migrateImportedHint');
9059
9101
  if(hintEl) hintEl.style.display='none';
9060
- document.getElementById('migrateConcurrencyRow').style.display='none';
9061
9102
  document.getElementById('migrateConcurrencyWarn').style.display='none';
9062
9103
  document.getElementById('migrateProgress').style.display='block';
9063
9104
  document.getElementById('migrateLiveLog').innerHTML='';
@@ -9142,7 +9183,7 @@ async function checkMigrateStatus(){
9142
9183
  const progEl=document.getElementById('migrateProgress');
9143
9184
  if(!progEl)return;
9144
9185
  progEl.style.display='block';
9145
- document.getElementById('migrateStartBtn').style.display='none';
9186
+ document.getElementById('migrateStartConcurrencyWrap').style.display='none';
9146
9187
  document.getElementById('migrateScanBtn').disabled=true;
9147
9188
  document.getElementById('migrateStopBtn').disabled=false;
9148
9189
  const pct=s.total>0?Math.round((s.processed/s.total)*100):0;
@@ -9262,7 +9303,7 @@ function onMigrateDone(wasStopped,skipReload){
9262
9303
  document.getElementById('migrateStopBtn').style.display='none';
9263
9304
  if(wasStopped){
9264
9305
  document.getElementById('migrateBar').style.background='linear-gradient(90deg,#f59e0b,#fbbf24)';
9265
- document.getElementById('migrateStartBtn').style.display='inline-flex';
9306
+ document.getElementById('migrateStartConcurrencyWrap').style.display='flex';
9266
9307
  document.getElementById('migrateStartBtn').textContent=t('migrate.resume');
9267
9308
  document.getElementById('migratePhaseLabel').textContent=t('migrate.phase.stopped');
9268
9309
  }else{
@@ -622,6 +622,7 @@ export class ViewerServer {
622
622
  const items = tasks.map((t) => {
623
623
  const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(t.id) as { skill_status: string | null; owner: string | null } | undefined;
624
624
  const hubTask = this.getHubTaskForLocal(t.id);
625
+ const share = this.resolveTaskTeamShareForApi(t.id, hubTask);
625
626
  return {
626
627
  id: t.id,
627
628
  sessionKey: t.sessionKey,
@@ -633,7 +634,7 @@ export class ViewerServer {
633
634
  chunkCount: this.store.countChunksByTask(t.id),
634
635
  skillStatus: meta?.skill_status ?? null,
635
636
  owner: meta?.owner ?? "agent:main",
636
- sharingVisibility: hubTask?.visibility ?? null,
637
+ sharingVisibility: share.visibility,
637
638
  };
638
639
  });
639
640
 
@@ -737,6 +738,8 @@ export class ViewerServer {
737
738
  const t = this.store.getTask(taskId);
738
739
  if (!t) return null;
739
740
  const meta = db.prepare("SELECT skill_status, owner FROM tasks WHERE id = ?").get(taskId) as { skill_status: string | null; owner: string | null } | undefined;
741
+ const hubTask = this.getHubTaskForLocal(taskId);
742
+ const ts = this.resolveTaskTeamShareForApi(taskId, hubTask);
740
743
  return {
741
744
  id: t.id, sessionKey: t.sessionKey, title: t.title,
742
745
  summary: t.summary ?? "", status: t.status,
@@ -744,6 +747,7 @@ export class ViewerServer {
744
747
  chunkCount: this.store.countChunksByTask(t.id),
745
748
  skillStatus: meta?.skill_status ?? null,
746
749
  owner: meta?.owner ?? "agent:main",
750
+ sharingVisibility: ts.visibility,
747
751
  score,
748
752
  };
749
753
  }).filter(Boolean);
@@ -780,6 +784,7 @@ export class ViewerServer {
780
784
  const meta = db.prepare("SELECT skill_status, skill_reason FROM tasks WHERE id = ?").get(taskId) as
781
785
  { skill_status: string | null; skill_reason: string | null } | undefined;
782
786
  const hubTask = this.getHubTaskForLocal(taskId);
787
+ const ts = this.resolveTaskTeamShareForApi(taskId, hubTask);
783
788
 
784
789
  this.jsonResponse(res, {
785
790
  id: task.id,
@@ -794,9 +799,9 @@ export class ViewerServer {
794
799
  skillStatus: meta?.skill_status ?? null,
795
800
  skillReason: meta?.skill_reason ?? null,
796
801
  skillLinks,
797
- sharingVisibility: hubTask?.visibility ?? null,
798
- sharingGroupId: hubTask?.group_id ?? null,
799
- hubTaskId: hubTask ? true : false,
802
+ sharingVisibility: ts.visibility,
803
+ sharingGroupId: ts.groupId,
804
+ hubTaskId: ts.hasHubLink,
800
805
  });
801
806
  }
802
807
 
@@ -1616,7 +1621,8 @@ export class ViewerServer {
1616
1621
 
1617
1622
  const isLocalShared = task.owner === "public";
1618
1623
  const hubTask = this.getHubTaskForLocal(taskId);
1619
- const isTeamShared = !!hubTask;
1624
+ const taskShareUi = this.resolveTaskTeamShareForApi(taskId, hubTask);
1625
+ const isTeamShared = taskShareUi.hasHubLink;
1620
1626
  const currentScope = isTeamShared ? "team" : isLocalShared ? "local" : "private";
1621
1627
 
1622
1628
  if (scope === currentScope) {
@@ -1676,6 +1682,7 @@ export class ViewerServer {
1676
1682
  });
1677
1683
  if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1678
1684
  else this.store.downgradeTeamSharedTaskToLocal(taskId);
1685
+ this.store.clearTeamSharedChunksForTask(taskId);
1679
1686
  hubSynced = true;
1680
1687
  } catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
1681
1688
  }
@@ -1689,6 +1696,8 @@ export class ViewerServer {
1689
1696
  });
1690
1697
  if (this.sharingRole === "hub" && hubClient.userId) this.store.deleteHubTaskBySource(hubClient.userId, taskId);
1691
1698
  else if (!isLocalShared) this.store.unmarkTaskShared(taskId);
1699
+ else this.store.downgradeTeamSharedTaskToLocal(taskId);
1700
+ this.store.clearTeamSharedChunksForTask(taskId);
1692
1701
  hubSynced = true;
1693
1702
  } catch (err) { this.log.warn(`Failed to unshare task from team: ${err}`); }
1694
1703
  }
@@ -1816,6 +1825,39 @@ export class ViewerServer {
1816
1825
  return scopedHubInstanceId === currentHubInstanceId;
1817
1826
  }
1818
1827
 
1828
+ /**
1829
+ * Task list/detail/search: derive team-share badge when getHubTaskForLocal misses (e.g. client
1830
+ * hub_instance_id drift, or empty hub_task_id from hub while synced_chunks was recorded).
1831
+ */
1832
+ private resolveTaskTeamShareForApi(taskId: string, hubTask: any): { visibility: string | null; hasHubLink: boolean; groupId: string | null } {
1833
+ if (hubTask) {
1834
+ return {
1835
+ visibility: hubTask.visibility ?? null,
1836
+ hasHubLink: true,
1837
+ groupId: hubTask.group_id ?? null,
1838
+ };
1839
+ }
1840
+ const lst = this.store.getLocalSharedTask(taskId);
1841
+ if (lst) {
1842
+ const hid = String(lst.hubTaskId ?? "").trim();
1843
+ const teamLinked = hid.length > 0 || (lst.syncedChunks ?? 0) > 0;
1844
+ if (teamLinked) return { visibility: lst.visibility || null, hasHubLink: true, groupId: lst.groupId ?? null };
1845
+ }
1846
+ try {
1847
+ const db = (this.store as any).db;
1848
+ const chunkTeam = db.prepare(`
1849
+ SELECT t.visibility AS v, t.group_id AS g FROM team_shared_chunks t
1850
+ INNER JOIN chunks c ON c.id = t.chunk_id
1851
+ WHERE c.task_id = ?
1852
+ LIMIT 1
1853
+ `).get(taskId) as { v: string; g: string | null } | undefined;
1854
+ if (chunkTeam) {
1855
+ return { visibility: chunkTeam.v || null, hasHubLink: true, groupId: chunkTeam.g ?? null };
1856
+ }
1857
+ } catch { /* schema / db edge */ }
1858
+ return { visibility: null, hasHubLink: false, groupId: null };
1859
+ }
1860
+
1819
1861
  private getHubMemoryForChunk(chunkId: string): any {
1820
1862
  if (this.sharingRole === "hub") {
1821
1863
  const db = (this.store as any).db;
@@ -3982,7 +4024,7 @@ export class ViewerServer {
3982
4024
  try {
3983
4025
  if (this.store) {
3984
4026
  importedSessions = this.store.getDistinctSessionKeys()
3985
- .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
4027
+ .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-") || /^agent:[^:]+:(import|session:)/.test(sk));
3986
4028
  if (importedSessions.length > 0) {
3987
4029
  const placeholders = importedSessions.map(() => "?").join(",");
3988
4030
  const row = (this.store as any).db.prepare(
@@ -4195,7 +4237,7 @@ export class ViewerServer {
4195
4237
  totalProcessed++;
4196
4238
 
4197
4239
  const contentHash = crypto.createHash("sha256").update(row.text).digest("hex");
4198
- if (this.store.chunkExistsByContent(`openclaw-import-${agentId}`, "assistant", row.text)) {
4240
+ if (this.store.chunkExistsByContent(`agent:${agentId}:import`, "assistant", row.text) || this.store.chunkExistsByContent(`openclaw-import-${agentId}`, "assistant", row.text)) {
4199
4241
  totalSkipped++;
4200
4242
  send("item", {
4201
4243
  index: i + 1,
@@ -4295,7 +4337,7 @@ export class ViewerServer {
4295
4337
  const chunkId = uuid();
4296
4338
  const chunk: Chunk = {
4297
4339
  id: chunkId,
4298
- sessionKey: `openclaw-import-${agentId}`,
4340
+ sessionKey: `agent:${agentId}:import`,
4299
4341
  turnId: `import-${row.id}`,
4300
4342
  seq: 0,
4301
4343
  role: "assistant",
@@ -4450,8 +4492,8 @@ export class ViewerServer {
4450
4492
  const idx = incIdx();
4451
4493
  totalProcessed++;
4452
4494
 
4453
- const sessionKey = `openclaw-session-${sessionId}`;
4454
- if (this.store.chunkExistsByContent(sessionKey, msgRole, content)) {
4495
+ const sessionKey = `agent:${agentId}:session:${sessionId}`;
4496
+ if (this.store.chunkExistsByContent(sessionKey, msgRole, content) || this.store.chunkExistsByContent(`openclaw-session-${sessionId}`, msgRole, content)) {
4455
4497
  totalSkipped++;
4456
4498
  send("item", { index: idx, total: totalMsgs, status: "skipped", preview: content.slice(0, 120), source: file, agent: agentId, role: msgRole, reason: "duplicate" });
4457
4499
  continue;
@@ -4702,7 +4744,7 @@ export class ViewerServer {
4702
4744
  const ctx = this.ctx!;
4703
4745
 
4704
4746
  const importSessions = this.store.getDistinctSessionKeys()
4705
- .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-"));
4747
+ .filter((sk: string) => sk.startsWith("openclaw-import-") || sk.startsWith("openclaw-session-") || /^agent:[^:]+:(import|session:)/.test(sk));
4706
4748
 
4707
4749
  type PendingItem = { sessionKey: string; action: "full" | "skill-only"; owner: string };
4708
4750
  const pendingItems: PendingItem[] = [];