aicortexpy 0.2.2__tar.gz → 0.2.4__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 (102) hide show
  1. {aicortexpy-0.2.2/aicortexpy.egg-info → aicortexpy-0.2.4}/PKG-INFO +1 -1
  2. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/__init__.py +1 -1
  3. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/acp_conn.py +48 -13
  4. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/acp_errors.py +23 -0
  5. aicortexpy-0.2.4/aicortexpy/app_workdir.py +72 -0
  6. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/artifact_ws.py +5 -1
  7. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/artifacts.py +6 -0
  8. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/auto_commit.py +3 -1
  9. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/cli.py +10 -0
  10. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/client.py +116 -1
  11. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/agent_cmd.py +5 -0
  12. aicortexpy-0.2.4/aicortexpy/commands/graph_cmd.py +204 -0
  13. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/memory_cmd.py +1 -1
  14. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/config.py +7 -2
  15. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/daemon.py +28 -10
  16. aicortexpy-0.2.4/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +46 -0
  17. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/execenv.py +13 -1
  18. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/models.py +12 -3
  19. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/openclaw_config.py +3 -1
  20. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/process.py +16 -3
  21. aicortexpy-0.2.4/aicortexpy/procutil.py +113 -0
  22. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/repo.py +13 -12
  23. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/runner.py +17 -6
  24. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/runtime_config.py +73 -1
  25. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/terminal.py +3 -0
  26. {aicortexpy-0.2.2 → aicortexpy-0.2.4/aicortexpy.egg-info}/PKG-INFO +1 -1
  27. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/SOURCES.txt +3 -1
  28. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/pyproject.toml +1 -1
  29. aicortexpy-0.2.4/tests/test_app_workdir.py +59 -0
  30. aicortexpy-0.2.2/aicortexpy/commands/graph_cmd.py +0 -69
  31. aicortexpy-0.2.2/aicortexpy/data/default_skills/aicortex-graph/SKILL.md +0 -30
  32. aicortexpy-0.2.2/aicortexpy/procutil.py +0 -61
  33. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/MANIFEST.in +0 -0
  34. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/README.md +0 -0
  35. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortex/__init__.py +0 -0
  36. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortex/__main__.py +0 -0
  37. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/__main__.py +0 -0
  38. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/acp_client.py +0 -0
  39. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/agent_env.py +0 -0
  40. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/attachments.py +0 -0
  41. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/capabilities.py +0 -0
  42. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/chat_acp_pool.py +0 -0
  43. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/cli_context.py +0 -0
  44. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/codex_config.py +0 -0
  45. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/__init__.py +0 -0
  46. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/attachment_cmd.py +0 -0
  47. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/auth_cmd.py +0 -0
  48. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/autopilot_cmd.py +0 -0
  49. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/config_cmd.py +0 -0
  50. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/daemon_cmd.py +0 -0
  51. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/issue_cmd.py +0 -0
  52. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/label_cmd.py +0 -0
  53. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/meeting_cmd.py +0 -0
  54. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/project_cmd.py +0 -0
  55. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/repo_cmd.py +0 -0
  56. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/runtime_cmd.py +0 -0
  57. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/setup_cmd.py +0 -0
  58. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/skill_cmd.py +0 -0
  59. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/squad_cmd.py +0 -0
  60. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/update_cmd.py +0 -0
  61. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/version_cmd.py +0 -0
  62. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/commands/workspace_cmd.py +0 -0
  63. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/agent-memory/SKILL.md +0 -0
  64. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-atlassian/SKILL.md +0 -0
  65. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-graph/SKILL.md +0 -0
  66. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-outlook/SKILL.md +0 -0
  67. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicorkit-wxmsg/SKILL.md +0 -0
  68. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicortex-basics/SKILL.md +0 -0
  69. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicortex-bridge-cdp/SKILL.md +0 -0
  70. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/aicortex-cli-ops/SKILL.md +0 -0
  71. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/graphify/SKILL.md +0 -0
  72. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/question-form/SKILL.md +0 -0
  73. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/test-report-generator/SKILL.md +0 -0
  74. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_cases.py +0 -0
  75. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/data/default_skills/test-report-generator/scripts/generate_report.py +0 -0
  76. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/design_seed.py +0 -0
  77. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/diskusage.py +0 -0
  78. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/gc.py +0 -0
  79. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/handoff_bundle.py +0 -0
  80. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/health.py +0 -0
  81. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/id_resolve.py +0 -0
  82. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/local_skills.py +0 -0
  83. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/oauth_login.py +0 -0
  84. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/output.py +0 -0
  85. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/permission.py +0 -0
  86. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/pinned_workdir.py +0 -0
  87. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/platform_client.py +0 -0
  88. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/platform_info.py +0 -0
  89. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/poisoned.py +0 -0
  90. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/prompt.py +0 -0
  91. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/sandbox.py +0 -0
  92. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/security.py +0 -0
  93. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/stream.py +0 -0
  94. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/tunnel.py +0 -0
  95. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy/wakeup.py +0 -0
  96. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/dependency_links.txt +0 -0
  97. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/entry_points.txt +0 -0
  98. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/requires.txt +0 -0
  99. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/aicortexpy.egg-info/top_level.txt +0 -0
  100. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/scripts/aicortex +0 -0
  101. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/scripts/aicortex.cmd +0 -0
  102. {aicortexpy-0.2.2 → aicortexpy-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: aicortexpy
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: AICortex Python CLI and local agent runtime daemon (Go CLI compatible)
5
5
  Author: AICortex
6
6
  License: Apache-2.0
@@ -1,3 +1,3 @@
1
1
  """AICortex Python CLI + local agent runtime daemon."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.2.4"
@@ -23,7 +23,11 @@ from .acp_client import (
23
23
  kiro_tool_name,
24
24
  resolve_resumed_session_id,
25
25
  )
26
- from .acp_errors import ACPProviderErrorSniffer, promote_acp_result_on_provider_error
26
+ from .acp_errors import (
27
+ ACPProviderErrorSniffer,
28
+ enrich_kiro_acp_error,
29
+ promote_acp_result_on_provider_error,
30
+ )
27
31
  from .procutil import kill_process_tree, popen_kwargs_for_killable_group
28
32
  from .security import SecurityPolicy, build_cursor_acp_global_flags, default_security_policy
29
33
 
@@ -33,9 +37,20 @@ HANDSHAKE_TIMEOUT = 120.0
33
37
  PROMPT_TIMEOUT = 20 * 60.0
34
38
  STDOUT_MAX_LINE = 10 * 1024 * 1024 # match Go scanner max token
35
39
 
40
+ # UI / agent form may send these; ACP session/set_model rejects them (e.g. Cursor -32602).
41
+ _ACP_MODEL_SENTINELS = frozenset({"", "auto", "default", "none"})
42
+
36
43
  MessageCB = Callable[[dict[str, Any]], None]
37
44
 
38
45
 
46
+ def normalize_acp_model(model: str | None) -> str:
47
+ """Return a concrete model id for ACP, or '' to keep the agent default."""
48
+ m = (model or "").strip()
49
+ if m.lower() in _ACP_MODEL_SENTINELS:
50
+ return ""
51
+ return m
52
+
53
+
39
54
  @dataclass
40
55
  class ACPPromptResult:
41
56
  status: str
@@ -73,15 +88,16 @@ def kiro_acp_supported(executable: str) -> bool:
73
88
 
74
89
 
75
90
  def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
91
+ from .procutil import run_no_window
92
+
76
93
  path = shutil.which(executable) or executable
77
94
  args = [path, *extra_before]
78
95
  try:
79
- proc = subprocess.run(
96
+ proc = run_no_window(
80
97
  args,
81
98
  capture_output=True,
82
99
  timeout=5,
83
100
  check=False,
84
- **_hide_window(),
85
101
  )
86
102
  return proc.returncode == 0
87
103
  except (OSError, subprocess.TimeoutExpired):
@@ -89,11 +105,9 @@ def _acp_help_ok(executable: str, *, extra_before: list[str]) -> bool:
89
105
 
90
106
 
91
107
  def _hide_window() -> dict:
92
- if sys.platform != "win32":
93
- return {}
94
- startup = subprocess.STARTUPINFO()
95
- startup.dwFlags |= subprocess.STARTF_USESHOWWINDOW
96
- return {"startupinfo": startup}
108
+ from .procutil import popen_kwargs_hidden_console
109
+
110
+ return popen_kwargs_hidden_console()
97
111
 
98
112
 
99
113
  def choose_cursor_invocation(executable: str, args: list[str]) -> tuple[str, list[str]]:
@@ -344,6 +358,8 @@ class _BaseACPConn:
344
358
  status, error = promote_acp_result_on_provider_error(
345
359
  status, error, output, self._sniffer
346
360
  )
361
+ if self.provider == "kiro" and status == "failed" and error:
362
+ error = enrich_kiro_acp_error(error)
347
363
 
348
364
  usage = None
349
365
  with client._usage_lock:
@@ -528,26 +544,43 @@ class _BaseACPConn:
528
544
  raise RuntimeError(f"{self.provider} acp session/new returned no session ID")
529
545
  self._session_id = session_id
530
546
  self._client.session_id = session_id
531
- self._model = model
547
+ # UI may send sentinel "auto"/"default"; ACP session/set_model rejects those.
548
+ effective_model = normalize_acp_model(model)
549
+ self._model = effective_model
532
550
  log.info("%s acp session ready session_id=%s", self.provider, session_id)
533
- if not model:
551
+ if not effective_model:
552
+ if (model or "").strip():
553
+ log.info(
554
+ "%s acp skipping set_model for sentinel model=%r",
555
+ self.provider,
556
+ model,
557
+ )
534
558
  return
535
559
  try:
536
560
  if self.model_via_config_option:
537
561
  self._client.request(
538
562
  "session/set_config_option",
539
- {"sessionId": session_id, "configId": "model", "value": model},
563
+ {
564
+ "sessionId": session_id,
565
+ "configId": "model",
566
+ "value": effective_model,
567
+ },
540
568
  timeout=handshake_timeout,
541
569
  )
542
570
  else:
543
571
  self._client.request(
544
572
  "session/set_model",
545
- {"sessionId": session_id, "modelId": model},
573
+ {"sessionId": session_id, "modelId": effective_model},
546
574
  timeout=handshake_timeout,
547
575
  )
548
576
  except Exception as e:
549
577
  if self.soft_set_model:
550
- log.debug("%s acp set_model skipped: %s", self.provider, e)
578
+ log.warning(
579
+ "%s acp set_model soft-failed model=%r: %s (continuing with agent default)",
580
+ self.provider,
581
+ effective_model,
582
+ e,
583
+ )
551
584
  return
552
585
  self.close()
553
586
  raise RuntimeError(f"{self.provider} acp set model failed: {e}") from e
@@ -556,6 +589,8 @@ class _BaseACPConn:
556
589
  class CursorACPConn(_BaseACPConn):
557
590
  provider = "cursor"
558
591
  require_set_model = True
592
+ # Prefer pooled chat over hard-failing on an unknown model id.
593
+ soft_set_model = True
559
594
 
560
595
  @classmethod
561
596
  def open(
@@ -19,6 +19,10 @@ _ACP_TERMINAL_ERROR_RE = re.compile(
19
19
  _ACP_AGENT_OUTPUT_TERMINAL_RE = re.compile(r"API call failed after \d+ retr(?:y|ies)")
20
20
  _ACP_MAX_ERROR_LINES = 8
21
21
 
22
+ _KIRO_EXPLICIT_AUTH_RE = re.compile(r"no token|not logged in", re.IGNORECASE)
23
+ _KIRO_DISPATCH_RE = re.compile(r"dispatch failure", re.IGNORECASE)
24
+ _KIRO_LOGIN_HINT_RE = re.compile(r"kiro-cli login", re.IGNORECASE)
25
+
22
26
 
23
27
  class ACPProviderErrorSniffer:
24
28
  def __init__(self, provider: str) -> None:
@@ -105,3 +109,22 @@ def promote_acp_result_on_provider_error(
105
109
  if msg:
106
110
  return "failed", msg
107
111
  return final_status, final_error
112
+
113
+
114
+ def enrich_kiro_acp_error(error: str) -> str:
115
+ """Append login/retry hints from Kiro ACP error text alone (no extra CLI probes)."""
116
+ if not error or _KIRO_LOGIN_HINT_RE.search(error):
117
+ return error
118
+ if _KIRO_EXPLICIT_AUTH_RE.search(error):
119
+ return (
120
+ "kiro-cli is not logged in. Run `kiro-cli login`, then retry. "
121
+ f"({error})"
122
+ )
123
+ if _KIRO_DISPATCH_RE.search(error):
124
+ # Kiro often surfaces logout/network problems only as "dispatch failure".
125
+ return (
126
+ f"{error} "
127
+ "(common causes: kiro-cli not logged in - run `kiro-cli login`; "
128
+ "or transient AWS Q network error - retry)"
129
+ )
130
+ return error
@@ -0,0 +1,72 @@
1
+ """Pinned app template / instance workdirs (port of Go daemon/app_workdir.go)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .client import Task
10
+
11
+ log = logging.getLogger("aicortexpy.app_workdir")
12
+
13
+ _DEFAULT_SEED = b"<!DOCTYPE html><html><body><h1>App</h1></body></html>\n"
14
+
15
+
16
+ def app_uses_pinned_workdir(task: Task) -> bool:
17
+ return bool(task.app_id.strip()) and task.app_mode in ("edit", "run")
18
+
19
+
20
+ def prepare_app_workdir(workspaces_root: Path, task: Task) -> Path:
21
+ """Create/reuse app template or instance dir; seed index.html from task.app_html."""
22
+ ws = (task.workspace_id or "").strip()
23
+ app_id = task.app_id.strip()
24
+ if not ws or not app_id:
25
+ raise ValueError("missing app workdir identity")
26
+
27
+ base = workspaces_root / ws / "apps" / app_id
28
+ mode = task.app_mode
29
+ if mode == "edit":
30
+ directory = base / "template"
31
+ elif mode == "run":
32
+ if task.app_workdir_mode == "shared":
33
+ directory = base / "instances" / "shared"
34
+ else:
35
+ user_id = task.app_user_id.strip() or "anonymous"
36
+ directory = base / "instances" / "users" / user_id
37
+ else:
38
+ raise ValueError(f"unknown app mode {mode!r}")
39
+
40
+ directory.mkdir(parents=True, exist_ok=True)
41
+ index_path = directory / "index.html"
42
+ seed_text = task.app_html.strip()
43
+ seed = seed_text.encode("utf-8") if seed_text else _DEFAULT_SEED
44
+ need_write = (not index_path.is_file()) or index_path.stat().st_size == 0
45
+ if not need_write and seed_text:
46
+ # Server AppHTML is authoritative at task start; refresh stale disk
47
+ # copies (e.g. first-run default seed) so complete_task won't clobber DB.
48
+ try:
49
+ existing = index_path.read_text(encoding="utf-8")
50
+ except OSError:
51
+ existing = ""
52
+ if existing.strip() != seed_text:
53
+ need_write = True
54
+ if need_write:
55
+ index_path.write_bytes(seed)
56
+ log.info("seeded app index.html at %s mode=%s", index_path, mode)
57
+ return directory
58
+
59
+
60
+ def read_app_index_html(work_dir: str | Path) -> str:
61
+ path = Path(work_dir) / "index.html"
62
+ return path.read_text(encoding="utf-8")
63
+
64
+
65
+ def is_app_workdir_path(path: Path) -> bool:
66
+ """True when path sits under .../apps/<id>/(template|instances)/..."""
67
+ parts = path.resolve().parts
68
+ try:
69
+ i = parts.index("apps")
70
+ except ValueError:
71
+ return False
72
+ return i + 1 < len(parts)
@@ -110,10 +110,14 @@ class ArtifactBrowser:
110
110
  st = child.stat()
111
111
  except OSError:
112
112
  continue
113
+ child_rel = (
114
+ f"{rel.rstrip('/')}/{child.name}".lstrip("/") if rel else child.name
115
+ )
116
+ # Protocol (Go ArtifactListEntry) expects `path`, not `rel_path`.
113
117
  entries.append(
114
118
  {
115
119
  "name": child.name,
116
- "rel_path": f"{rel.rstrip('/')}/{child.name}".lstrip("/") if rel else child.name,
120
+ "path": child_rel,
117
121
  "is_dir": child.is_dir(),
118
122
  "size": st.st_size if child.is_file() else 0,
119
123
  "mtime": int(st.st_mtime),
@@ -64,4 +64,10 @@ def report_issue_artifacts(
64
64
  try:
65
65
  report(items)
66
66
  except Exception as e:
67
+ # Issue preview is feature-flagged off by default (404 "not found").
68
+ # Chat/app workdirs often have index.html that is not an issue artifact.
69
+ status = getattr(e, "status", None)
70
+ if status in (400, 404):
71
+ log.debug("report artifacts skipped: %s", e)
72
+ return
67
73
  log.warning("report artifacts failed: %s", e)
@@ -27,7 +27,9 @@ EXCLUDE = [
27
27
 
28
28
 
29
29
  def _git(cwd: Path, *args: str) -> tuple[int, str, str]:
30
- proc = subprocess.run(
30
+ from .procutil import run_no_window
31
+
32
+ proc = run_no_window(
31
33
  ["git", *args],
32
34
  cwd=str(cwd),
33
35
  capture_output=True,
@@ -63,11 +63,21 @@ def _setup_logging(
63
63
  level = logging.INFO
64
64
  else:
65
65
  level = logging.WARNING
66
+ # Windows consoles are often GBK; force UTF-8 so agent names like 小政 log cleanly.
67
+ for stream in (sys.stdout, sys.stderr):
68
+ reconf = getattr(stream, "reconfigure", None)
69
+ if callable(reconf):
70
+ try:
71
+ reconf(encoding="utf-8", errors="replace")
72
+ except Exception:
73
+ pass
66
74
  logging.basicConfig(
67
75
  level=level,
68
76
  format="%(asctime)s %(levelname)s %(name)s: %(message)s",
69
77
  datefmt="%H:%M:%S",
70
78
  force=True,
79
+ encoding="utf-8",
80
+ errors="replace",
71
81
  )
72
82
 
73
83
 
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import logging
6
7
  import ssl
7
8
  import urllib.error
8
9
  import urllib.request
@@ -12,6 +13,7 @@ from typing import Any
12
13
  from .platform_info import host_os
13
14
 
14
15
  MAX_ATTACHMENT_BYTES = 100 << 20
16
+ log = logging.getLogger("aicortexpy")
15
17
 
16
18
 
17
19
  class APIError(Exception):
@@ -31,6 +33,29 @@ def is_not_found(err: Exception, needle: str) -> bool:
31
33
  )
32
34
 
33
35
 
36
+ def is_connection_reset(err: BaseException) -> bool:
37
+ """True when the peer closed the TCP socket mid-request (e.g. WinError 10054)."""
38
+ if isinstance(err, ConnectionResetError):
39
+ return True
40
+ if isinstance(err, BrokenPipeError):
41
+ return True
42
+ if isinstance(err, OSError):
43
+ # Windows: 10054 (reset), 10053 (local abort); POSIX: ECONNRESET=104 / macOS 54
44
+ if getattr(err, "winerror", None) in (10054, 10053):
45
+ return True
46
+ if getattr(err, "errno", None) in (104, 54):
47
+ return True
48
+ # urllib wraps the socket error in URLError.reason
49
+ reason = getattr(err, "reason", None)
50
+ if isinstance(reason, BaseException) and reason is not err:
51
+ return is_connection_reset(reason)
52
+ cause = getattr(err, "__cause__", None) or getattr(err, "__context__", None)
53
+ if isinstance(cause, BaseException) and cause is not err:
54
+ return is_connection_reset(cause)
55
+ msg = str(err)
56
+ return "10054" in msg or "10053" in msg
57
+
58
+
34
59
  @dataclass
35
60
  class Task:
36
61
  raw: dict[str, Any]
@@ -177,6 +202,38 @@ class Task:
177
202
  def session_kind(self) -> str:
178
203
  return self._s("session_kind")
179
204
 
205
+ @property
206
+ def app_id(self) -> str:
207
+ return self._s("app_id")
208
+
209
+ @property
210
+ def app_mode(self) -> str:
211
+ return self._s("app_mode")
212
+
213
+ @property
214
+ def app_name(self) -> str:
215
+ return self._s("app_name")
216
+
217
+ @property
218
+ def app_description(self) -> str:
219
+ return self._s("app_description")
220
+
221
+ @property
222
+ def app_spec(self) -> str:
223
+ return self._s("app_spec")
224
+
225
+ @property
226
+ def app_workdir_mode(self) -> str:
227
+ return self._s("app_workdir_mode")
228
+
229
+ @property
230
+ def app_html(self) -> str:
231
+ return self._s("app_html")
232
+
233
+ @property
234
+ def app_user_id(self) -> str:
235
+ return self._s("app_user_id")
236
+
180
237
  @property
181
238
  def squad_id(self) -> str:
182
239
  return self._s("squad_id")
@@ -238,6 +295,9 @@ class DaemonClient:
238
295
  "Authorization": f"Bearer {self.token}",
239
296
  "X-Client-Platform": "daemon",
240
297
  "X-Client-OS": host_os(),
298
+ # Avoid Windows urllib keep-alive reuse of half-closed sockets
299
+ # (WinError 10054 mid-claim after the server already dispatched).
300
+ "Connection": "close",
241
301
  }
242
302
  if self.version:
243
303
  headers["X-Client-Version"] = self.version
@@ -293,7 +353,32 @@ class DaemonClient:
293
353
  return data if isinstance(data, dict) else {}
294
354
 
295
355
  def claim_task(self, runtime_id: str) -> Task | None:
296
- data = self.post(f"/api/daemon/runtimes/{runtime_id}/tasks/claim", {})
356
+ path = f"/api/daemon/runtimes/{runtime_id}/tasks/claim"
357
+ try:
358
+ data = self.post(path, {})
359
+ except Exception as e:
360
+ # Server may have already marked the task dispatched before the
361
+ # response was reset. Retry once: ClaimTaskForRuntime redelivers
362
+ # unstarted dispatched rows. Only fail orphans if retry also dies
363
+ # — otherwise we kill healthy work that just lost the HTTP body.
364
+ if not is_connection_reset(e):
365
+ raise
366
+ log.warning(
367
+ "claim %s connection reset; retrying once for redelivery",
368
+ runtime_id[:8],
369
+ )
370
+ try:
371
+ data = self.post(path, {})
372
+ except Exception as e2:
373
+ if is_connection_reset(e2):
374
+ n = self.fail_orphaned_dispatched(runtime_id, reason=str(e2))
375
+ if n:
376
+ log.warning(
377
+ "claim %s retry reset; failed %d orphaned dispatched task(s)",
378
+ runtime_id[:8],
379
+ n,
380
+ )
381
+ raise
297
382
  if not isinstance(data, dict):
298
383
  return None
299
384
  task = data.get("task")
@@ -301,6 +386,33 @@ class DaemonClient:
301
386
  return None
302
387
  return Task(task)
303
388
 
389
+ def list_pending_tasks(self, runtime_id: str) -> list[dict[str, Any]]:
390
+ data = self.get(f"/api/daemon/runtimes/{runtime_id}/tasks/pending")
391
+ return [x for x in data if isinstance(x, dict)] if isinstance(data, list) else []
392
+
393
+ def fail_orphaned_dispatched(self, runtime_id: str, *, reason: str = "") -> int:
394
+ """Fail dispatched tasks left behind when a claim response was lost."""
395
+ try:
396
+ pending = self.list_pending_tasks(runtime_id)
397
+ except Exception:
398
+ return 0
399
+ failed = 0
400
+ msg = "claim response lost (connection reset); please retry"
401
+ if reason:
402
+ msg = f"{msg}: {reason}"
403
+ for row in pending:
404
+ if str(row.get("status") or "") != "dispatched":
405
+ continue
406
+ tid = str(row.get("id") or "")
407
+ if not tid:
408
+ continue
409
+ try:
410
+ self.fail_task(tid, msg, failure_reason="runtime_recovery")
411
+ failed += 1
412
+ except Exception:
413
+ continue
414
+ return failed
415
+
304
416
  def start_task(self, task_id: str) -> None:
305
417
  self.post(f"/api/daemon/tasks/{task_id}/start", {})
306
418
 
@@ -318,6 +430,7 @@ class DaemonClient:
318
430
  work_dir: str = "",
319
431
  session_id: str = "",
320
432
  branch_name: str = "",
433
+ app_html: str = "",
321
434
  ) -> None:
322
435
  body: dict[str, Any] = {"output": output}
323
436
  if work_dir:
@@ -326,6 +439,8 @@ class DaemonClient:
326
439
  body["session_id"] = session_id
327
440
  if branch_name:
328
441
  body["branch_name"] = branch_name
442
+ if app_html.strip():
443
+ body["app_html"] = app_html
329
444
  self.post(f"/api/daemon/tasks/{task_id}/complete", body)
330
445
 
331
446
  def fail_task(
@@ -28,6 +28,7 @@ def add_parser(sub: argparse._SubParsersAction) -> None:
28
28
  create.add_argument("--name", required=True)
29
29
  create.add_argument("--instructions", default="")
30
30
  create.add_argument("--from-template", default="")
31
+ create.add_argument("--runtime-id", default="", help="Runtime id to execute this agent")
31
32
  create.add_argument("--output", choices=["json", "table"], default="json")
32
33
  update = asub.add_parser("update", help="Update agent")
33
34
  update.add_argument("agent_id")
@@ -79,9 +80,13 @@ def run(ns: argparse.Namespace) -> int:
79
80
  if ns.agent_command == "create":
80
81
  if ns.from_template:
81
82
  body = {"name": ns.name, "template": ns.from_template, "workspace_id": client.workspace_id}
83
+ if ns.runtime_id:
84
+ body["runtime_id"] = ns.runtime_id
82
85
  print_json(client.post("/api/agents/from-template", body))
83
86
  else:
84
87
  body = {"name": ns.name, "workspace_id": client.workspace_id}
88
+ if ns.runtime_id:
89
+ body["runtime_id"] = ns.runtime_id
85
90
  if ns.instructions:
86
91
  body["instructions"] = ns.instructions
87
92
  print_json(client.post("/api/agents", body))