memstack-skill-loader 4.0.1__tar.gz → 4.0.3__tar.gz

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 (28) hide show
  1. {memstack_skill_loader-4.0.1/src/memstack_skill_loader.egg-info → memstack_skill_loader-4.0.3}/PKG-INFO +1 -1
  2. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/pyproject.toml +1 -1
  3. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/__init__.py +1 -1
  4. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/agent_runner.py +104 -29
  5. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/dashboard.html +53 -9
  6. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3/src/memstack_skill_loader.egg-info}/PKG-INFO +1 -1
  7. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/MANIFEST.in +0 -0
  8. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/README.md +0 -0
  9. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/setup.cfg +0 -0
  10. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/__main__.py +0 -0
  11. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/categories.py +0 -0
  12. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/compression.py +0 -0
  13. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/config.py +0 -0
  14. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/dashboard.py +0 -0
  15. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/indexer.py +0 -0
  16. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/license.py +0 -0
  17. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/memory_db.py +0 -0
  18. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/search.py +0 -0
  19. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/server.py +0 -0
  20. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/skill_config.py +0 -0
  21. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/stats.py +0 -0
  22. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/tfidf_search.py +0 -0
  23. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader/version_check.py +0 -0
  24. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader.egg-info/SOURCES.txt +0 -0
  25. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader.egg-info/dependency_links.txt +0 -0
  26. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader.egg-info/entry_points.txt +0 -0
  27. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader.egg-info/requires.txt +0 -0
  28. {memstack_skill_loader-4.0.1 → memstack_skill_loader-4.0.3}/src/memstack_skill_loader.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.1
3
+ Version: 4.0.3
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "memstack-skill-loader"
7
- version = "4.0.1"
7
+ version = "4.0.3"
8
8
  description = "MCP server that vector-indexes MemStack Pro skills for on-demand loading"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -1,3 +1,3 @@
1
1
  """MemStack Skill Loader — MCP server for semantic skill search."""
2
2
 
3
- __version__ = "4.0.1"
3
+ __version__ = "4.0.3"
@@ -295,12 +295,45 @@ def _invoke_agent(name: str, prompt: str, working_dir: str, log_path: Optional[P
295
295
  )
296
296
  with _lock:
297
297
  _current_process = proc
298
+
299
+ stdout_chunks: list[str] = []
300
+ stderr_chunks: list[str] = []
301
+
302
+ def _reader(stream: object, dest: list[str]) -> None:
303
+ try:
304
+ for line in stream: # type: ignore[union-attr]
305
+ dest.append(line)
306
+ except Exception:
307
+ pass
308
+
309
+ t_out = threading.Thread(target=_reader, args=(proc.stdout, stdout_chunks), daemon=True)
310
+ t_err = threading.Thread(target=_reader, args=(proc.stderr, stderr_chunks), daemon=True)
311
+
298
312
  try:
299
- stdout_data, stderr_data = proc.communicate(input=prompt, timeout=timeout)
300
- except subprocess.TimeoutExpired:
301
- proc.kill()
302
- proc.communicate()
303
- raise
313
+ if proc.stdin:
314
+ try:
315
+ proc.stdin.write(prompt)
316
+ proc.stdin.close()
317
+ except OSError:
318
+ pass
319
+ t_out.start()
320
+ t_err.start()
321
+
322
+ deadline = time.monotonic() + timeout
323
+ while True:
324
+ t_out.join(timeout=30)
325
+ rc = proc.poll()
326
+ if rc is not None:
327
+ break
328
+ if time.monotonic() > deadline:
329
+ proc.kill()
330
+ t_out.join(5)
331
+ t_err.join(5)
332
+ raise subprocess.TimeoutExpired(cmd, timeout)
333
+ t_out.join(5)
334
+ t_err.join(5)
335
+ stdout_data = "".join(stdout_chunks)
336
+ stderr_data = "".join(stderr_chunks)
304
337
  finally:
305
338
  with _lock:
306
339
  if _current_process is proc:
@@ -367,9 +400,9 @@ class Session:
367
400
  self.started_at = time.strftime("%Y-%m-%d %H:%M:%S")
368
401
  self.messages: list[dict] = []
369
402
  self.agents: dict[str, dict] = {
370
- "manager": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": ""},
371
- "builder": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": ""},
372
- "reviewer": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": ""},
403
+ "manager": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": "", "started_at": None},
404
+ "builder": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": "", "started_at": None},
405
+ "reviewer": {"status": "idle", "input_tokens": 0, "output_tokens": 0, "context_tokens": 0, "last_output": "", "started_at": None},
373
406
  }
374
407
 
375
408
  def add_message(self, from_agent: str, to_agent: str, content: str) -> None:
@@ -471,6 +504,7 @@ def _orchestrate(session: Session) -> None:
471
504
  try:
472
505
  # Step 1: Manager analyzes the task
473
506
  session.agents["manager"]["status"] = "busy"
507
+ session.agents["manager"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
474
508
  session._save_state()
475
509
 
476
510
  context = _gather_project_context(session.working_dir)
@@ -480,13 +514,26 @@ def _orchestrate(session: Session) -> None:
480
514
  manager_prompt += "\n\nAdditional context from user:\n" + session.context
481
515
  if session.user_name:
482
516
  manager_prompt = f"The user's name is {session.user_name}.\n\n" + manager_prompt
483
- manager_output, m_in, m_out, m_ctx = _invoke_agent(
484
- "manager", manager_prompt, session.working_dir,
485
- log_path=session_log_dir / "manager.log",
486
- skip_permissions=True, session_id=session.session_id,
487
- timeout=session.timeout,
488
- model=session.models.get("manager", ""),
489
- )
517
+ try:
518
+ manager_output, m_in, m_out, m_ctx = _invoke_agent(
519
+ "manager", manager_prompt, session.working_dir,
520
+ log_path=session_log_dir / "manager.log",
521
+ skip_permissions=True, session_id=session.session_id,
522
+ timeout=min(180, session.timeout),
523
+ model=session.models.get("manager", ""),
524
+ )
525
+ except subprocess.TimeoutExpired:
526
+ session.agents["manager"]["status"] = "timeout"
527
+ session.status = "error"
528
+ session.result = "Manager timed out after 3 minutes. Try a simpler task description or break the task into smaller pieces."
529
+ session._save_state()
530
+ return
531
+ except RuntimeError:
532
+ session.agents["manager"]["status"] = "crashed"
533
+ session.status = "error"
534
+ session.result = "Manager agent stopped unexpectedly."
535
+ session._save_state()
536
+ return
490
537
  session.agents["manager"]["input_tokens"] += m_in
491
538
  session.agents["manager"]["output_tokens"] += m_out
492
539
  session.agents["manager"]["context_tokens"] = m_ctx
@@ -515,6 +562,7 @@ def _orchestrate(session: Session) -> None:
515
562
 
516
563
  # Builder
517
564
  session.agents["builder"]["status"] = "busy"
565
+ session.agents["builder"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
518
566
  session._save_state()
519
567
 
520
568
  builder_prompt = (
@@ -526,13 +574,26 @@ def _orchestrate(session: Session) -> None:
526
574
  )
527
575
  if session.user_name:
528
576
  builder_prompt = f"The user's name is {session.user_name}.\n\n" + builder_prompt
529
- builder_output, b_in, b_out, b_ctx = _invoke_agent(
530
- "builder", builder_prompt, session.working_dir,
531
- log_path=session_log_dir / "builder.log",
532
- skip_permissions=True, bare=True, session_id=session.session_id,
533
- timeout=session.timeout,
534
- model=session.models.get("builder", ""),
535
- )
577
+ try:
578
+ builder_output, b_in, b_out, b_ctx = _invoke_agent(
579
+ "builder", builder_prompt, session.working_dir,
580
+ log_path=session_log_dir / "builder.log",
581
+ skip_permissions=True, bare=True, session_id=session.session_id,
582
+ timeout=session.timeout,
583
+ model=session.models.get("builder", ""),
584
+ )
585
+ except subprocess.TimeoutExpired:
586
+ session.agents["builder"]["status"] = "timeout"
587
+ session.status = "error"
588
+ session.result = "Builder timed out. The task may be too large for a single agent pass."
589
+ session._save_state()
590
+ return
591
+ except RuntimeError:
592
+ session.agents["builder"]["status"] = "crashed"
593
+ session.status = "error"
594
+ session.result = "Builder agent stopped unexpectedly."
595
+ session._save_state()
596
+ return
536
597
  session.agents["builder"]["input_tokens"] += b_in
537
598
  session.agents["builder"]["output_tokens"] += b_out
538
599
  session.agents["builder"]["context_tokens"] = b_ctx
@@ -568,6 +629,7 @@ def _orchestrate(session: Session) -> None:
568
629
 
569
630
  # Reviewer
570
631
  session.agents["reviewer"]["status"] = "busy"
632
+ session.agents["reviewer"]["started_at"] = time.strftime("%Y-%m-%dT%H:%M:%S")
571
633
  session._save_state()
572
634
 
573
635
  reviewer_prompt = (
@@ -575,13 +637,26 @@ def _orchestrate(session: Session) -> None:
575
637
  + f"\n\nOriginal task from user:\n{session.task}"
576
638
  + f"\n\nBuilder output (iteration {iteration}):\n{builder_output}"
577
639
  )
578
- reviewer_output, r_in, r_out, r_ctx = _invoke_agent(
579
- "reviewer", reviewer_prompt, session.working_dir,
580
- log_path=session_log_dir / "reviewer.log",
581
- bare=True, session_id=session.session_id,
582
- timeout=session.timeout,
583
- model=session.models.get("reviewer", ""),
584
- )
640
+ try:
641
+ reviewer_output, r_in, r_out, r_ctx = _invoke_agent(
642
+ "reviewer", reviewer_prompt, session.working_dir,
643
+ log_path=session_log_dir / "reviewer.log",
644
+ bare=True, session_id=session.session_id,
645
+ timeout=session.timeout,
646
+ model=session.models.get("reviewer", ""),
647
+ )
648
+ except subprocess.TimeoutExpired:
649
+ session.agents["reviewer"]["status"] = "timeout"
650
+ session.status = "error"
651
+ session.result = "Reviewer timed out."
652
+ session._save_state()
653
+ return
654
+ except RuntimeError:
655
+ session.agents["reviewer"]["status"] = "crashed"
656
+ session.status = "error"
657
+ session.result = "Reviewer agent stopped unexpectedly."
658
+ session._save_state()
659
+ return
585
660
  session.agents["reviewer"]["input_tokens"] += r_in
586
661
  session.agents["reviewer"]["output_tokens"] += r_out
587
662
  session.agents["reviewer"]["context_tokens"] = r_ctx
@@ -1449,7 +1449,7 @@
1449
1449
  <summary class="model-selection-summary">Model Selection</summary>
1450
1450
  <div class="model-selection-grid">
1451
1451
  <div class="model-selection-col">
1452
- <label class="model-selection-label">Manager</label>
1452
+ <label id="agent-model-label-manager" class="model-selection-label">Manager</label>
1453
1453
  <select id="agent-model-manager" class="model-selection-select">
1454
1454
  <option value="claude-opus-4-6">claude-opus-4-6</option>
1455
1455
  <option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
@@ -1457,7 +1457,7 @@
1457
1457
  </select>
1458
1458
  </div>
1459
1459
  <div class="model-selection-col">
1460
- <label class="model-selection-label">Builder</label>
1460
+ <label id="agent-model-label-builder" class="model-selection-label">Builder</label>
1461
1461
  <select id="agent-model-builder" class="model-selection-select">
1462
1462
  <option value="claude-opus-4-6">claude-opus-4-6</option>
1463
1463
  <option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
@@ -1465,7 +1465,7 @@
1465
1465
  </select>
1466
1466
  </div>
1467
1467
  <div class="model-selection-col">
1468
- <label class="model-selection-label">Reviewer</label>
1468
+ <label id="agent-model-label-reviewer" class="model-selection-label">Reviewer</label>
1469
1469
  <select id="agent-model-reviewer" class="model-selection-select">
1470
1470
  <option value="claude-opus-4-6">claude-opus-4-6</option>
1471
1471
  <option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
@@ -2730,7 +2730,8 @@ document.addEventListener('visibilitychange', () => {
2730
2730
  const STATUS_COLORS = {
2731
2731
  idle: '#8b949e', busy: '#58a6ff', done: '#3fb950',
2732
2732
  running: '#3fb950', stopped: '#6e7681', error: '#f85149',
2733
- starting: '#d2a8ff', completed: '#3fb950'
2733
+ starting: '#d2a8ff', completed: '#3fb950',
2734
+ timeout: '#f0883e', crashed: '#f85149'
2734
2735
  };
2735
2736
 
2736
2737
  const ROLE_NAME_COLORS = {manager: '#d2a8ff', builder: '#58a6ff', reviewer: '#7ee787'};
@@ -2742,6 +2743,7 @@ const BUSY_PHRASES = {
2742
2743
  };
2743
2744
  let busyPhraseIntervals = {};
2744
2745
  let busyPhraseIndices = {manager: 0, builder: 0, reviewer: 0};
2746
+ let elapsedIntervals = {};
2745
2747
 
2746
2748
  const MODEL_DEFAULTS = {manager: 'claude-opus-4-6', builder: 'claude-sonnet-4-6', reviewer: 'claude-sonnet-4-6'};
2747
2749
 
@@ -2778,6 +2780,8 @@ async function resetAgentUI() {
2778
2780
  lastGitSuccess = true;
2779
2781
  const dsCard = document.getElementById('diary-summary-card');
2780
2782
  if (dsCard) { dsCard.style.display = 'none'; dsCard.innerHTML = ''; }
2783
+ document.getElementById('agent-workdir-input').value = '';
2784
+ loadLastWorkdir();
2781
2785
  }
2782
2786
 
2783
2787
  async function startAgentTask() {
@@ -2833,6 +2837,12 @@ async function loadAgentMonitor() {
2833
2837
  if (name) opt.textContent = name;
2834
2838
  }
2835
2839
  }
2840
+ for (const role of ['manager', 'builder', 'reviewer']) {
2841
+ const lbl = document.getElementById('agent-model-label-' + role);
2842
+ if (lbl && userProfile.agent_names && userProfile.agent_names[role]) {
2843
+ lbl.textContent = userProfile.agent_names[role];
2844
+ }
2845
+ }
2836
2846
  fetchAgentStatus();
2837
2847
  loadRecentProjects();
2838
2848
  loadLastWorkdir();
@@ -3111,10 +3121,17 @@ function renderAgentUI(data) {
3111
3121
  launcher.style.display = 'none';
3112
3122
  active.style.display = 'none';
3113
3123
  completed.style.display = '';
3114
- const icon = status === 'completed' ? '&#9989;' : status === 'stopped' ? '&#9209;' : '&#9888;&#65039;';
3124
+ const hasTimeout = Object.values(agents).some(ag => ag.status === 'timeout');
3125
+ const hasCrash = Object.values(agents).some(ag => ag.status === 'crashed');
3126
+ let icon, title;
3127
+ if (status === 'completed') { icon = '&#9989;'; title = 'Task Completed'; }
3128
+ else if (status === 'stopped') { icon = '&#9209;'; title = 'Task Stopped'; }
3129
+ else if (hasTimeout) { icon = '&#9203;'; title = 'Task Timed Out'; }
3130
+ else if (hasCrash) { icon = '&#9888;&#65039;'; title = 'Task Failed'; }
3131
+ else { icon = '&#9888;&#65039;'; title = 'Task Failed'; }
3115
3132
  document.querySelector('#agent-completed div div:first-child').innerHTML = icon;
3116
3133
  const h2 = document.querySelector('#agent-completed h2');
3117
- if (h2) h2.textContent = status === 'completed' ? 'Task Completed' : status === 'stopped' ? 'Task Stopped' : 'Task Failed';
3134
+ if (h2) h2.textContent = title;
3118
3135
  document.getElementById('agent-result-summary').textContent = status === 'stopped' ? (data.result || 'Task stopped by user') : (data.result || `Session ${status}`);
3119
3136
  const COMPACTION_LIMIT = 200000;
3120
3137
  const COMPACTION_WARN = 155000;
@@ -3218,8 +3235,12 @@ function renderAgentUI(data) {
3218
3235
  const phrase = BUSY_PHRASES[role][busyPhraseIndices[role] % BUSY_PHRASES[role].length];
3219
3236
  const customName = (userProfile.agent_names && userProfile.agent_names[role]) || role.charAt(0).toUpperCase() + role.slice(1);
3220
3237
  statusDisplay = `<span class="agent-status-phrase" id="agent-phrase-${role}"><span class="agent-name-label" style="color:${ROLE_NAME_COLORS[role] || '#e6edf3'}">${escapeHtml(customName)}:</span> ${escapeHtml(phrase)}</span>`;
3221
- } else if ((a.status === 'done' || a.status === 'completed') && data.started_at) {
3222
- const startMs = new Date(data.started_at).getTime();
3238
+ } else if (a.status === 'timeout') {
3239
+ statusDisplay = 'Timed out';
3240
+ } else if (a.status === 'crashed') {
3241
+ statusDisplay = 'Crashed';
3242
+ } else if ((a.status === 'done' || a.status === 'completed') && (a.started_at || data.started_at)) {
3243
+ const startMs = new Date(a.started_at || data.started_at).getTime();
3223
3244
  const elapsed = Math.max(0, Math.round((Date.now() - startMs) / 1000));
3224
3245
  const mins = Math.floor(elapsed / 60);
3225
3246
  const secs = elapsed % 60;
@@ -3227,6 +3248,15 @@ function renderAgentUI(data) {
3227
3248
  } else {
3228
3249
  statusDisplay = a.status;
3229
3250
  }
3251
+ let elapsedHtml = '';
3252
+ if (a.status === 'busy' && a.started_at) {
3253
+ const secs = Math.max(0, Math.floor((Date.now() - new Date(a.started_at).getTime()) / 1000));
3254
+ elapsedHtml = `<div class="agent-elapsed" id="agent-elapsed-${role}" style="font-size:0.7rem;color:#8b949e;margin-top:0.3rem;">Running: ${Math.floor(secs/60)}m ${secs%60}s</div>`;
3255
+ } else if ((a.status === 'done' || a.status === 'completed' || a.status === 'timeout' || a.status === 'crashed') && a.started_at) {
3256
+ const secs = Math.max(0, Math.floor((Date.now() - new Date(a.started_at).getTime()) / 1000));
3257
+ const label = a.status === 'timeout' ? 'Timed out after' : a.status === 'crashed' ? 'Crashed after' : 'Completed in';
3258
+ elapsedHtml = `<div style="font-size:0.7rem;color:#8b949e;margin-top:0.3rem;">${label} ${Math.floor(secs/60)}m ${secs%60}s</div>`;
3259
+ }
3230
3260
  const COMPACTION_LIMIT = 200000;
3231
3261
  const tokens = (a.context_tokens || 0);
3232
3262
  const pct = tokens > 0 ? Math.min(100, Math.round(tokens / COMPACTION_LIMIT * 100)) : -1;
@@ -3245,7 +3275,8 @@ function renderAgentUI(data) {
3245
3275
  showToast(role + ' context at ' + pct + '% — diary auto-saved', 'warning', 5000);
3246
3276
  }
3247
3277
  }
3248
- return `<div class="agent-card${a.status === 'error' ? ' agent-card-error-border' : ''}" style="${pulseStyle}">
3278
+ const errorBorder = (a.status === 'error' || a.status === 'timeout' || a.status === 'crashed') ? ' agent-card-error-border' : '';
3279
+ return `<div class="agent-card${errorBorder}" style="${pulseStyle}">
3249
3280
  <span class="agent-tooltip">${escapeHtml(ROLE_DESCRIPTIONS[role] || '')}</span>
3250
3281
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.6rem;">
3251
3282
  <strong style="color:#e6edf3;font-size:0.95rem;">${roleIcon} ${escapeHtml((userProfile.agent_names && userProfile.agent_names[role]) || role.charAt(0).toUpperCase() + role.slice(1))}</strong>
@@ -3254,6 +3285,7 @@ function renderAgentUI(data) {
3254
3285
  <div style="font-size:0.75rem;color:#8b949e;line-height:1.6;">
3255
3286
  <div style="margin-top:0.3rem;color:#c9d1d9;font-size:0.72rem;max-height:3em;overflow:hidden;text-overflow:ellipsis;">${escapeHtml((a.last_output || '').substring(0, 150))}</div>
3256
3287
  </div>
3288
+ ${elapsedHtml}
3257
3289
  ${contextHtml}
3258
3290
  </div>`;
3259
3291
  }).join('');
@@ -3276,6 +3308,18 @@ function renderAgentUI(data) {
3276
3308
  busyPhraseIntervals[role] = null;
3277
3309
  busyPhraseIndices[role] = 0;
3278
3310
  }
3311
+ if (a.status === 'busy' && a.started_at && !elapsedIntervals[role]) {
3312
+ const startMs = new Date(a.started_at).getTime();
3313
+ elapsedIntervals[role] = setInterval(() => {
3314
+ const el = document.getElementById('agent-elapsed-' + role);
3315
+ if (!el) return;
3316
+ const secs = Math.max(0, Math.floor((Date.now() - startMs) / 1000));
3317
+ el.textContent = 'Running: ' + Math.floor(secs / 60) + 'm ' + (secs % 60) + 's';
3318
+ }, 1000);
3319
+ } else if (a.status !== 'busy' && elapsedIntervals[role]) {
3320
+ clearInterval(elapsedIntervals[role]);
3321
+ elapsedIntervals[role] = null;
3322
+ }
3279
3323
  });
3280
3324
 
3281
3325
  renderMessageLog(document.getElementById('agent-messages'), messages);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: memstack-skill-loader
3
- Version: 4.0.1
3
+ Version: 4.0.3
4
4
  Summary: MCP server that vector-indexes MemStack Pro skills for on-demand loading
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: mcp>=1.0.0