Compare commits
2 Commits
kimi/issue
...
kimi/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96acac5c5f | ||
| 2849dba756 |
@@ -144,6 +144,60 @@ class ShellHand:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_run_env(env: dict | None) -> dict:
|
||||||
|
"""Merge *env* overrides into the current process environment."""
|
||||||
|
import os
|
||||||
|
|
||||||
|
run_env = os.environ.copy()
|
||||||
|
if env:
|
||||||
|
run_env.update(env)
|
||||||
|
return run_env
|
||||||
|
|
||||||
|
async def _exec_subprocess(
|
||||||
|
self,
|
||||||
|
command: str,
|
||||||
|
effective_timeout: int,
|
||||||
|
cwd: str | None,
|
||||||
|
run_env: dict,
|
||||||
|
start: float,
|
||||||
|
) -> ShellResult:
|
||||||
|
"""Launch *command*, enforce timeout, and return the result."""
|
||||||
|
proc = await asyncio.create_subprocess_shell(
|
||||||
|
command,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
cwd=cwd,
|
||||||
|
env=run_env,
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
||||||
|
proc.communicate(), timeout=effective_timeout
|
||||||
|
)
|
||||||
|
except TimeoutError:
|
||||||
|
proc.kill()
|
||||||
|
await proc.wait()
|
||||||
|
logger.warning("Shell command timed out after %ds: %s", effective_timeout, command)
|
||||||
|
return ShellResult(
|
||||||
|
command=command,
|
||||||
|
success=False,
|
||||||
|
exit_code=-1,
|
||||||
|
error=f"Command timed out after {effective_timeout}s",
|
||||||
|
latency_ms=(time.time() - start) * 1000,
|
||||||
|
timed_out=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
exit_code = proc.returncode if proc.returncode is not None else -1
|
||||||
|
return ShellResult(
|
||||||
|
command=command,
|
||||||
|
success=exit_code == 0,
|
||||||
|
exit_code=exit_code,
|
||||||
|
stdout=stdout_bytes.decode("utf-8", errors="replace").strip(),
|
||||||
|
stderr=stderr_bytes.decode("utf-8", errors="replace").strip(),
|
||||||
|
latency_ms=(time.time() - start) * 1000,
|
||||||
|
)
|
||||||
|
|
||||||
async def run(
|
async def run(
|
||||||
self,
|
self,
|
||||||
command: str,
|
command: str,
|
||||||
@@ -164,7 +218,6 @@ class ShellHand:
|
|||||||
"""
|
"""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
# Validate
|
|
||||||
validation_error = self._validate_command(command)
|
validation_error = self._validate_command(command)
|
||||||
if validation_error:
|
if validation_error:
|
||||||
return ShellResult(
|
return ShellResult(
|
||||||
@@ -174,64 +227,21 @@ class ShellHand:
|
|||||||
latency_ms=(time.time() - start) * 1000,
|
latency_ms=(time.time() - start) * 1000,
|
||||||
)
|
)
|
||||||
|
|
||||||
effective_timeout = timeout or self._default_timeout
|
|
||||||
cwd = working_dir or self._working_dir
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import os
|
return await self._exec_subprocess(
|
||||||
|
|
||||||
run_env = os.environ.copy()
|
|
||||||
if env:
|
|
||||||
run_env.update(env)
|
|
||||||
|
|
||||||
proc = await asyncio.create_subprocess_shell(
|
|
||||||
command,
|
command,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
effective_timeout=timeout or self._default_timeout,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
cwd=working_dir or self._working_dir,
|
||||||
cwd=cwd,
|
run_env=self._build_run_env(env),
|
||||||
env=run_env,
|
start=start,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
stdout_bytes, stderr_bytes = await asyncio.wait_for(
|
|
||||||
proc.communicate(), timeout=effective_timeout
|
|
||||||
)
|
|
||||||
except TimeoutError:
|
|
||||||
proc.kill()
|
|
||||||
await proc.wait()
|
|
||||||
latency = (time.time() - start) * 1000
|
|
||||||
logger.warning("Shell command timed out after %ds: %s", effective_timeout, command)
|
|
||||||
return ShellResult(
|
|
||||||
command=command,
|
|
||||||
success=False,
|
|
||||||
exit_code=-1,
|
|
||||||
error=f"Command timed out after {effective_timeout}s",
|
|
||||||
latency_ms=latency,
|
|
||||||
timed_out=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
latency = (time.time() - start) * 1000
|
|
||||||
exit_code = proc.returncode if proc.returncode is not None else -1
|
|
||||||
stdout = stdout_bytes.decode("utf-8", errors="replace").strip()
|
|
||||||
stderr = stderr_bytes.decode("utf-8", errors="replace").strip()
|
|
||||||
|
|
||||||
return ShellResult(
|
|
||||||
command=command,
|
|
||||||
success=exit_code == 0,
|
|
||||||
exit_code=exit_code,
|
|
||||||
stdout=stdout,
|
|
||||||
stderr=stderr,
|
|
||||||
latency_ms=latency,
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
latency = (time.time() - start) * 1000
|
|
||||||
logger.warning("Shell command failed: %s — %s", command, exc)
|
logger.warning("Shell command failed: %s — %s", command, exc)
|
||||||
return ShellResult(
|
return ShellResult(
|
||||||
command=command,
|
command=command,
|
||||||
success=False,
|
success=False,
|
||||||
error=str(exc),
|
error=str(exc),
|
||||||
latency_ms=latency,
|
latency_ms=(time.time() - start) * 1000,
|
||||||
)
|
)
|
||||||
|
|
||||||
def status(self) -> dict:
|
def status(self) -> dict:
|
||||||
|
|||||||
@@ -772,23 +772,10 @@ class ThinkingEngine:
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Thought issue filing skipped: %s", exc)
|
logger.debug("Thought issue filing skipped: %s", exc)
|
||||||
|
|
||||||
def _gather_system_snapshot(self) -> str:
|
# ── System snapshot helpers ────────────────────────────────────────────
|
||||||
"""Gather lightweight real system state for grounding thoughts in reality.
|
|
||||||
|
|
||||||
Returns a short multi-line string with current time, thought count,
|
def _snap_thought_count(self, now: datetime) -> str | None:
|
||||||
recent chat activity, and task queue status. Never crashes — every
|
"""Return today's thought count, or *None* on failure."""
|
||||||
section is independently try/excepted.
|
|
||||||
"""
|
|
||||||
parts: list[str] = []
|
|
||||||
|
|
||||||
# Current local time
|
|
||||||
now = datetime.now().astimezone()
|
|
||||||
tz = now.strftime("%Z") or "UTC"
|
|
||||||
parts.append(
|
|
||||||
f"Local time: {now.strftime('%I:%M %p').lstrip('0')} {tz}, {now.strftime('%A %B %d')}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Thought count today (cheap DB query)
|
|
||||||
try:
|
try:
|
||||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
with _get_conn(self._db_path) as conn:
|
with _get_conn(self._db_path) as conn:
|
||||||
@@ -796,66 +783,94 @@ class ThinkingEngine:
|
|||||||
"SELECT COUNT(*) as c FROM thoughts WHERE created_at >= ?",
|
"SELECT COUNT(*) as c FROM thoughts WHERE created_at >= ?",
|
||||||
(today_start.isoformat(),),
|
(today_start.isoformat(),),
|
||||||
).fetchone()["c"]
|
).fetchone()["c"]
|
||||||
parts.append(f"Thoughts today: {count}")
|
return f"Thoughts today: {count}"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Thought count query failed: %s", exc)
|
logger.debug("Thought count query failed: %s", exc)
|
||||||
pass
|
return None
|
||||||
|
|
||||||
# Recent chat activity (in-memory, no I/O)
|
def _snap_chat_activity(self) -> list[str]:
|
||||||
|
"""Return chat-activity lines (in-memory, no I/O)."""
|
||||||
try:
|
try:
|
||||||
from infrastructure.chat_store import message_log
|
from infrastructure.chat_store import message_log
|
||||||
|
|
||||||
messages = message_log.all()
|
messages = message_log.all()
|
||||||
if messages:
|
if messages:
|
||||||
parts.append(f"Chat messages this session: {len(messages)}")
|
|
||||||
last = messages[-1]
|
last = messages[-1]
|
||||||
parts.append(f'Last chat ({last.role}): "{last.content[:80]}"')
|
return [
|
||||||
else:
|
f"Chat messages this session: {len(messages)}",
|
||||||
parts.append("No chat messages this session")
|
f'Last chat ({last.role}): "{last.content[:80]}"',
|
||||||
|
]
|
||||||
|
return ["No chat messages this session"]
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Chat activity query failed: %s", exc)
|
logger.debug("Chat activity query failed: %s", exc)
|
||||||
pass
|
return []
|
||||||
|
|
||||||
# Task queue (lightweight DB query)
|
def _snap_task_queue(self) -> str | None:
|
||||||
|
"""Return a one-line task queue summary, or *None*."""
|
||||||
try:
|
try:
|
||||||
from swarm.task_queue.models import get_task_summary_for_briefing
|
from swarm.task_queue.models import get_task_summary_for_briefing
|
||||||
|
|
||||||
summary = get_task_summary_for_briefing()
|
s = get_task_summary_for_briefing()
|
||||||
running = summary.get("running", 0)
|
running, pending = s.get("running", 0), s.get("pending_approval", 0)
|
||||||
pending = summary.get("pending_approval", 0)
|
done, failed = s.get("completed", 0), s.get("failed", 0)
|
||||||
done = summary.get("completed", 0)
|
|
||||||
failed = summary.get("failed", 0)
|
|
||||||
if running or pending or done or failed:
|
if running or pending or done or failed:
|
||||||
parts.append(
|
return (
|
||||||
f"Tasks: {running} running, {pending} pending, "
|
f"Tasks: {running} running, {pending} pending, "
|
||||||
f"{done} completed, {failed} failed"
|
f"{done} completed, {failed} failed"
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Task queue query failed: %s", exc)
|
logger.debug("Task queue query failed: %s", exc)
|
||||||
pass
|
return None
|
||||||
|
|
||||||
# Workspace updates (file-based communication with Hermes)
|
def _snap_workspace(self) -> list[str]:
|
||||||
|
"""Return workspace-update lines (file-based Hermes comms)."""
|
||||||
try:
|
try:
|
||||||
from timmy.workspace import workspace_monitor
|
from timmy.workspace import workspace_monitor
|
||||||
|
|
||||||
updates = workspace_monitor.get_pending_updates()
|
updates = workspace_monitor.get_pending_updates()
|
||||||
|
lines: list[str] = []
|
||||||
new_corr = updates.get("new_correspondence")
|
new_corr = updates.get("new_correspondence")
|
||||||
new_inbox = updates.get("new_inbox_files", [])
|
|
||||||
|
|
||||||
if new_corr:
|
if new_corr:
|
||||||
# Count entries (assuming each entry starts with a timestamp or header)
|
line_count = len([ln for ln in new_corr.splitlines() if ln.strip()])
|
||||||
line_count = len([line for line in new_corr.splitlines() if line.strip()])
|
lines.append(
|
||||||
parts.append(
|
|
||||||
f"Workspace: {line_count} new correspondence entries (latest from: Hermes)"
|
f"Workspace: {line_count} new correspondence entries (latest from: Hermes)"
|
||||||
)
|
)
|
||||||
|
new_inbox = updates.get("new_inbox_files", [])
|
||||||
if new_inbox:
|
if new_inbox:
|
||||||
files_str = ", ".join(new_inbox[:5])
|
files_str = ", ".join(new_inbox[:5])
|
||||||
if len(new_inbox) > 5:
|
if len(new_inbox) > 5:
|
||||||
files_str += f", ... (+{len(new_inbox) - 5} more)"
|
files_str += f", ... (+{len(new_inbox) - 5} more)"
|
||||||
parts.append(f"Workspace: {len(new_inbox)} new inbox files: {files_str}")
|
lines.append(f"Workspace: {len(new_inbox)} new inbox files: {files_str}")
|
||||||
|
return lines
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.debug("Workspace check failed: %s", exc)
|
logger.debug("Workspace check failed: %s", exc)
|
||||||
pass
|
return []
|
||||||
|
|
||||||
|
def _gather_system_snapshot(self) -> str:
|
||||||
|
"""Gather lightweight real system state for grounding thoughts in reality.
|
||||||
|
|
||||||
|
Returns a short multi-line string with current time, thought count,
|
||||||
|
recent chat activity, and task queue status. Never crashes — every
|
||||||
|
section is independently try/excepted.
|
||||||
|
"""
|
||||||
|
now = datetime.now().astimezone()
|
||||||
|
tz = now.strftime("%Z") or "UTC"
|
||||||
|
|
||||||
|
parts: list[str] = [
|
||||||
|
f"Local time: {now.strftime('%I:%M %p').lstrip('0')} {tz}, {now.strftime('%A %B %d')}"
|
||||||
|
]
|
||||||
|
|
||||||
|
thought_line = self._snap_thought_count(now)
|
||||||
|
if thought_line:
|
||||||
|
parts.append(thought_line)
|
||||||
|
|
||||||
|
parts.extend(self._snap_chat_activity())
|
||||||
|
|
||||||
|
task_line = self._snap_task_queue()
|
||||||
|
if task_line:
|
||||||
|
parts.append(task_line)
|
||||||
|
|
||||||
|
parts.extend(self._snap_workspace())
|
||||||
|
|
||||||
return "\n".join(parts) if parts else ""
|
return "\n".join(parts) if parts else ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user