@turmind/halo 1.6.1 → 1.6.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turmind/halo",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "Halo — multi-agent collaborative workspace. CLI + TUI + HTTP/WS server in one package.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,6 +1,6 @@
1
1
  id: deepseek
2
2
  runtime: deepseek
3
- revision: 2026101002
3
+ revision: 2026101101
4
4
  displayName: DeepSeek
5
5
  displayName_zh: DeepSeek
6
6
  description: DeepSeek V4 via OpenAI-compatible chat completions API
@@ -15,9 +15,18 @@ secrets:
15
15
  secret: true
16
16
 
17
17
  # Capabilities verified via live probes (2026-05-27, re-probed 2026-09-24).
18
- # reasoning_effort accepts { low, medium, high, max, xhigh } — bogus values
19
- # 400 with the full valid list in the error. Caching is automatic;
20
- # cached_tokens shows up from turn 2.
18
+ # Caching is automatic; cached_tokens shows up from turn 2.
19
+ #
20
+ # Thinking (re-probed 2026-10-11, api-docs.deepseek.com/guides/thinking_mode +
21
+ # GET /models): `thinking:{type:enabled|disabled}` toggles, on by default;
22
+ # `reasoning_effort` grades it. The API's real levels are low / high / max
23
+ # (/models `effort.supported_levels`, default high); it also accepts none /
24
+ # minimal / medium / xhigh / ultra and folds them itself (minimal→low,
25
+ # medium/xhigh→high, ultra→max) — all 200 live, a bogus value 422s with that
26
+ # list. So the presets are the three real levels and deepseek-agent.ts passes
27
+ # the label through unmapped: an older agent.yaml with medium / xhigh keeps
28
+ # working at high. Before 2026-10-11 the runtime sent no reasoning_effort at
29
+ # all (every agent ran at the API default, high).
21
30
  #
22
31
  # Vision is per model: `image_url` blocks are no longer rejected at the
23
32
  # deserialize layer on either id, but only `deepseek-flash` (V4.1-Flash,
@@ -42,13 +51,11 @@ models:
42
51
  - { value: 5m, label: 5min }
43
52
  thinking:
44
53
  defaultEnabled: true
45
- default: xhigh
54
+ default: high
46
55
  effortPresets:
47
56
  - { value: low, label: Low }
48
- - { value: medium, label: Medium }
49
57
  - { value: high, label: High }
50
58
  - { value: max, label: Max }
51
- - { value: xhigh, label: XHigh }
52
59
 
53
60
  # DeepSeek retired the versioned flash ids (deepseek-v4-flash,
54
61
  # deepseek-v4-flash-vision-exp) in favour of a rolling `deepseek-flash`
@@ -70,10 +77,8 @@ models:
70
77
  - { value: 5m, label: 5min }
71
78
  thinking:
72
79
  defaultEnabled: true
73
- default: xhigh
80
+ default: high
74
81
  effortPresets:
75
82
  - { value: low, label: Low }
76
- - { value: medium, label: Medium }
77
83
  - { value: high, label: High }
78
84
  - { value: max, label: Max }
79
- - { value: xhigh, label: XHigh }
@@ -1,6 +1,6 @@
1
1
  id: hunyuan
2
2
  runtime: hunyuan
3
- revision: 2026101003
3
+ revision: 2026101101
4
4
  displayName: Hunyuan (Tencent)
5
5
  displayName_zh: 混元(腾讯)
6
6
  description: Tencent Hunyuan Hy4 preview / Hy3 on TokenHub — agent-oriented MoE with no_think / think_low / think_high modes
@@ -42,14 +42,18 @@ secrets:
42
42
  # - Caching re-verified on hy3 / hy4-preview (turn 2 cached_tokens ≈ prompt).
43
43
  # - Tool calls verified on hy3 / hy4-preview. Image: hy3 / hy4-preview both
44
44
  # answered wrongly about a solid red PNG — keep image:false.
45
- # - Max output: no official per-model number found (search-only claims of
46
- # 64K, unconfirmed: https://www.tencentcloud.com/techpedia/147980); the
47
- # gateway accepted max_tokens 2,000,000 without error, so it isn't a hard
48
- # check either — maxOutputTokens stays at the conservative 16384.
45
+ # - Max output: the gateway accepted max_tokens 2,000,000 without error, so
46
+ # a rejection probe proves nothing.
47
+ # 2026-10-11: the TokenHub model list (cloud.tencent.com/document/product/1823/130051,
48
+ # updated 2026-09-28, 语言模型 table: 上下文窗口 / 最大输入 / 最大输出) gives
49
+ # Hy4 preview 1M / 960k / 64k and Hy3 256k / 192k / 128k → maxOutputTokens
50
+ # 65536 / 131072. hy3-preview is not on that page → stays at 16384. (One
51
+ # long-output run on hy3 with max_tokens 200000 stopped at 147,389 completion
52
+ # tokens, finish_reason length — above the doc figure; the doc value is used.)
49
53
  models:
50
54
  - id: hy3
51
55
  displayName: Hunyuan Hy3
52
- maxOutputTokens: 16384
56
+ maxOutputTokens: 131072
53
57
  contextWindow: 262144
54
58
  maxContextWindow: 262144
55
59
  compressAt: 0.9
@@ -77,7 +81,7 @@ models:
77
81
 
78
82
  - id: hy4-preview
79
83
  displayName: Hunyuan Hy4 Preview
80
- maxOutputTokens: 16384
84
+ maxOutputTokens: 65536
81
85
  contextWindow: 272000
82
86
  maxContextWindow: 1048576
83
87
  compressAt: 0.9
@@ -1,6 +1,6 @@
1
1
  id: qwen
2
2
  runtime: qwen
3
- revision: 2026101003
3
+ revision: 2026101101
4
4
  displayName: Qwen (Aliyun)
5
5
  displayName_zh: 通义千问(阿里云)
6
6
  description: Qwen3 family on Aliyun Bailian via the Anthropic-compatible Messages API
@@ -32,20 +32,31 @@ secrets:
32
32
  # despite docs saying a vision-specific model is required);
33
33
  # qwen3.7-max returned `Unexpected item type in content` (not
34
34
  # supported). So image:false on max, image:true on plus.
35
- # - effort presets: pure Halo-side abstraction. Same effort→budget
36
- # table as bedrock-agent.ts via qwen-agent.ts's effortToBudget().
35
+ # - effort presets on the manual (3.7 / 3.6) models: pure Halo-side
36
+ # abstraction. Same effort→budget table as bedrock-agent.ts via
37
+ # qwen-agent.ts's effortToBudget().
37
38
  # - temperature range: doc says [0, 2) instead of Anthropic's [0, 1] —
38
39
  # doesn't affect Halo since we don't expose temperature in the form.
39
40
  # 2026-10-10 census (help.aliyun.com/zh/model-studio/anthropic-api-messages +
40
41
  # the text-generation model guide, both list 3.8-max / 3.8-flash / 3.7-plus as
41
42
  # current, 1M context): live-probed on the Anthropic endpoint — manual
42
43
  # thinking.budget_tokens still accepted (the doc marks it "即将废弃" in favour
43
- # of output_config.effort; qwen-agent still sends budget_tokens), thinking
44
+ # of output_config.effort — see 2026-10-11 below), thinking
44
45
  # disabled honored, tool_use OK, image OK, cache_control ephemeral hit on
45
46
  # turn 2. `ttl: '1h'` is accepted but the entry is still written as
46
47
  # ephemeral_5m, so 5m stays the only TTL preset. Max output isn't published
47
48
  # per model on these pages — maxOutputTokens stays 16384. 3.7-max / 3.6-plus
48
49
  # are not on the deprecation page, kept below the new generation.
50
+ # 2026-10-11 (help.aliyun.com/zh/model-studio/anthropic-api-messages,
51
+ # `output_config.effort`): the doc lists effort values for qwen3.8-max /
52
+ # qwen3.8-flash only — xhigh (default) / medium / low, with max and high
53
+ # folded onto xhigh by the API. So 3.8 is `mode: adaptive`: qwen-agent sends
54
+ # thinking:{type:'enabled'} + output_config.effort. An agent.yaml that already
55
+ # carries an explicit budget_tokens keeps sending it (still accepted). For 3.8
56
+ # the same doc says max_tokens caps reply + thinking together. The 3.7 / 3.6
57
+ # models have no documented effort values → they stay `mode: manual`
58
+ # (budget_tokens). Thinking-field omitted = thinking on for every model here,
59
+ # so off is always sent as type:'disabled'.
49
60
  models:
50
61
  - id: qwen3.8-max
51
62
  displayName: Qwen3.8 Max
@@ -63,13 +74,13 @@ models:
63
74
  ttlPresets:
64
75
  - { value: 5m, label: 5min }
65
76
  thinking:
66
- mode: manual
77
+ mode: adaptive
67
78
  defaultEnabled: true
68
- default: high
69
- defaultBudgetTokens: 8192
79
+ default: xhigh
70
80
  effortPresets:
81
+ - { value: low, label: Low }
71
82
  - { value: medium, label: Medium }
72
- - { value: high, label: High }
83
+ - { value: xhigh, label: XHigh }
73
84
 
74
85
  - id: qwen3.7-plus
75
86
  displayName: Qwen3.7 Plus
@@ -111,13 +122,13 @@ models:
111
122
  ttlPresets:
112
123
  - { value: 5m, label: 5min }
113
124
  thinking:
114
- mode: manual
125
+ mode: adaptive
115
126
  defaultEnabled: true
116
- default: high
117
- defaultBudgetTokens: 8192
127
+ default: xhigh
118
128
  effortPresets:
129
+ - { value: low, label: Low }
119
130
  - { value: medium, label: Medium }
120
- - { value: high, label: High }
131
+ - { value: xhigh, label: XHigh }
121
132
 
122
133
  - id: qwen3.7-max
123
134
  displayName: Qwen3.7 Max
@@ -79,7 +79,8 @@ Sensible defaults if user didn't say:
79
79
 
80
80
  **One-shot (at-mode)** — pass `--run-at` instead of `--schedule`. The job
81
81
  fires once at that instant and auto-disables. Useful for "remind me at 3pm
82
- today" or "send the report next Monday morning".
82
+ today" or "send the report next Monday morning". On `update`, passing one
83
+ of the two switches the job's mode and clears the other; never pass both.
83
84
 
84
85
  | Spoken | Flag |
85
86
  |-----------------------------------|-------------------------------------------|
@@ -125,8 +126,9 @@ user which; don't pick for them.
125
126
  (`cron-<id>-<suffix>`), so nothing carries over between runs. Use it for
126
127
  tasks that don't need memory ("每天抓一次行情发给我", a fixed report).
127
128
  Excludes `--session`. On `update`: `--stateless on` / `--stateless off`
128
- (clear `--session ""` first if one is set). Old per-run sessions are kept and
129
- stay reviewable in the Sessions tab. `get` / `list` show
129
+ (clear `--session ""` first if one is set). Only the newest 10 runs are kept;
130
+ an older run's session is deleted with it (unless someone continued chatting
131
+ in it). Deleting the job deletes them too. `get` / `list` show
130
132
  `effective_session_id: null` for a stateless job.
131
133
 
132
134
  **Folder (`--dir`, optional)** — the run focuses on a workspace sub-folder:
@@ -96,7 +96,10 @@ def parse_targets(raw: str) -> list[dict]:
96
96
  if not raw:
97
97
  return []
98
98
  if raw.startswith('['):
99
- return json.loads(raw)
99
+ try:
100
+ return json.loads(raw)
101
+ except ValueError as err:
102
+ die(f'invalid --targets JSON: {err}')
100
103
  out = []
101
104
  for token in raw.split(','):
102
105
  token = token.strip()
@@ -127,7 +130,8 @@ def parse_timeout_sec(raw: str | None) -> int | None:
127
130
  return v
128
131
 
129
132
 
130
- SESSION_ID_RE = re.compile(r'^[A-Za-z0-9_:-]{1,200}$')
133
+ # Always fullmatch: with `$`, re.match also accepts one trailing "\n".
134
+ SESSION_ID_RE = re.compile(r'[A-Za-z0-9_:-]{1,200}')
131
135
 
132
136
 
133
137
  def parse_session(raw: str | None) -> str | None:
@@ -137,7 +141,7 @@ def parse_session(raw: str | None) -> str | None:
137
141
  if raw is None or not raw.strip():
138
142
  return None
139
143
  v = raw.strip()
140
- if not SESSION_ID_RE.match(v):
144
+ if not SESSION_ID_RE.fullmatch(v):
141
145
  die(f'invalid --session "{raw}": expected a root session id (letters, digits, _ : - only; no ">" sub-session path)')
142
146
  return v
143
147
 
@@ -393,6 +397,10 @@ def cmd_create(args):
393
397
  active_until = parse_active_bound('--active-until', args.active_until or '', args.timezone)
394
398
  validate_active_window(run_at_ms, active_from, active_until, new_until=True)
395
399
 
400
+ # A custom id becomes the job's own session id (`cron-<id>`), so it takes
401
+ # the session-id charset: a `>` would make it a sub-session path.
402
+ if args.id and not SESSION_ID_RE.fullmatch(args.id):
403
+ die(f'invalid --id "{args.id}": letters, digits, _ : - only (it becomes the job\'s session id)')
396
404
  job_id = args.id or gen_job_id()
397
405
  check_session_rules({
398
406
  'id': job_id, 'workspace_path': args.workspace, 'agent_id': args.agent or 'default',
@@ -451,12 +459,19 @@ def cmd_update(args):
451
459
  if args.workspace is not None: sets.append(('workspace_path', args.workspace))
452
460
  if args.agent is not None: sets.append(('agent_id', args.agent))
453
461
  if args.prompt is not None: sets.append(('user_prompt', args.prompt))
462
+ # Recurring vs one-shot, same as the REST PUT: both in one call is an
463
+ # error; one side alone is a mode switch that clears the other (else the
464
+ # runner prefers run_at and its post-fire auto-disable kills the schedule).
465
+ has_schedule = bool(args.schedule and args.schedule.strip())
466
+ run_at_ms = parse_run_at(args.run_at) if args.run_at is not None else None
467
+ if run_at_ms is not None and run_at_ms <= now_ms():
468
+ die('--run-at must be in the future')
469
+ if has_schedule and run_at_ms is not None:
470
+ die('--schedule and --run-at are mutually exclusive')
454
471
  if args.schedule is not None: sets.append(('schedule', args.schedule))
455
- if args.run_at is not None:
456
- run_at_ms = parse_run_at(args.run_at)
457
- if run_at_ms is not None and run_at_ms <= now_ms():
458
- die('--run-at must be in the future')
459
- sets.append(('run_at', run_at_ms))
472
+ elif run_at_ms is not None: sets.append(('schedule', ''))
473
+ if args.run_at is not None: sets.append(('run_at', run_at_ms))
474
+ elif has_schedule: sets.append(('run_at', None))
460
475
  if args.timezone is not None: sets.append(('timezone', args.timezone))
461
476
  if args.timeout_sec is not None:
462
477
  # `--timeout-sec ""` clears back to the runner default (NULL),
@@ -498,6 +513,8 @@ def cmd_update(args):
498
513
  # Window rules on the merged row (also catches --run-at on a job that
499
514
  # still has a window).
500
515
  merged = {**cur, **dict(sets)}
516
+ if not (merged.get('schedule') or '').strip() and merged.get('run_at') is None:
517
+ die('--schedule or --run-at required (this update would leave the job with neither)')
501
518
  validate_active_window(
502
519
  merged.get('run_at'), merged.get('active_from'), merged.get('active_until'),
503
520
  new_until=bool(args.active_until and args.active_until.strip()),
@@ -195,6 +195,7 @@ Common things to surface:
195
195
  - **Stale memory/**: entries older than ~3 months whose topic is no longer relevant (a decision that's been superseded, a pitfall that was fixed). Auto-detecting staleness over-reaches — the dates and titles are evidence, but the call belongs to the user.
196
196
  - **Missing index entries**: docs that exist on disk but aren't linked from INDEX.md. List them and ask whether they belong.
197
197
  - **Section bloat**: an INSTRUCTIONS.md section that's grown into a wall of mixed-priority bullets. Suggest splitting or distilling.
198
+ - **INDEX bloat**: INDEX.md is injected into every agent's prompt on every turn, so each section should stay a short paragraph (2–4 sentences + key state / code / doc links). Flag sections that carry protocol details, field lists or edge cases, and suggest moving that detail into the linked doc (create one under `docs/` if none exists) — check the detail is really there before cutting it from INDEX.
198
199
  - **Conflicting rules**: two bullets in INSTRUCTIONS.md that pull in opposite directions (e.g. "always run tests" + a later "skip tests for one-line fixes").
199
200
  - **Generic fluff** in INSTRUCTIONS.md ("write elegant code", "follow best practices") — gently flag, ask if the user wants it gone.
200
201