samagotchi 0.66.0 → 0.68.0

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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -1
  3. data/docs/architecture.md +55 -30
  4. data/docs/cli.md +3 -1
  5. data/docs/configuration.md +50 -9
  6. data/docs/context.md +35 -1
  7. data/docs/desktop.md +90 -22
  8. data/docs/guardrails.md +8 -5
  9. data/docs/hooks.md +2 -2
  10. data/docs/internals/anthropic.md +156 -0
  11. data/docs/internals/background-tasks.md +1 -1
  12. data/docs/internals/bundles.md +2 -0
  13. data/docs/internals/engine.md +129 -38
  14. data/docs/internals/gemma4-contract.md +1 -1
  15. data/docs/internals/llm-context-bench.md +5 -4
  16. data/docs/internals/prompt-caching.md +26 -11
  17. data/docs/plugins.md +126 -23
  18. data/docs/sessions.md +6 -6
  19. data/docs/sub-agent.md +4 -1
  20. data/docs/testing.md +18 -4
  21. data/lib/samagotchi/bootstrap/probe.rb +4 -3
  22. data/lib/samagotchi/bootstrap_command.rb +13 -3
  23. data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
  24. data/lib/samagotchi/bridge.rb +6 -3
  25. data/lib/samagotchi/bundles/claude-peer/manifest.yml +7 -6
  26. data/lib/samagotchi/bundles/claude-peer/plugin.rb +34 -313
  27. data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +8 -3
  28. data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +14 -6
  29. data/lib/samagotchi/bundles/codex-peer/manifest.yml +7 -6
  30. data/lib/samagotchi/bundles/codex-peer/plugin.rb +37 -312
  31. data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +8 -3
  32. data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +14 -6
  33. data/lib/samagotchi/bundles/mcp/manifest.yml +5 -5
  34. data/lib/samagotchi/bundles/mcp/plugin.rb +82 -294
  35. data/lib/samagotchi/bundles/system/config_modification_protocol.md +7 -3
  36. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  37. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  38. data/lib/samagotchi/config.rb +15 -7
  39. data/lib/samagotchi/context_absorber.rb +66 -13
  40. data/lib/samagotchi/desktop/macos/AgentTerminal.swift +260 -0
  41. data/lib/samagotchi/desktop/macos/App.swift +3 -3
  42. data/lib/samagotchi/desktop/macos/ChiRunner.swift +5 -2
  43. data/lib/samagotchi/desktop/macos/Images.swift +2 -2
  44. data/lib/samagotchi/desktop/macos/Kitty.swift +33 -197
  45. data/lib/samagotchi/desktop/macos/Panel.swift +118 -82
  46. data/lib/samagotchi/desktop/macos/ProcessRunner.swift +1 -1
  47. data/lib/samagotchi/desktop/macos/Tmux.swift +239 -0
  48. data/lib/samagotchi/desktop/macos.rb +84 -13
  49. data/lib/samagotchi/desktop_command.rb +14 -3
  50. data/lib/samagotchi/effective_model.rb +283 -0
  51. data/lib/samagotchi/engine.rb +307 -549
  52. data/lib/samagotchi/engine_peers.rb +3 -0
  53. data/lib/samagotchi/guardrail_wiring.rb +1 -1
  54. data/lib/samagotchi/guardrails/approval.rb +1 -1
  55. data/lib/samagotchi/guardrails/approvals.rb +1 -1
  56. data/lib/samagotchi/guardrails/rules.rb +1 -1
  57. data/lib/samagotchi/guardrails/targets.rb +1 -1
  58. data/lib/samagotchi/hook_routes.rb +151 -0
  59. data/lib/samagotchi/hooks/registry.rb +2 -2
  60. data/lib/samagotchi/host_keys.rb +147 -0
  61. data/lib/samagotchi/level_setting_command.rb +99 -0
  62. data/lib/samagotchi/llm/adapter_common.rb +138 -0
  63. data/lib/samagotchi/llm/anthropic_messages.rb +28 -105
  64. data/lib/samagotchi/llm/api_key.rb +23 -7
  65. data/lib/samagotchi/llm/chat_loop.rb +10 -6
  66. data/lib/samagotchi/llm/openai_chat.rb +9 -92
  67. data/lib/samagotchi/llm/refusal_memo.rb +29 -0
  68. data/lib/samagotchi/llm_context_override.rb +1 -0
  69. data/lib/samagotchi/llm_context_strategy.rb +5 -10
  70. data/lib/samagotchi/peer_sources.rb +58 -0
  71. data/lib/samagotchi/plugin/api.rb +64 -155
  72. data/lib/samagotchi/plugin/loader.rb +4 -2
  73. data/lib/samagotchi/plugin/peer.rb +394 -0
  74. data/lib/samagotchi/plugin/tool_spec.rb +166 -0
  75. data/lib/samagotchi/plugin_tasks.rb +2 -2
  76. data/lib/samagotchi/reasoning_history_command.rb +29 -90
  77. data/lib/samagotchi/row_refs.rb +1 -1
  78. data/lib/samagotchi/self_report.rb +5 -3
  79. data/lib/samagotchi/send_command.rb +21 -11
  80. data/lib/samagotchi/session.rb +22 -8
  81. data/lib/samagotchi/session_continue.rb +58 -15
  82. data/lib/samagotchi/session_inbox.rb +0 -13
  83. data/lib/samagotchi/session_setting.rb +86 -0
  84. data/lib/samagotchi/thinking_command.rb +24 -76
  85. data/lib/samagotchi/thinking_notices.rb +169 -0
  86. data/lib/samagotchi/tool_call_parser.rb +1 -1
  87. data/lib/samagotchi/toolkit_binding.rb +138 -0
  88. data/lib/samagotchi/tools/context_read.rb +14 -0
  89. data/lib/samagotchi/tools/execute.rb +12 -12
  90. data/lib/samagotchi/tools/registry.rb +1 -1
  91. data/lib/samagotchi/tools/task_create.rb +6 -6
  92. data/lib/samagotchi/tools/task_get.rb +13 -13
  93. data/lib/samagotchi/tools/task_list.rb +6 -6
  94. data/lib/samagotchi/tools/task_record.rb +52 -0
  95. data/lib/samagotchi/tools/task_runtime.rb +55 -91
  96. data/lib/samagotchi/tools/task_stop.rb +6 -6
  97. data/lib/samagotchi/tools/task_wait.rb +12 -13
  98. data/lib/samagotchi/tools/tool_index.rb +454 -0
  99. data/lib/samagotchi/update_command.rb +9 -6
  100. data/lib/samagotchi/version.rb +1 -1
  101. data/lib/samagotchi/web/app.rb +71 -93
  102. data/lib/samagotchi/web/capabilities.rb +1 -1
  103. data/lib/samagotchi/web/public/activity.js +2 -2
  104. data/lib/samagotchi/web/public/annotate_actions.js +1 -1
  105. data/lib/samagotchi/web/public/app.js +103 -539
  106. data/lib/samagotchi/web/public/bubbles.js +291 -0
  107. data/lib/samagotchi/web/public/cards_place.js +184 -0
  108. data/lib/samagotchi/web/public/chains.js +1 -1
  109. data/lib/samagotchi/web/public/chip_popover.js +111 -0
  110. data/lib/samagotchi/web/public/command_list.js +12 -10
  111. data/lib/samagotchi/web/public/commands.js +2 -2
  112. data/lib/samagotchi/web/public/composer_size.js +2 -2
  113. data/lib/samagotchi/web/public/copy.js +1 -1
  114. data/lib/samagotchi/web/public/ctx.js +1 -1
  115. data/lib/samagotchi/web/public/delivery_line.js +4 -4
  116. data/lib/samagotchi/web/public/families_view.js +1 -1
  117. data/lib/samagotchi/web/public/format.js +11 -7
  118. data/lib/samagotchi/web/public/images.js +25 -2
  119. data/lib/samagotchi/web/public/index.html +15 -7
  120. data/lib/samagotchi/web/public/info_bar.js +3 -3
  121. data/lib/samagotchi/web/public/info_bar_model.js +110 -0
  122. data/lib/samagotchi/web/public/init_row.js +21 -10
  123. data/lib/samagotchi/web/public/level_chip.js +29 -80
  124. data/lib/samagotchi/web/public/lists_view.js +64 -9
  125. data/lib/samagotchi/web/public/llm_context_chip.js +38 -107
  126. data/lib/samagotchi/web/public/model_picker.js +26 -22
  127. data/lib/samagotchi/web/public/notify.js +6 -4
  128. data/lib/samagotchi/web/public/question_card.js +1 -1
  129. data/lib/samagotchi/web/public/ref_scan.js +2 -8
  130. data/lib/samagotchi/web/public/refs.js +13 -0
  131. data/lib/samagotchi/web/public/session_state.js +14 -0
  132. data/lib/samagotchi/web/public/session_title.js +2 -3
  133. data/lib/samagotchi/web/public/sessions_list.js +0 -8
  134. data/lib/samagotchi/web/public/stage_frame.js +179 -0
  135. data/lib/samagotchi/web/public/stage_model.js +35 -2
  136. data/lib/samagotchi/web/public/stage_slots.js +130 -0
  137. data/lib/samagotchi/web/public/stage_view.js +56 -335
  138. data/lib/samagotchi/web/public/stage_watch.js +65 -0
  139. data/lib/samagotchi/web/public/strip.js +3 -3
  140. data/lib/samagotchi/web/public/task_stop.js +3 -2
  141. data/lib/samagotchi/web/public/turn_events.js +1 -1
  142. data/lib/samagotchi/web/public/turn_html.js +2 -2
  143. data/lib/samagotchi/web/public/turn_model.js +2 -2
  144. data/lib/samagotchi/web/public/turn_view.js +1 -1
  145. data/lib/samagotchi/web/public/vendor/directive-helpers.js +8 -0
  146. data/lib/samagotchi/web/public/vendor/directives/repeat.js +9 -0
  147. data/lib/samagotchi/web/server.rb +0 -4
  148. data/lib/samagotchi/worker.rb +22 -24
  149. data/lib/samagotchi/worker_inbound.rb +36 -4
  150. data/lib/samagotchi/worker_wakes.rb +67 -7
  151. metadata +27 -1
@@ -8,7 +8,7 @@ import { html, nothing, render } from "./vendor/lit-html.js";
8
8
  import { commandBlockTemplate, rowHover } from "./command_view.js";
9
9
  import { copySource } from "./copy.js";
10
10
  import { activityDiffTemplate } from "./diff_view.js";
11
- import { steerSender } from "./format.js";
11
+ import { clip, steerSender } from "./format.js";
12
12
  import { rowTitleRef } from "./refs.js";
13
13
  import { readLinesTemplate } from "./read_view.js";
14
14
  import { STOPPED_LINE_CLASS, cancelLineText, formatDuration } from "./timing.js";
@@ -105,7 +105,7 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
105
105
  const duration = formatDuration(row.duration_ms);
106
106
  const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
107
107
  const output = full
108
- ? html`<div class="activity-output" title=${full}>${full.length > 300 ? `${full.slice(0, 300)}…` : full}</div>`
108
+ ? html`<div class="activity-output" title=${full}>${clip(full, 301)}</div>`
109
109
  : nothing;
110
110
  const status = statusParts(row.status, { noMatch: !!row.no_match });
111
111
  return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
@@ -25,7 +25,7 @@
25
25
  // - the turn's end closes the current gen.
26
26
  import { addCompleted, addStarted, newActivity, toolName } from "./activity.js";
27
27
  import { tallyText } from "./tally.js";
28
- import { leadTrimmed } from "./format.js";
28
+ import { clip, leadTrimmed } from "./format.js";
29
29
 
30
30
  const LABEL_MAX = 80;
31
31
  const LABEL_TOOLS = 3;
@@ -194,7 +194,7 @@ function isStep(gen) {
194
194
  }
195
195
 
196
196
  function cut(line) {
197
- return line.length > LABEL_MAX ? `${line.slice(0, LABEL_MAX).trimEnd()}…` : line;
197
+ return clip(line, LABEL_MAX + 1);
198
198
  }
199
199
 
200
200
  // "3 tool calls (1 failed)": failed = status error, the tally's predicate
@@ -58,7 +58,7 @@ function closedLabel(gen) {
58
58
  // that would repeat "thinking"). +body+ is the text: a string, or the live
59
59
  // step's Text node (appended by delta: a selection in it survives).
60
60
  // +cls+ "bubble thinking" is a plain answer's collapsed thinking block.
61
- export function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking", on = {} }) {
61
+ function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking", on = {} }) {
62
62
  const inner = html`<div class="thinking-body">${body}</div>`;
63
63
  if (!wrapped) return inner;
64
64
  const summary = line === null ? "thinking" : html`<span class="thinking-tag">thinking · </span>${line}`;
@@ -0,0 +1,8 @@
1
+ // lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, directive-helpers.js: the minified production build,
2
+ // unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
3
+ import{_$LH as o}from"./lit-html.js";
4
+ /**
5
+ * @license
6
+ * Copyright 2020 Google LLC
7
+ * SPDX-License-Identifier: BSD-3-Clause
8
+ */const{I:t}=o,i=o=>o,n=o=>null===o||"object"!=typeof o&&"function"!=typeof o,e={HTML:1,SVG:2,MATHML:3},l=(o,t)=>void 0===t?void 0!==o?._$litType$:o?._$litType$===t,d=o=>null!=o?._$litType$?.h,c=o=>void 0!==o?._$litDirective$,f=o=>o?._$litDirective$,r=o=>void 0===o.strings,s=()=>document.createComment(""),v=(o,n,e)=>{const l=o._$AA.parentNode,d=void 0===n?o._$AB:n._$AA;if(void 0===e){const i=l.insertBefore(s(),d),n=l.insertBefore(s(),d);e=new t(i,n,o,o.options)}else{const t=e._$AB.nextSibling,n=e._$AM,c=n!==o;if(c){let t;e._$AQ?.(o),e._$AM=o,void 0!==e._$AP&&(t=o._$AU)!==n._$AU&&e._$AP(t)}if(t!==d||c){let o=e._$AA;for(;o!==t;){const t=i(o).nextSibling;i(l).insertBefore(o,d),o=t}}}return e},u=(o,t,i=o)=>(o._$AI(t,i),o),m={},p=(o,t=m)=>o._$AH=t,M=o=>o._$AH,h=o=>{o._$AR(),o._$AA.remove()},j=o=>{o._$AR()};export{e as TemplateResultType,j as clearPart,M as getCommittedValue,f as getDirectiveClass,v as insertPart,d as isCompiledTemplateResult,c as isDirectiveResult,n as isPrimitive,r as isSingleExpression,l as isTemplateResult,h as removePart,u as setChildPartValue,p as setCommittedValue};
@@ -0,0 +1,9 @@
1
+ // lit-html 3.3.3 (npm tarball lit-html-3.3.3.tgz, directives/repeat.js: the minified production build,
2
+ // unmodified but for this line and the dropped sourceMappingURL). BSD-3-Clause.
3
+ import{noChange as e}from"../lit-html.js";import{directive as s,Directive as t,PartType as r}from"../directive.js";import{getCommittedValue as l,setChildPartValue as o,insertPart as i,removePart as n,setCommittedValue as f}from"../directive-helpers.js";
4
+ /**
5
+ * @license
6
+ * Copyright 2017 Google LLC
7
+ * SPDX-License-Identifier: BSD-3-Clause
8
+ */
9
+ const u=(e,s,t)=>{const r=new Map;for(let l=s;l<=t;l++)r.set(e[l],l);return r},c=s(class extends t{constructor(e){if(super(e),e.type!==r.CHILD)throw Error("repeat() can only be used in text expressions")}dt(e,s,t){let r;void 0===t?t=s:void 0!==s&&(r=s);const l=[],o=[];let i=0;for(const s of e)l[i]=r?r(s,i):i,o[i]=t(s,i),i++;return{values:o,keys:l}}render(e,s,t){return this.dt(e,s,t).values}update(s,[t,r,c]){const d=l(s),{values:p,keys:a}=this.dt(t,r,c);if(!Array.isArray(d))return this.ut=a,p;const h=this.ut??=[],v=[];let m,y,x=0,j=d.length-1,k=0,w=p.length-1;for(;x<=j&&k<=w;)if(null===d[x])x++;else if(null===d[j])j--;else if(h[x]===a[k])v[k]=o(d[x],p[k]),x++,k++;else if(h[j]===a[w])v[w]=o(d[j],p[w]),j--,w--;else if(h[x]===a[w])v[w]=o(d[x],p[w]),i(s,v[w+1],d[x]),x++,w--;else if(h[j]===a[k])v[k]=o(d[j],p[k]),i(s,d[x],d[j]),j--,k++;else if(void 0===m&&(m=u(a,k,w),y=u(h,x,j)),m.has(h[x]))if(m.has(h[j])){const e=y.get(a[k]),t=void 0!==e?d[e]:null;if(null===t){const e=i(s,d[x]);o(e,p[k]),v[k]=e}else v[k]=o(t,p[k]),i(s,d[x],t),d[e]=null;k++}else n(d[j]),j--;else n(d[x]),x++;for(;k<=w;){const e=i(s,v[w+1]);o(e,p[k]),v[k++]=e}for(;x<=j;){const e=d[x++];null!==e&&n(e)}return this.ut=a,f(s,v),e}});export{c as repeat};
@@ -241,10 +241,6 @@ module Samagotchi
241
241
  knows_dir(InfoProbe.call(host, port, timeout: timeout))
242
242
  end
243
243
 
244
- def self.probe_verdict(status, body)
245
- knows_dir(InfoProbe.verdict(status, body))
246
- end
247
-
248
244
  def self.knows_dir(info)
249
245
  info.is_a?(Hash) && !Array(info["features"]).include?("dir") ? :other : info
250
246
  end
@@ -22,6 +22,7 @@ require_relative "llm/errors"
22
22
  require_relative "continue_offer"
23
23
  require_relative "iteration_limit"
24
24
  require_relative "session_commands"
25
+ require_relative "session_setting"
25
26
  require_relative "queued_command"
26
27
  require_relative "model_profile"
27
28
  require_relative "child_reports"
@@ -153,9 +154,6 @@ module Samagotchi
153
154
  # The running turn has ended (its end event) and its after_turn hooks
154
155
  # run: a signal's grace waits for them (#signalled).
155
156
  @turn_ending = false
156
- # The worker's own saves and a signal's (#signal_grace_expired), one
157
- # at a time.
158
- @save_mutex = Mutex.new
159
157
  end
160
158
 
161
159
  # Whether the session was empty as the worker left it, so the caller
@@ -218,7 +216,7 @@ module Samagotchi
218
216
  # The session, its Engine and what runs its turns, before anyone can
219
217
  # reach it.
220
218
  def start
221
- # The wake turns' budget (delegate reports, attached context), from now.
219
+ # The wake turns' budget (delegate reports, attached context, peers), from now.
222
220
  @wakes = WorkerWakes.new(grace: WAKE_START_GRACE)
223
221
  # This worker holds the session now (a continue's starting marker held
224
222
  # off any other until it did), and a continue that died moving
@@ -272,7 +270,8 @@ module Samagotchi
272
270
  predecessor: @session.continues)
273
271
  # Its attached context's notes (ContextSources).
274
272
  @context_absorber = ContextAbsorber.new(session_id: @session_id, state_dir: @state_dir,
275
- project_root: @session.project_root)
273
+ project_root: @session.project_root,
274
+ peer_sources: @engine.peer_sources, peer_turns: -> { @wakes.peer_turns })
276
275
  # What comes in between turns, and how each kind is saved.
277
276
  @inbound = WorkerInbound.new(session: @session, state_dir: @state_dir, session_dir: @session_dir, engine: @engine,
278
277
  context_absorber: @context_absorber, wakes: @wakes,
@@ -459,7 +458,7 @@ module Samagotchi
459
458
  return unless mark
460
459
 
461
460
  Log.info(:worker, "interrupted_turn_settled", turn_id: mark.id, iteration: mark.iteration, at: mark.at)
462
- save_or_log(:interrupted_turn) { @session.save(state_dir: @state_dir) }
461
+ save_or_log(:interrupted_turn) { save_session }
463
462
  end
464
463
 
465
464
  # A question saved by a worker that died while it waited (an
@@ -472,7 +471,7 @@ module Samagotchi
472
471
  return unless id
473
472
 
474
473
  Log.info(:worker, "dead_question_dropped", id: id)
475
- save_or_log(:dead_question) { @session.save(state_dir: @state_dir) }
474
+ save_or_log(:dead_question) { save_session }
476
475
  end
477
476
 
478
477
  # The attached context's notes (WorkerInbound#absorb_context), and the
@@ -496,13 +495,14 @@ module Samagotchi
496
495
  # A turn nobody typed for a changed source (a context wake): a continue
497
496
  # turn from context:<name> whose first message is the wake note already
498
497
  # in the conversation (named by +turn_id+), on the same rules as a
499
- # delegate report's wake turn: it counts toward session.max_wakes, and
500
- # a failure goes back to before the turn (the note stays, as a plain
501
- # one: WorkerInbound#unmark_wake) and pauses wakes until a human's input.
498
+ # delegate report's wake turn: it counts toward session.max_wakes (a
499
+ # peer's answer toward context.peer_turns too), and a failure goes back
500
+ # to before the turn (the note stays, as a plain one:
501
+ # WorkerInbound#unmark_wake) and pauses wakes until a human's input.
502
502
  # +context_wake+: a WorkerInbound::ContextWake.
503
503
  def run_context_wake_turn(context_wake)
504
504
  name = context_wake.name
505
- Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes.count!)
505
+ Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes.count!(peer: context_wake.peer?))
506
506
  @turn_flow.before_prompt_turn
507
507
  run_engine_turn(nil, continue: true, origin: context_wake.origin,
508
508
  id: context_wake.turn_id, max_iterations: IterationLimit.for) do |result, error|
@@ -871,15 +871,10 @@ module Samagotchi
871
871
  event[:anytime] = true if anytime
872
872
  # Its line was shown at its command_queued (waits: turn_end).
873
873
  event[:queued] = true if command.waits_for_turn_end?
874
- # /llm-context or /model: what the next turn runs under now (the web's chip).
875
- event[:llm_context] = @engine.llm_context_summary if changed.intersect?(%i[llm_context model])
876
- # /thinking or /model: the level the next turn runs at (the web's chip).
877
- event[:thinking] = @engine.thinking_explained&.summary if changed.intersect?(%i[thinking model])
878
- # /reasoning-history or /model: the level the next turn runs under
879
- # (the web's chip); nil on a native host, which ignores it.
880
- event[:reasoning_history] = @engine.reasoning_history_explained&.summary if changed.intersect?(%i[reasoning_history model])
881
- # /toolkit: what the next turn declares (the web's chip; nil on full).
882
- event[:toolkit] = @engine.toolkit_summary if changed.include?(:toolkit)
874
+ # The chips the command moved (SessionSetting#changed_by): what the next
875
+ # turn runs under now (/llm-context, /thinking, /reasoning-history,
876
+ # /toolkit, and /model for the model's).
877
+ SessionSetting::ALL.each { |setting| event[setting.key] = setting.summary(@engine) if setting.changed_by?(changed) }
883
878
  event[:confirm] = confirm.to_h if confirm
884
879
  event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
885
880
  @engine.announce(event)
@@ -908,7 +903,7 @@ module Samagotchi
908
903
  # the new parent beside it (Session::PARENT_FILE): this turn's
909
904
  # status, its rings and every save from here on say so.
910
905
  @session.parent_id = Session.parent_override(@session_id, state_dir: @state_dir) || @session.parent_id
911
- @session.save(state_dir: @state_dir)
906
+ save_session
912
907
  result = @engine.run_turn(@session, prompt, pending_input: pending_input_drain, **turn_args)
913
908
  rescue StandardError => e
914
909
  error = e
@@ -1220,12 +1215,13 @@ module Samagotchi
1220
1215
 
1221
1216
  # A human's input (ClientId.human?) un-archives the session;
1222
1217
  # a delegate's, a plugin's or a reminder's doesn't.
1223
- # It also resets the delegate-report wakes (budget and pause).
1218
+ # It also resets the wakes (budget and pause); input typed in the web or
1219
+ # a TUI the peer wakes too (chi send doesn't: a peer can run it).
1224
1220
  def user_input(client_id)
1225
1221
  return unless ClientId.human?(client_id)
1226
1222
 
1227
1223
  ArchiveStore.user_input(@session_id, state_dir: @state_dir)
1228
- @wakes.human_input!
1224
+ @wakes.human_input!(interactive: ClientId.interactive?(client_id))
1229
1225
  end
1230
1226
 
1231
1227
  def stopped_on_disk?
@@ -1235,8 +1231,10 @@ module Samagotchi
1235
1231
  # Save the session, a stopped one too: a stop is the marker file
1236
1232
  # (Session.mark_stopped), which every load reads over the saved status,
1237
1233
  # so this save can't hide it, and the turn the stop canceled is kept.
1234
+ # Session#save serializes it with every other save of @session (Bridge
1235
+ # threads, the turn's checkpoints, a signal's thread).
1238
1236
  def save_session
1239
- @save_mutex.synchronize { @session.save(state_dir: @state_dir) }
1237
+ @session.save(state_dir: @state_dir)
1240
1238
  end
1241
1239
 
1242
1240
  def signalled? = !@signals.signal.nil?
@@ -4,12 +4,14 @@ require "fileutils"
4
4
  require "securerandom"
5
5
  require "time"
6
6
 
7
+ require_relative "child_reports"
7
8
  require_relative "client_id"
8
9
  require_relative "config"
9
10
  require_relative "context_note"
10
11
  require_relative "log"
11
12
  require_relative "session"
12
13
  require_relative "session_inbox"
14
+ require_relative "worker_wakes"
13
15
 
14
16
  module Samagotchi
15
17
  # What comes into a worker's session between turns, each kind with its
@@ -43,6 +45,8 @@ module Samagotchi
43
45
  # the turn +turn_id+.
44
46
  ContextWake = Data.define(:delivery, :turn_id) do
45
47
  def name = delivery.name
48
+ # For a peer agent's answer (its own budget, WorkerWakes#peer_open?).
49
+ def peer? = !delivery.peer.nil?
46
50
  def origin = { client_id: "#{ClientId::CONTEXT_PREFIX}#{name}" }
47
51
  end
48
52
 
@@ -230,14 +234,42 @@ module Samagotchi
230
234
  # context.wake is on, the wake budget is open (WorkerWakes#context_open?:
231
235
  # no continue offer, not paused, under session.max_wakes, past the start
232
236
  # grace) and the source hasn't woken the session in the last
233
- # CONTEXT_WAKE_WINDOW. The others stay plain notes. Within the start
234
- # grace a change is a note: it came while the session was away.
237
+ # CONTEXT_WAKE_WINDOW; a peer's answer (Delivery#peer) has no window but
238
+ # the peer budget (WorkerWakes#peer_open?). The others stay plain notes.
239
+ # Within the start grace a change is a note: it came while the session
240
+ # was away. A peer's answer held by either budget is announced once.
235
241
  def context_wake_for(batch, now: Time.now)
236
242
  candidates = batch.deliveries.select(&:wake_note)
237
243
  return nil if candidates.empty? || !context_wakes_on?
238
- return nil unless @wakes.context_open?(awaiting_continue: @awaiting_continue.call, names: candidates.map(&:name))
239
244
 
240
- candidates.find { |delivery| !woke_lately?(delivery.subscription, now) }
245
+ unless @wakes.context_open?(awaiting_continue: @awaiting_continue.call, names: candidates.map(&:name))
246
+ peer_held(candidates.find(&:peer), ChildRing::MAX_WAKES_KEY) if @wakes.spent?
247
+ return nil
248
+ end
249
+
250
+ held = []
251
+ waking = candidates.select do |delivery|
252
+ next !woke_lately?(delivery.subscription, now) unless delivery.peer
253
+
254
+ next true if @wakes.peer_open?(name: delivery.name)
255
+
256
+ held << delivery
257
+ false
258
+ end
259
+ peer_held(held.first, WorkerWakes::PEER_TURNS_KEY)
260
+ waking.first
261
+ end
262
+
263
+ # A peer's answer waits as a note: the UIs hear it once per
264
+ # interactive input (WorkerWakes#notice_peer_budget!). +key+: the
265
+ # budget that holds it.
266
+ def peer_held(delivery, key)
267
+ return unless delivery && @wakes.notice_peer_budget!
268
+
269
+ turns = key == ChildRing::MAX_WAKES_KEY ? @wakes.max : @wakes.peer_turns
270
+ @engine.announce(type: :hook_notice, hook: "context", level: :info, between_turns: true,
271
+ text: "#{delivery.peer} answered again; it waits as a note for your next message " \
272
+ "(#{turns} wake turn#{"s" if turns != 1} in a row, #{key})")
241
273
  end
242
274
 
243
275
  def context_wakes_on? = Config.get("context.wake") != false
@@ -13,23 +13,36 @@ module Samagotchi
13
13
  # it what happened. It also holds the reports a delegate wake turn was
14
14
  # started for until the turn's first boundary takes them (#hand_over,
15
15
  # #take_handed).
16
+ #
17
+ # A peer agent's answers (PeerSources) have a budget of their own on top:
18
+ # context.peer_turns wake turns in a row, reset only by input typed in the
19
+ # web or a TUI (ClientId.interactive?), not by chi send, which a peer can
20
+ # run (docs/context.md, Waking).
16
21
  class WorkerWakes
22
+ PEER_TURNS_KEY = "context.peer_turns"
23
+ PEER_TURNS_DEFAULT = 3
24
+
17
25
  # @param grace [Numeric] seconds after the start with no wake turn
18
26
  # @param clock [#call] monotonic seconds
19
27
  # @param max [#call, nil] the budget; nil reads session.max_wakes
20
- def initialize(grace:, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, max: nil)
28
+ # @param peer_max [#call, nil] the peer budget; nil reads context.peer_turns
29
+ def initialize(grace:, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }, max: nil, peer_max: nil)
21
30
  @grace = grace
22
31
  @clock = clock
23
32
  @max = max
33
+ @peer_max = peer_max
24
34
  @started_at = clock.call
25
35
  @in_a_row = 0
36
+ @peer_in_a_row = 0
26
37
  @paused = false
27
38
  @budget_noticed = false
39
+ @peer_noticed = false
28
40
  @handed = nil
29
41
  end
30
42
 
31
- # Wake turns run since the last human input.
32
- attr_reader :in_a_row
43
+ # Wake turns run since the last human input; peer wake turns since the
44
+ # last interactive one.
45
+ attr_reader :in_a_row, :peer_in_a_row
33
46
 
34
47
  def grace_left = @grace - (@clock.call - @started_at)
35
48
 
@@ -44,6 +57,35 @@ module Samagotchi
44
57
  ChildRing::MAX_WAKES_DEFAULT
45
58
  end
46
59
 
60
+ # context.peer_turns: 0 (or less) is 0, the peer policy off.
61
+ def peer_max
62
+ return @peer_max.call if @peer_max
63
+
64
+ value = Integer(Config.get(PEER_TURNS_KEY), exception: false)
65
+ value ? [value, 0].max : PEER_TURNS_DEFAULT
66
+ rescue StandardError
67
+ PEER_TURNS_DEFAULT
68
+ end
69
+
70
+ # The peer wake turns that may run in a row: context.peer_turns, or
71
+ # session.max_wakes when it is lower; 0 when the peer policy is off.
72
+ def peer_turns
73
+ peers = peer_max
74
+ peers.zero? ? 0 : [peers, max].min
75
+ end
76
+
77
+ # Whether session.max_wakes holds wake turns now.
78
+ def spent? = @in_a_row >= max
79
+
80
+ # Whether a peer's answer may start a wake turn by the peer budget
81
+ # (#context_open? asked first). +name+: the source, for the log line.
82
+ def peer_open?(name:)
83
+ return true if @peer_in_a_row < peer_turns
84
+
85
+ Log.info(:worker, "context_wake_held", reason: "peer_turns", names: name)
86
+ false
87
+ end
88
+
47
89
  # For delegate reports: :due when a wake turn may run now (the block
48
90
  # says reports wait; asked only when nothing else holds), :budget when
49
91
  # only the budget stops it, nil otherwise. The start grace is the
@@ -68,8 +110,12 @@ module Samagotchi
68
110
  true
69
111
  end
70
112
 
71
- # A wake turn starts. @return [Integer] the wake turns in a row now
72
- def count! = @in_a_row += 1
113
+ # A wake turn starts; +peer+: for a peer's answer, which counts toward
114
+ # both budgets. @return [Integer] the wake turns in a row now
115
+ def count!(peer: false)
116
+ @peer_in_a_row += 1 if peer
117
+ @in_a_row += 1
118
+ end
73
119
 
74
120
  # A wake turn failed (or was stopped before it got anywhere): none
75
121
  # starts again until a human's input.
@@ -77,11 +123,17 @@ module Samagotchi
77
123
  @paused = true
78
124
  end
79
125
 
80
- # A human's input resets the budget, the pause and the budget notice.
81
- def human_input!
126
+ # A human's input resets the budget, the pause and the budget notice;
127
+ # an +interactive+ one (ClientId.interactive?: the web, a TUI) the peer
128
+ # budget and its notice too. chi send isn't: a peer can run it.
129
+ def human_input!(interactive: true)
82
130
  @in_a_row = 0
83
131
  @paused = false
84
132
  @budget_noticed = false
133
+ return unless interactive
134
+
135
+ @peer_in_a_row = 0
136
+ @peer_noticed = false
85
137
  end
86
138
 
87
139
  # @return [Boolean] true the first time the budget is spent since the
@@ -92,6 +144,14 @@ module Samagotchi
92
144
  @budget_noticed = true
93
145
  end
94
146
 
147
+ # @return [Boolean] true the first time a peer's answer is held since
148
+ # the last interactive input: the UIs hear it once
149
+ def notice_peer_budget!
150
+ return false if @peer_noticed
151
+
152
+ @peer_noticed = true
153
+ end
154
+
95
155
  # The idle loop's sleep: +poll+, or less while a wake (the block says
96
156
  # one is due) waits out the start grace.
97
157
  def idle_wait(poll)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: samagotchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.66.0
4
+ version: 0.68.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -130,6 +130,7 @@ files:
130
130
  - docs/desktop.md
131
131
  - docs/guardrails.md
132
132
  - docs/hooks.md
133
+ - docs/internals/anthropic.md
133
134
  - docs/internals/background-tasks.md
134
135
  - docs/internals/bundles.md
135
136
  - docs/internals/context-telemetry.md
@@ -260,6 +261,7 @@ files:
260
261
  - lib/samagotchi/delivery.rb
261
262
  - lib/samagotchi/desktop.rb
262
263
  - lib/samagotchi/desktop/macos.rb
264
+ - lib/samagotchi/desktop/macos/AgentTerminal.swift
263
265
  - lib/samagotchi/desktop/macos/App.swift
264
266
  - lib/samagotchi/desktop/macos/Broadcast.swift
265
267
  - lib/samagotchi/desktop/macos/ChiRunner.swift
@@ -270,8 +272,10 @@ files:
270
272
  - lib/samagotchi/desktop/macos/Models.swift
271
273
  - lib/samagotchi/desktop/macos/Panel.swift
272
274
  - lib/samagotchi/desktop/macos/ProcessRunner.swift
275
+ - lib/samagotchi/desktop/macos/Tmux.swift
273
276
  - lib/samagotchi/desktop_command.rb
274
277
  - lib/samagotchi/edit_preview.rb
278
+ - lib/samagotchi/effective_model.rb
275
279
  - lib/samagotchi/empty_answer_retry.rb
276
280
  - lib/samagotchi/engine.rb
277
281
  - lib/samagotchi/engine_peers.rb
@@ -304,12 +308,14 @@ files:
304
308
  - lib/samagotchi/guardrails/shell_paths.rb
305
309
  - lib/samagotchi/guardrails/targets.rb
306
310
  - lib/samagotchi/guardrails/verdict.rb
311
+ - lib/samagotchi/hook_routes.rb
307
312
  - lib/samagotchi/hooks.rb
308
313
  - lib/samagotchi/hooks/bundle_loader.rb
309
314
  - lib/samagotchi/hooks/loader.rb
310
315
  - lib/samagotchi/hooks/registry.rb
311
316
  - lib/samagotchi/hooks/stream_watch.rb
312
317
  - lib/samagotchi/host_config.rb
318
+ - lib/samagotchi/host_keys.rb
313
319
  - lib/samagotchi/host_model.rb
314
320
  - lib/samagotchi/host_registry.rb
315
321
  - lib/samagotchi/idle_client.rb
@@ -325,7 +331,9 @@ files:
325
331
  - lib/samagotchi/kernel_loop.rb
326
332
  - lib/samagotchi/last_turn.rb
327
333
  - lib/samagotchi/launch_mode.rb
334
+ - lib/samagotchi/level_setting_command.rb
328
335
  - lib/samagotchi/live_versions.rb
336
+ - lib/samagotchi/llm/adapter_common.rb
329
337
  - lib/samagotchi/llm/adapters.rb
330
338
  - lib/samagotchi/llm/anthropic_assembly.rb
331
339
  - lib/samagotchi/llm/anthropic_messages.rb
@@ -342,6 +350,7 @@ files:
342
350
  - lib/samagotchi/llm/native_tool_normalizer.rb
343
351
  - lib/samagotchi/llm/openai_chat.rb
344
352
  - lib/samagotchi/llm/prompt_cache.rb
353
+ - lib/samagotchi/llm/refusal_memo.rb
345
354
  - lib/samagotchi/llm/stream_drops.rb
346
355
  - lib/samagotchi/llm/turn_settings.rb
347
356
  - lib/samagotchi/llm/usage.rb
@@ -399,15 +408,18 @@ files:
399
408
  - lib/samagotchi/owner_lock.rb
400
409
  - lib/samagotchi/parent_report.rb
401
410
  - lib/samagotchi/paths.rb
411
+ - lib/samagotchi/peer_sources.rb
402
412
  - lib/samagotchi/pending_input_queue.rb
403
413
  - lib/samagotchi/plugin/api.rb
404
414
  - lib/samagotchi/plugin/attached_context.rb
405
415
  - lib/samagotchi/plugin/context.rb
406
416
  - lib/samagotchi/plugin/loader.rb
417
+ - lib/samagotchi/plugin/peer.rb
407
418
  - lib/samagotchi/plugin/service.rb
408
419
  - lib/samagotchi/plugin/sessions.rb
409
420
  - lib/samagotchi/plugin/side_question.rb
410
421
  - lib/samagotchi/plugin/tool_result.rb
422
+ - lib/samagotchi/plugin/tool_spec.rb
411
423
  - lib/samagotchi/plugin_session_state.rb
412
424
  - lib/samagotchi/plugin_tasks.rb
413
425
  - lib/samagotchi/process_group.rb
@@ -449,6 +461,7 @@ files:
449
461
  - lib/samagotchi/session_metrics.rb
450
462
  - lib/samagotchi/session_observer.rb
451
463
  - lib/samagotchi/session_retention.rb
464
+ - lib/samagotchi/session_setting.rb
452
465
  - lib/samagotchi/session_setup.rb
453
466
  - lib/samagotchi/sessions_command.rb
454
467
  - lib/samagotchi/setting_explained.rb
@@ -482,6 +495,7 @@ files:
482
495
  - lib/samagotchi/text_diff.rb
483
496
  - lib/samagotchi/thinking.rb
484
497
  - lib/samagotchi/thinking_command.rb
498
+ - lib/samagotchi/thinking_notices.rb
485
499
  - lib/samagotchi/thinking_tails.rb
486
500
  - lib/samagotchi/thinking_ticker.rb
487
501
  - lib/samagotchi/thought_stream_splitter.rb
@@ -495,6 +509,7 @@ files:
495
509
  - lib/samagotchi/tool_runner.rb
496
510
  - lib/samagotchi/tool_view.rb
497
511
  - lib/samagotchi/toolkit.rb
512
+ - lib/samagotchi/toolkit_binding.rb
498
513
  - lib/samagotchi/toolkit_command.rb
499
514
  - lib/samagotchi/tools/args.rb
500
515
  - lib/samagotchi/tools/ask_user_question.rb
@@ -523,9 +538,11 @@ files:
523
538
  - lib/samagotchi/tools/task_create.rb
524
539
  - lib/samagotchi/tools/task_get.rb
525
540
  - lib/samagotchi/tools/task_list.rb
541
+ - lib/samagotchi/tools/task_record.rb
526
542
  - lib/samagotchi/tools/task_runtime.rb
527
543
  - lib/samagotchi/tools/task_stop.rb
528
544
  - lib/samagotchi/tools/task_wait.rb
545
+ - lib/samagotchi/tools/tool_index.rb
529
546
  - lib/samagotchi/tools/tool_path.rb
530
547
  - lib/samagotchi/tools/web_fetch.rb
531
548
  - lib/samagotchi/tools/write.rb
@@ -556,8 +573,11 @@ files:
556
573
  - lib/samagotchi/web/public/annotations.js
557
574
  - lib/samagotchi/web/public/answer_refs.js
558
575
  - lib/samagotchi/web/public/app.js
576
+ - lib/samagotchi/web/public/bubbles.js
559
577
  - lib/samagotchi/web/public/card.js
578
+ - lib/samagotchi/web/public/cards_place.js
560
579
  - lib/samagotchi/web/public/chains.js
580
+ - lib/samagotchi/web/public/chip_popover.js
561
581
  - lib/samagotchi/web/public/chunk_router.js
562
582
  - lib/samagotchi/web/public/command_complete.js
563
583
  - lib/samagotchi/web/public/command_list.js
@@ -578,6 +598,7 @@ files:
578
598
  - lib/samagotchi/web/public/images.js
579
599
  - lib/samagotchi/web/public/index.html
580
600
  - lib/samagotchi/web/public/info_bar.js
601
+ - lib/samagotchi/web/public/info_bar_model.js
581
602
  - lib/samagotchi/web/public/init_row.js
582
603
  - lib/samagotchi/web/public/keep_focus.js
583
604
  - lib/samagotchi/web/public/kept_commands.js
@@ -607,8 +628,11 @@ files:
607
628
  - lib/samagotchi/web/public/session_title.js
608
629
  - lib/samagotchi/web/public/sessions_list.js
609
630
  - lib/samagotchi/web/public/shown_texts.js
631
+ - lib/samagotchi/web/public/stage_frame.js
610
632
  - lib/samagotchi/web/public/stage_model.js
633
+ - lib/samagotchi/web/public/stage_slots.js
611
634
  - lib/samagotchi/web/public/stage_view.js
635
+ - lib/samagotchi/web/public/stage_watch.js
612
636
  - lib/samagotchi/web/public/stream_handlers.js
613
637
  - lib/samagotchi/web/public/strip.js
614
638
  - lib/samagotchi/web/public/tally.js
@@ -622,7 +646,9 @@ files:
622
646
  - lib/samagotchi/web/public/turn_model.js
623
647
  - lib/samagotchi/web/public/turn_view.js
624
648
  - lib/samagotchi/web/public/update_notice.js
649
+ - lib/samagotchi/web/public/vendor/directive-helpers.js
625
650
  - lib/samagotchi/web/public/vendor/directive.js
651
+ - lib/samagotchi/web/public/vendor/directives/repeat.js
626
652
  - lib/samagotchi/web/public/vendor/directives/unsafe-html.js
627
653
  - lib/samagotchi/web/public/vendor/lit-html.js
628
654
  - lib/samagotchi/web/qr.rb