ask-app-server 0.4.7 → 0.4.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d32c61a5ff66b6b3babd900aeae87b296267e2d9e4b1bf2bddd10ef4407d3d2f
4
- data.tar.gz: 1ed9f839391bc2049a85bd0f3f98a7cb877c5184856ab8bfdf0c16e956d02156
3
+ metadata.gz: 2272260cc312765a2fb145f0c7e121c895ef47baf6aa1e8394c4ad94e36ee835
4
+ data.tar.gz: 2c344972b0c6629793bc26901d4ea9560dacfa8cc5c1f9367749dbd7e90b4438
5
5
  SHA512:
6
- metadata.gz: b88dcd89903d66bb42229f4ba94cb9a9674041fb7dfaeb47fcedd4369d6ebb0216a32138cc0b0144162ba9c43e5fb9365b4914f1e69978172bc2a622752c46fb
7
- data.tar.gz: 92d17d5b601731f762a1f3b18a23773239d72994149b61af213ce2975cc974f6850e44058c2cb4e121c7a45fe5eea6091e3c751a5c676a9e72503107939b05b8
6
+ metadata.gz: 126c904d33f636b7336d6f44bca17cd8b2b6914d75d120cc1285537c353635e2eb800fccb2a52b651c5003c2f1afd7f6dd8cc3d069a53937175e9c0eb69211b9
7
+ data.tar.gz: 92f6895aa13391cb72d61e72764cd273d7bc0bcb12a8f627113fa315e90935818360e2a24c9cdad1b6616cabdae94396825acde43cb6fd9e2fa7763f896c7a48
@@ -44,6 +44,11 @@ module Ask
44
44
  @approval = approval
45
45
  @require_approval = require_approval
46
46
  @agent_dir = agent_dir
47
+ # The session's workspace is the tools' home: bash commands
48
+ # without an explicit cd run there (default_workdir), so the
49
+ # agent never drifts into the host's cwd — the recurring
50
+ # "where is the project?" failure mode.
51
+ @tools.each { |tool| tool.default_workdir = @agent_dir if tool.respond_to?(:default_workdir=) }
47
52
  @session = nil
48
53
  @translator = nil
49
54
  @on_event_block = nil
@@ -293,11 +298,29 @@ module Ask
293
298
  while @session.queued_steers.positive?
294
299
  @session.run("", reset: false)
295
300
  end
301
+ # A run that returned with the turn still active ended
302
+ # without a terminal event — the model stream dropped
303
+ # mid-turn (a dead connection, a provider that closed the
304
+ # stream early). Without this, the client would wait on a
305
+ # ghost run forever. Surface it as a failure so the fix
306
+ # loop can redeliver.
307
+ if @translator.turn_active?
308
+ @logger.error("Turn ended without a completion event — the model stream dropped mid-turn")
309
+ @translator.turn_failed("The model stream ended without completing the turn (the connection dropped mid-stream)")
310
+ end
296
311
  rescue => e
297
- # Agent may have been aborted — that's fine
298
- @logger.debug("Agent run error: #{e.message}") if ENV["DEBUG"]
312
+ # An aborted turn raises Ask::Agent::Aborted — that's not a
313
+ # failure, the client asked for it. Everything else is a real
314
+ # run failure the client must see: without a turn.failed, a
315
+ # watching board would wait forever on a dead turn. The
316
+ # translator's failure event carries the message.
317
+ unless e.is_a?(Ask::Agent::Aborted) || e.class.name.to_s.include?("Aborted")
318
+ @logger.error("Agent run failed: #{e.class}: #{e.message}")
319
+ @translator.turn_failed(e.message.to_s[0, 500])
320
+ end
299
321
  ensure
300
322
  @running_mutex.synchronize { @running = false }
323
+ @logger.debug("Run thread ended: turn_active=#{@translator.turn_active?} last_seq=#{@translator.last_seq}")
301
324
  end
302
325
  end
303
326
 
@@ -37,11 +37,14 @@ module Ask
37
37
  # Parse --config and --socket from args
38
38
  config_path = nil
39
39
  socket_path = nil
40
+ socket_only = false
40
41
  args.each_with_index do |arg, i|
41
42
  if arg == "--config" && i + 1 < args.length
42
43
  config_path = args[i + 1]
43
44
  elsif arg == "--socket" && i + 1 < args.length
44
45
  socket_path = args[i + 1]
46
+ elsif arg == "--socket-only"
47
+ socket_only = true
45
48
  end
46
49
  end
47
50
 
@@ -90,19 +93,37 @@ module Ask
90
93
  $stderr.puts "socket: #{socket_server.socket_path}"
91
94
  end
92
95
 
93
- # Start the server (stdio transport, blocks)
94
- server = Server.new(session_manager: session_manager)
95
-
96
- begin
97
- server.start
98
- rescue Interrupt
99
- $stderr.puts "\n[ask-app-server] Shutting down..." if config.debug?
100
- ensure
101
- # Clean up on Ctrl-C AND on normal exit (stdin EOF): the socket
102
- # file must not outlive the host.
103
- server.stop
104
- socket_server&.stop
105
- herdr_reporter&.close
96
+ if socket_only
97
+ # Daemon mode: the host lives on the socket alone, no stdio
98
+ # transport — its lifetime must not depend on stdin (a
99
+ # supervisor's EOF would take the whole host down with it).
100
+ if socket_server.nil?
101
+ $stderr.puts "[ask-app-server] --socket-only needs --socket"
102
+ exit 1
103
+ end
104
+ begin
105
+ socket_server.join
106
+ rescue Interrupt
107
+ $stderr.puts "\n[ask-app-server] Shutting down..." if config.debug?
108
+ ensure
109
+ socket_server&.stop
110
+ herdr_reporter&.close
111
+ end
112
+ else
113
+ # Start the server (stdio transport, blocks)
114
+ server = Server.new(session_manager: session_manager)
115
+
116
+ begin
117
+ server.start
118
+ rescue Interrupt
119
+ $stderr.puts "\n[ask-app-server] Shutting down..." if config.debug?
120
+ ensure
121
+ # Clean up on Ctrl-C AND on normal exit (stdin EOF): the socket
122
+ # file must not outlive the host.
123
+ server.stop
124
+ socket_server&.stop
125
+ herdr_reporter&.close
126
+ end
106
127
  end
107
128
  end
108
129
 
@@ -141,11 +162,17 @@ module Ask
141
162
 
142
163
  USAGE:
143
164
  ask-app-server Start in stdio mode
165
+ ask-app-server --socket SOCK --socket-only
166
+ Start as a socket daemon (no stdio)
144
167
 
145
168
  OPTIONS:
146
169
  --version, -v Show version
147
170
  --help, -h Show this help message
148
171
  --config PATH Config file path (default: auto-detect)
172
+ --socket PATH Also expose a unix socket at PATH
173
+ --socket-only Daemon mode: serve the socket alone
174
+ (the host's lifetime does not depend
175
+ on stdin)
149
176
 
150
177
  CONFIG FILE (JSON):
151
178
  Search order:
@@ -17,7 +17,13 @@ module Ask
17
17
  # delivery means the log is append-only; clients with cursors older
18
18
  # than the cap miss the dropped events (durable log is a host
19
19
  # storage concern).
20
- MAX_EVENTS = 2000
20
+ # How many events the translator buffers for cursor-based
21
+ # delivery. The cap must clear the largest thinking flood: a
22
+ # reasoning model streams thousands of deltas per turn (observed
23
+ # >9000), and events dropped here are dropped for every connected
24
+ # client — the completion event could fall in the gap and the
25
+ # client would watch a ghost run forever.
26
+ MAX_EVENTS = 20_000
21
27
 
22
28
  # Optional observer called with every emitted canonical Event.
23
29
  # This is the single choke point for all session events (translations
@@ -135,6 +141,24 @@ module Ask
135
141
  @events.select { |e| e.seq > after_seq }
136
142
  end
137
143
 
144
+ # Whether a turn is mid-flight (started, not completed or failed).
145
+ # A run that returns while a turn is still active ended without a
146
+ # terminal event — the model stream dropped mid-turn — and the
147
+ # adapter must surface that as a failure, not let the client wait
148
+ # on a ghost.
149
+ def turn_active?
150
+ @turn_active
151
+ end
152
+
153
+ # Emit a turn.failed event. Called by the adapter when a run dies
154
+ # (an exception, or a stream that ended without completing).
155
+ def turn_failed(message)
156
+ @turn_active = false
157
+ payload = { "error" => message.to_s }
158
+ payload["turnId"] = @turn_id if @turn_id
159
+ emit("turn.failed", payload)
160
+ end
161
+
138
162
  private
139
163
 
140
164
  def next_seq
@@ -231,13 +255,6 @@ module Ask
231
255
  emit("error", payload)
232
256
  end
233
257
 
234
- def turn_failed(message)
235
- @turn_active = false
236
- payload = { "error" => message.to_s }
237
- payload["turnId"] = @turn_id if @turn_id
238
- emit("turn.failed", payload)
239
- end
240
-
241
258
  def emit(type, payload)
242
259
  event = Ask::SessionProtocol::Events.event(type: type, seq: next_seq, payload: payload)
243
260
  @events << event
@@ -188,6 +188,11 @@ module Ask
188
188
  # events its sessions have produced since the connection's cursor,
189
189
  # then advance the cursor. Cursor-based, so each client receives
190
190
  # exactly the events after its own seq (replay on subscribe).
191
+ #
192
+ # Each event carries its sessionId: a connection may subscribe to
193
+ # several sessions, and the client routes events to the right
194
+ # watcher — without it, a multi-session client would record one
195
+ # session's events into another's run.
191
196
  def push_pending
192
197
  connections.each do |connection|
193
198
  connection.subscriptions.keys.each do |session_id|
@@ -198,7 +203,7 @@ module Ask
198
203
  next if events.empty?
199
204
 
200
205
  events.each do |ev|
201
- connection.write({ method: "session/event", params: { event: ev.to_h } })
206
+ connection.write({ method: "session/event", params: { sessionId: session_id, event: ev.to_h } })
202
207
  end
203
208
  connection.advance(session_id, events.last.seq)
204
209
  end
@@ -62,6 +62,14 @@ module Ask
62
62
 
63
63
  # Extract provider prefix from model string (e.g., "opencode_go/deepseek-v4-flash")
64
64
  model_id, model_provider = parse_model_string(model || DEFAULT_MODEL)
65
+ # A provider-qualified model must stay resolvable: the agent's
66
+ # model catalog looks up bare ids, so the prefixed model is
67
+ # registered under its provider before the session builds its
68
+ # chat. Without this, "deepseek/deepseek-v4-flash" becomes the
69
+ # bare "deepseek-v4-flash" which the catalog may not know — the
70
+ # session falls back to the wrong provider and the run hangs
71
+ # silently on a missing credential.
72
+ register_prefixed_model(model_id, model_provider) if model_provider
65
73
  model_for_agent = model_provider ? model_id : (model || DEFAULT_MODEL)
66
74
 
67
75
  adapter = AgentAdapter.new(
@@ -318,6 +326,31 @@ module Ask
318
326
  parts = str.split("/", 2)
319
327
  [parts[1], parts[0]]
320
328
  end
329
+
330
+ # Make a provider-qualified model resolvable by the agent's model
331
+ # catalog: the catalog looks up bare ids, so a model that arrived
332
+ # as "provider/id" is registered under its provider. If the id is
333
+ # already known, its existing provider is preserved — the caller's
334
+ # explicit prefix is the override.
335
+ def register_prefixed_model(model_id, model_provider)
336
+ catalog = Ask::ModelCatalog.respond_to?(:instance) ? Ask::ModelCatalog.instance : nil
337
+ return unless catalog&.respond_to?(:register)
338
+
339
+ existing = catalog.find(model_id) rescue nil
340
+ return if existing && existing.provider.to_s == model_provider.to_s
341
+ return if existing && existing.provider.to_s != "unknown"
342
+
343
+ info = OpenStruct.new(
344
+ id: model_id.to_s,
345
+ provider: model_provider.to_s,
346
+ chat?: true,
347
+ context: 4096,
348
+ output: 4096
349
+ )
350
+ catalog.register(info)
351
+ rescue StandardError
352
+ nil # registration is best-effort; the session will surface a real error
353
+ end
321
354
  end
322
355
  end
323
356
  end
@@ -63,6 +63,12 @@ module Ask
63
63
  FileUtils.rm_f(@socket_path) rescue nil
64
64
  end
65
65
 
66
+ # Block until the acceptor stops (the process's main loop in
67
+ # socket-only mode — there's no stdio transport to join on).
68
+ def join
69
+ @acceptor&.join
70
+ end
71
+
66
72
  # Whether the server is running.
67
73
  def running?
68
74
  @running
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module AppServer
5
- VERSION = "0.4.7"
5
+ VERSION = "0.4.9"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-app-server
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.7
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '0.1'
46
+ version: 0.2.0
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: '0.1'
53
+ version: 0.2.0
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: ask-tools
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -85,14 +85,14 @@ dependencies:
85
85
  requirements:
86
86
  - - ">="
87
87
  - !ruby/object:Gem::Version
88
- version: 0.4.5
88
+ version: 0.4.6
89
89
  type: :runtime
90
90
  prerelease: false
91
91
  version_requirements: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: 0.4.5
95
+ version: 0.4.6
96
96
  - !ruby/object:Gem::Dependency
97
97
  name: minitest
98
98
  requirement: !ruby/object:Gem::Requirement