axmap-cli 1.8.0 โ†’ 1.10.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.
package/mcp/server.mjs CHANGED
@@ -46,6 +46,7 @@ import path from 'node:path'
46
46
  import readline from 'node:readline'
47
47
  import { fileURLToPath } from 'node:url'
48
48
  import { syncConfiguredProfile } from '../src/identity.mjs'
49
+ import { resolveTarget } from '../src/workspace.mjs'
49
50
 
50
51
  const HERE = path.dirname(fileURLToPath(import.meta.url))
51
52
 
@@ -77,14 +78,26 @@ let rootsTask = null
77
78
  let rootsGeneration = 0
78
79
  let rootRequestId = 0
79
80
  const pendingRoots = new Map()
80
- let REPO = resolveRepo()
81
-
82
- function gitRoot(cwd) {
83
- const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', windowsHide: true })
84
- const top = r.status === 0 ? (r.stdout ?? '').trim() : ''
85
- return top ? fs.realpathSync(top) : null
81
+ /**
82
+ * ๐Ÿ”ด ์‹œ์ž‘ ํด๋”๊ฐ€ ์—ฌ๋Ÿฌ ์ €์žฅ์†Œ๋ฅผ ๋‹ด์€ ์ž‘์—… ํด๋”๋ผ ๋ชจํ˜ธํ•˜๋ฉด ์—ฌ๊ธฐ์„œ ์ฃฝ์ง€ ์•Š๋Š”๋‹ค.
83
+ * ์„œ๋ฒ„๊ฐ€ ๋– ์•ผ ๋„๊ตฌ ํ˜ธ์ถœ์ด ๊ทธ ๋ชจํ˜ธํ•จ๊ณผ ๊ณ ๋ฅด๋Š” ๋ฐฉ๋ฒ•์„ ์—์ด์ „ํŠธ์—๊ฒŒ ์•Œ๋ ค ์ค„ ์ˆ˜ ์žˆ๋‹ค.
84
+ * ๋Œ€์ƒ์€ ๋„๊ตฌ๋ฅผ ๋ถ€๋ฅผ ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ ์ •ํ•˜๋ฏ€๋กœ, ์‚ฌ๋žŒ์ด `.axmap.json` ์„ ์“ฐ๋ฉด ๋‹ค์Œ
85
+ * ํ˜ธ์ถœ๋ถ€ํ„ฐ ๋ฐ”๋กœ ๋”ฐ๋ฅธ๋‹ค.
86
+ */
87
+ let REPO
88
+ try { REPO = resolveRepo() } catch (e) {
89
+ REPO = process.cwd()
90
+ process.stderr.write(`axMap MCP: ๋Œ€์ƒ ์ €์žฅ์†Œ๋ฅผ ์•„์ง ์ •ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.\n${e.message}\n`)
86
91
  }
87
92
 
93
+ /**
94
+ * ํด๋” โ†’ ๋Œ€์ƒ ์ €์žฅ์†Œ. ์œ„๋กœ ์ฐพ๊ธฐ(Git ์•ˆ)๋ฅผ ๋จผ์ € ํ•˜๊ณ , Git ๋ฐ–์ด๋ฉด ์ž‘์—… ํด๋” ๊ทœ์น™
95
+ * (`.axmap.json` ์ˆ˜๋™ ์„ค์ • โ†’ ๋ฐ”๋กœ ์•„๋ž˜ ์ €์žฅ์†Œ ์ž๋™ ํƒ์ƒ‰ โ†’ ๋ชจํ˜ธํ•˜๋ฉด ์˜ค๋ฅ˜)์„ ์“ด๋‹ค.
96
+ * ๊ทœ์น™์€ `src/workspace.mjs` ํ•œ ๊ณณ์— ์žˆ๋‹ค โ€” CLIยท๋กœ์ปฌ ์„ธ์…˜ ์—ฐ๊ฒฐ๊ณผ ๊ฐ™์€ ๋‹ต์„ ๋‚ด์•ผ ํ•œ๋‹ค.
97
+ * ์ €์žฅ์†Œ๊ฐ€ ์—†์œผ๋ฉด null, ๋ชจํ˜ธํ•˜๊ฑฐ๋‚˜ ํŒ์ •ํ•˜์ง€ ๋ชปํ•˜๋ฉด ์˜ˆ์™ธ๋‹ค.
98
+ */
99
+ function gitRoot(cwd) { return resolveTarget(cwd) }
100
+
88
101
  /** Explicit pin > per-call cwd > client roots > launch cwd.
89
102
  * A global desktop selection must never redirect another CLI session. */
90
103
  function resolveRepo(args = {}) {
@@ -111,7 +124,9 @@ function resolveRepo(args = {}) {
111
124
  return top
112
125
  }))]
113
126
  if (dirs.length === 1) return dirs[0]
114
- const launched = gitRoot(process.cwd())
127
+ // ์‹œ์ž‘ ํด๋”๋Š” ์—ฌ๋Ÿฌ roots ์ค‘ ํ•˜๋‚˜๋ฅผ ๊ณ ๋ฅด๋Š” ๊ทผ๊ฑฐ๋กœ๋งŒ ์“ด๋‹ค. ํŒ์ •ํ•˜์ง€ ๋ชปํ•˜๋ฉด ๊ทผ๊ฑฐ๊ฐ€ ์—†๋Š” ๊ฒƒ์ด๋‹ค.
128
+ let launched = null
129
+ try { launched = gitRoot(process.cwd()) } catch { /* ๊ณ ๋ฅผ ๊ทผ๊ฑฐ ์—†์Œ โ†’ ์•„๋ž˜ ์˜ค๋ฅ˜ */ }
115
130
  if (dirs.includes(launched)) return launched
116
131
  throw new Error('์ž‘์—… ์ €์žฅ์†Œ๋ฅผ ํ•˜๋‚˜๋กœ ์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๋„๊ตฌ์˜ cwd ์— ์ž‘์—… ํด๋” ์ ˆ๋Œ€ ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜์„ธ์š”.')
117
132
  }
@@ -493,7 +508,7 @@ const TOOLS = [
493
508
  },
494
509
  {
495
510
  name: 'ax_dispatch',
496
- description: '๋ช…์‹œ์ ์œผ๋กœ ์ƒ๋Œ€์˜ ์‹คํ–‰ ํ„ด์ด ํ•„์š”ํ•œ ์ž‘์—…์šฉ ์ €์ˆ˜์ค€ ์ „๋‹ฌ์ด๋‹ค. ์ผ๋ฐ˜ ์ง„ํ–‰ยท์™„๋ฃŒยท๊ณ„์•ฝ ํ™•์ธ ์•Œ๋ฆผ์—๋Š” ์ƒˆ ํ„ด ์—†๋Š” ax_message๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ˆ˜์‹ ์ž๊ฐ€ ์ž‘์—… ๊ฒฝ๊ณ„์—์„œ ax_mailbox๋กœ ๋ฌถ์Œ ํšŒ์ˆ˜ํ•œ๋‹ค. ๊ธฐ์กด ๋งฅ๋ฝ์„ ์œ ์ง€ํ•˜๋ฉฐ ์ƒˆ ๋ชจ๋ธ ํ”„๋กœ์„ธ์Šค๋ฅผ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. submitted๋Š” ์™„๋ฃŒ๊ฐ€ ์•„๋‹ˆ๋‹ค. unknown์ด๋ฉด ์ž๋™ ์žฌ์‹œ๋„ํ•˜์ง€ ๋ง๊ณ  ax_result๋กœ ํ™•์ธํ•œ๋‹ค. ์ž‘์—… ์ค‘์ธ ์„ธ์…˜์„ ๊ฐ•์ œ ์ค‘๋‹จํ•˜์ง€ ์•Š๋Š”๋‹ค.',
511
+ description: '๋ช…์‹œ์ ์œผ๋กœ ์ƒ๋Œ€์˜ ์‹คํ–‰ ํ„ด์ด ํ•„์š”ํ•œ ์ž‘์—…์šฉ ์ „๋‹ฌ์ด๋‹ค. ๊ด€๋ฆฌํ˜• Claude CodeยทCodexยทagy์™€ Windows ์ผ๋ฐ˜ Claude Code(console)๋Š” ๋Œ€๊ธฐ ์ค‘ ์‹คํ–‰ํ•˜๊ณ  ์ž‘์—…ยท์ž…๋ ฅยท์Šน์ธ ๋Œ€๊ธฐ ์ค‘์ด๋ฉด FIFO ํ์— ์ ‘์ˆ˜ํ•ด idle์—์„œ ์ž๋™ ์‹คํ–‰ํ•œ๋‹ค. ๊ธฐ์กด ๋งฅ๋ฝ๊ณผ ํ”„๋กœ์„ธ์Šค๋ฅผ ์œ ์ง€ํ•œ๋‹ค. queued/submitted๋Š” ์™„๋ฃŒ๊ฐ€ ์•„๋‹ˆ๋ฉฐ ax_result๋กœ ํ™•์ธํ•œ๋‹ค. unknown์€ ์žฌ์ „์†กํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ผ๋ฐ˜ ์ง„ํ–‰ ์•Œ๋ฆผ์—๋Š” ์ƒˆ ํ„ด ์—†๋Š” ax_message๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ž‘์—… ์ค‘์ธ ์„ธ์…˜์„ ๊ฐ•์ œ ์ค‘๋‹จํ•˜์ง€ ์•Š๋Š”๋‹ค.',
497
512
  inputSchema: { type: 'object', properties: {
498
513
  to: { type: 'string', description: 'ax_sessions์—์„œ ํ™•์ธํ•œ ์ƒ๋Œ€ ์„ธ์…˜ UUID' },
499
514
  prompt: { type: 'string', description: '์ƒ๋Œ€๊ฐ€ ์‹คํ–‰ํ•  ์ž‘์—… ์š”์ฒญ' },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//name": "๐Ÿ”ด npm ์˜ 'axmap' ์€ 2022๋…„๋ถ€ํ„ฐ ๋‚จ์ด ์“ฐ๊ณ  ์žˆ๋Š” ๋‹ค๋ฅธ ๊พธ๋Ÿฌ๋ฏธ๋‹ค(leozin_ ์˜ Map ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, 1.1.2). ๊ทธ๋ž˜์„œ `npx axmap` ์€ ์šฐ๋ฆฌ ๊ฒƒ์ด ์•„๋‹ˆ๊ณ , ๋ˆ„๊ตฌ์—๊ฒŒ๋„ ๊ทธ๋ ‡๊ฒŒ ์•ˆ๋‚ดํ•˜๋ฉด ์•ˆ ๋œ๋‹ค. 2026-08-28 ์— axmap-cli ๋กœ ์ •ํ–ˆ๋‹ค โ€” ์Šค์ฝ”ํ”„(@์‚ฌ์šฉ์ž/์ด๋ฆ„)๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ์ด์œ ๋Š” ์Šค์ฝ”ํ”„๊ฐ€ npm ๊ณ„์ • ์ด๋ฆ„์— ๋ฌถ์—ฌ์„œ ๋‚˜์ค‘์— ํŒ€์ด๋‚˜ ์กฐ์ง์œผ๋กœ ์˜ฎ๊ธธ ๋•Œ ์ด๋ฆ„์ด ํ†ต์งธ๋กœ ๋ฐ”๋€Œ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค. ์‚ฌ๋žŒ์ด ์น˜๋Š” **๋ช…๋ น** ์ด๋ฆ„์€ ๊ทธ๋Œ€๋กœ axmap ์ด๋‹ค (์•„๋ž˜ bin) โ€” ๊พธ๋Ÿฌ๋ฏธ ์ด๋ฆ„๊ณผ ๋ช…๋ น ์ด๋ฆ„์€ ๋‹ฌ๋ผ๋„ ๋œ๋‹ค.",
3
3
  "name": "axmap-cli",
4
- "version": "1.8.0",
4
+ "version": "1.10.0",
5
5
  "//private": "๐Ÿ”ด ์—ฌ๊ธฐ ์žˆ๋˜ \"private\": true ๋ฅผ 2026-08-28 ์— ์ง€์› ๋‹ค. ๊ทธ ์ค„์ด ์žˆ๋Š” ๋™์•ˆ npm publish ๋Š” ๊ฑฐ๋ถ€๋๋‹ค. ๋˜๋Œ๋ฆฌ๋ ค๋ฉด ๋‹ค์‹œ ๋„ฃ์œผ๋ฉด ๋˜์ง€๋งŒ, ์ด๋ฏธ ์˜ฌ๋ผ๊ฐ„ ๋ฒ„์ „์€ ๊ทธ๋ž˜๋„ ์•ˆ ์‚ฌ๋ผ์ง„๋‹ค โ€” npm ์€ ๊ฐ™์€ ๋ฒˆํ˜ธ๋ฅผ ๋ฎ์–ด์“ฐ์ง€ ๋ชปํ•˜๊ณ  ์‚ญ์ œ๋„ 72์‹œ๊ฐ„ ์•ˆ์—๋งŒ ๋œ๋‹ค. ์ฆ‰ ์ด ์ค„์„ ๋˜์‚ด๋ฆฌ๋Š” ๊ฒƒ์€ '์•ž์œผ๋กœ ์•ˆ ์˜ฌ๋ฆฐ๋‹ค' ๋Š” ๋œป์ด์ง€ '์˜ฌ๋ฆฐ ๊ฒƒ์„ ์—†์•ค๋‹ค' ๋Š” ๋œป์ด ์•„๋‹ˆ๋‹ค.",
6
6
  "//publishConfig": "axmap-cli ๋Š” ์Šค์ฝ”ํ”„๊ฐ€ ์—†์–ด์„œ ๊ธฐ๋ณธ์ด ์ด๋ฏธ ๊ณต๊ฐœ๋‹ค. ์ฆ‰ ์ง€๊ธˆ์€ ์—†์–ด๋„ ๋œ๋‹ค. ๊ทธ๋ž˜๋„ ๋‚จ๊ฒจ ๋‘๋Š” ์ด์œ : ๋‚˜์ค‘์— @์กฐ์ง/axmap ์ฒ˜๋Ÿผ ์Šค์ฝ”ํ”„๋ฅผ ๋ถ™์ด๋Š” ๋‚  ์ด ์ค„์ด ์—†์œผ๋ฉด publish ๊ฐ€ '์œ ๋ฃŒ ํ”Œ๋žœ์ด ํ•„์š”ํ•˜๋‹ค'๋Š” ์—‰๋šฑํ•œ ๋ง๋กœ ์‹คํŒจํ•œ๋‹ค.",
7
7
  "publishConfig": {
package/src/bridge.mjs CHANGED
@@ -5,11 +5,12 @@ import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
7
  import { randomUUID } from 'node:crypto';
8
- import { spawnSync } from 'node:child_process';
8
+ import { spawn, spawnSync } from 'node:child_process';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { withRepoLock } from './repolock.mjs';
11
11
  import { submitConsole, checkConsole, validateConsolePrompt, findConsoleTranscript, externalStatus } from './external-console.mjs';
12
- import { errorCode, errorMessage } from './session-types.mjs';
12
+ import { errorCode, errorMessage, RPC_NOT_DELIVERED_EXIT } from './session-types.mjs';
13
+ import { commonKey, resolveTarget } from './workspace.mjs';
13
14
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
15
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
15
16
  const bridgeHome = () => path.join(process.env.AXMAP_STATE_DIR || path.join(os.homedir(), '.axmap'), 'bridge');
@@ -45,14 +46,23 @@ function writeJSON(p, data) {
45
46
  fs.unlinkSync(tmp);
46
47
  }
47
48
  }
48
- function repoKey(cwd) {
49
+ function commonDirKey(cwd) {
49
50
  const r = spawnSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
50
51
  cwd, encoding: 'utf8', windowsHide: true,
51
52
  });
52
- if (r.status !== 0)
53
+ return r.status === 0 ? commonKey(fs.realpathSync(r.stdout.trim())) : null;
54
+ }
55
+ function repoKey(cwd) {
56
+ // Git ์•ˆ์ด๋ฉด ์ง€๊ธˆ๊นŒ์ง€์™€ ๊ฐ™์€ ํ•œ ๋ฒˆ์˜ git ํ˜ธ์ถœ๋กœ ๋๋‚ธ๋‹ค. ์‹คํŒจํ–ˆ์„ ๋•Œ๋งŒ ์ž‘์—… ํด๋”
57
+ // ๊ทœ์น™(์ˆ˜๋™ ์„ค์ • โ†’ ์ž์‹ ์ €์žฅ์†Œ ์ž๋™ ํƒ์ƒ‰ โ†’ ๋ชจํ˜ธํ•˜๋ฉด ์˜ค๋ฅ˜)์œผ๋กœ ๋Œ€์ƒ ์ €์žฅ์†Œ๋ฅผ ์ •ํ•œ๋‹ค.
58
+ const direct = commonDirKey(cwd);
59
+ if (direct)
60
+ return direct;
61
+ const repo = resolveTarget(cwd);
62
+ const key = repo && commonDirKey(repo);
63
+ if (!key)
53
64
  throw new Error(`Git ์ž‘์—… ์ €์žฅ์†Œ๋ฅผ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค: ${cwd}`);
54
- const key = fs.realpathSync(r.stdout.trim());
55
- return process.platform === 'win32' ? key.toLowerCase() : key;
65
+ return key;
56
66
  }
57
67
  function addressError(address) {
58
68
  // Only a loopback address exported by a local provider hook is accepted.
@@ -90,6 +100,24 @@ function runAPI(session, args) {
90
100
  return null;
91
101
  }
92
102
  function transcript(session) {
103
+ if (session.provider === 'claude') {
104
+ const home = process.env.AXMAP_CLAUDE_HOME || process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
105
+ const relative = session.transcript ? path.relative(path.join(home, 'projects'), session.transcript) : '..';
106
+ if (!session.transcript || relative.startsWith('..') || path.isAbsolute(relative) || path.basename(session.transcript) !== `${session.conversationId}.jsonl`)
107
+ throw new Error('Claude transcript identity mismatch');
108
+ if (!fs.existsSync(session.transcript))
109
+ return [];
110
+ return fs.readFileSync(session.transcript, 'utf8').split('\n').slice(0, -1).filter(Boolean).flatMap(line => {
111
+ const entry = JSON.parse(line);
112
+ if (entry.sessionId && entry.sessionId !== session.conversationId)
113
+ return [];
114
+ const content = entry.message?.content;
115
+ const text = typeof content === 'string' ? content : Array.isArray(content) ? content.filter(b => b.type === 'text').map(b => b.text || '').join('\n') : '';
116
+ if (!text || !['user', 'assistant'].includes(entry.type || ''))
117
+ return [];
118
+ return [{ type: entry.type === 'user' ? 'USER_INPUT' : 'PLANNER_RESPONSE', source: 'MODEL', status: 'DONE', content: text }];
119
+ });
120
+ }
93
121
  const expected = path.join(agyHome(), 'brain', session.conversationId || session.id, '.system_generated', 'logs', 'transcript.jsonl');
94
122
  if (session.transcript !== expected)
95
123
  throw new Error('๋“ฑ๋ก๋œ ๋Œ€ํ™” ๊ธฐ๋ก ๊ฒฝ๋กœ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.');
@@ -98,26 +126,102 @@ function transcript(session) {
98
126
  lines.pop();
99
127
  return lines.filter(s => s.trim()).map(s => JSON.parse(s));
100
128
  }
101
- function publicSession(s) {
129
+ function publicSession(s, liveness) {
102
130
  return { id: s.id, provider: s.provider, workspaces: s.workspaces, model: s.model,
103
131
  conversationId: s.conversationId,
104
132
  transport: s.transport || 'agentapi', state: s.state, pid: s.pid,
105
- registeredAt: s.registeredAt, event: s.event, fullyIdle: s.fullyIdle,
106
- role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval };
133
+ registeredAt: s.registeredAt, event: s.event,
134
+ // ํ˜ธ์ŠคํŠธ๊ฐ€ ์‚ด์•„ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด ์ €์žฅ๋œ "๋Œ€๊ธฐ"๋ฅผ ๋Œ€๊ธฐ๋กœ ๋ณด๊ณ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
135
+ fullyIdle: liveness === 'unverified' ? false : s.fullyIdle,
136
+ role: s.role, usage: s.usage, usageKnown: s.usageKnown, maxTokens: s.maxTokens, pendingApproval: s.pendingApproval,
137
+ ...(liveness ? { liveness } : {}) };
138
+ }
139
+ /** ๊ด€๋ฆฌํ˜• ์„ธ์…˜(ptyยทstdio)์˜ ๋กœ์ปฌ ์ œ์–ด ํŒŒ์ดํ”„. ์„ธ์…˜ ํ˜ธ์ŠคํŠธ๊ฐ€ ์‚ด์•„ ์žˆ๋Š” ๋™์•ˆ๋งŒ ์กด์žฌํ•œ๋‹ค. */
140
+ export const hostPipe = (id) => process.platform === 'win32' ? `\\\\.\\pipe\\axmap-${id}` : path.join(bridgeHome(), `${id}.sock`);
141
+ /** Windows ์˜ ์—ด๋ฆฐ ํŒŒ์ดํ”„ ์ด๋ฆ„ ๋ชฉ๋ก. ์ฝ์ง€ ๋ชปํ•˜๋ฉด ์˜ค๋ฅ˜ ์ž์ฒด๋ฅผ ๋Œ๋ ค์ค€๋‹ค โ€”
142
+ * "๋ชฉ๋ก์— ์—†์Œ"๊ณผ "๋ชฉ๋ก์„ ๋ชป ์ฝ์Œ"์„ ๊ฐ™์€ ๊ฒƒ์œผ๋กœ ๋ณด๋ฉด ์‚ฐ ์„ธ์…˜์„ ๋‹ซํž˜์œผ๋กœ ์ ๋Š”๋‹ค. */
143
+ function openPipes() {
144
+ try {
145
+ return new Set(fs.readdirSync('\\\\.\\pipe\\').map(n => n.toLowerCase()));
146
+ }
147
+ catch (e) {
148
+ return e instanceof Error ? e : new Error(String(e));
149
+ }
150
+ }
151
+ /**
152
+ * ์ €์žฅ๋œ ์ƒํƒœ๊ฐ€ ์•„๋‹ˆ๋ผ ํ˜ธ์ŠคํŠธ๊ฐ€ ์‹ค์ œ๋กœ ์‚ด์•„ ์žˆ๋Š”์ง€. ์—ฐ๊ฒฐ ์š”์ฒญ์€ ๋ณด๋‚ด์ง€ ์•Š๋Š”๋‹ค.
153
+ *
154
+ * `bridge serve` ๊ฐ€ ๊ฐ•์ œ ์ข…๋ฃŒ๋˜๋ฉด ์ข…๋ฃŒ ์ฒ˜๋ฆฌ(state: closed)๋ฅผ ์“ฐ์ง€ ๋ชปํ•ด ๊ธฐ๋ก์ด
155
+ * ์˜์›ํžˆ `idle` ๋กœ ๋‚จ๋Š”๋‹ค (์‹ค์ธก: ์„ธ์…˜ d3cde187, ํŒŒ์ดํ”„ ์—†์Œ). ํŒŒ์ดํ”„๋Š” ํ˜ธ์ŠคํŠธ๊ฐ€
156
+ * listen ํ•œ ๋’ค์—๋งŒ ๊ธฐ๋ก์„ ์“ฐ๋ฏ€๋กœ, ํŒŒ์ดํ”„๊ฐ€ ์—†๋‹ค๋Š” ๊ฒƒ์€ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋๋‚ฌ๋‹ค๋Š” ๋œป์ด๋‹ค.
157
+ * Windows : ํŒŒ์ดํ”„ ์ด๋ฆ„ ๋ชฉ๋ก์— ์—†์œผ๋ฉด dead, ๋ชฉ๋ก์„ ๋ชป ์ฝ์œผ๋ฉด unverified
158
+ * ๊ทธ ๋ฐ– : ์†Œ์ผ“ ํŒŒ์ผ์ด ์—†์œผ๋ฉด dead. ์žˆ์œผ๋ฉด ๊ธฐ๋ก๋œ ํ˜ธ์ŠคํŠธ pid ๋กœ ํŒ๋‹จํ•˜๊ณ ,
159
+ * pid ๊ฐ€ ์—†์œผ๋ฉด(์ด์ „ ๋ฒ„์ „ ๊ธฐ๋ก) unverified โ€” ๋‚จ์€ ์†Œ์ผ“ ํŒŒ์ผ๋งŒ์œผ๋กœ๋Š” ๋ชจ๋ฅธ๋‹ค.
160
+ */
161
+ function hostLiveness(s, pipes) {
162
+ if (process.platform === 'win32') {
163
+ const listed = pipes();
164
+ if (listed instanceof Error)
165
+ return 'unverified';
166
+ return listed.has(`axmap-${s.id}`.toLowerCase()) ? 'alive' : 'dead';
167
+ }
168
+ try {
169
+ fs.statSync(hostPipe(s.id));
170
+ }
171
+ catch (e) {
172
+ return errorCode(e) === 'ENOENT' ? 'dead' : 'unverified';
173
+ }
174
+ if (!Number.isSafeInteger(s.hostPid) || s.hostPid <= 0)
175
+ return 'unverified';
176
+ try {
177
+ process.kill(s.hostPid, 0);
178
+ return 'alive';
179
+ }
180
+ catch (e) {
181
+ return errorCode(e) === 'ESRCH' ? 'dead' : errorCode(e) === 'EPERM' ? 'alive' : 'unverified';
182
+ }
183
+ }
184
+ function markClosed(target, s) {
185
+ // ํ˜ธ์ŠคํŠธ๊ฐ€ ๋๋‚œ ๋’ค ๊ธฐ๋ก์„ ๋ฐ”๊พธ๋Š” ์ชฝ์€ ์—ฌ๊ธฐ๋ฟ์ด์ง€๋งŒ, ๋ง‰ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ ์ค‘์ธ ํ˜ธ์ŠคํŠธ๊ฐ€ ๋”
186
+ // ์ตœ์‹  ๊ธฐ๋ก(์‚ฌ์šฉ๋Ÿ‰ ๋“ฑ)์„ ์ผ์„ ์ˆ˜ ์žˆ์–ด ๋‹ค์‹œ ์ฝ๊ณ  ๋ฐ”๊พผ๋‹ค.
187
+ let latest = s;
188
+ try {
189
+ latest = readJSON(target);
190
+ }
191
+ catch { /* ์•„๋ž˜์—์„œ ์ฝ์€ ๊ธฐ๋ก์œผ๋กœ ํ‘œ์‹œ๋งŒ ํ•œ๋‹ค */ }
192
+ if (latest.state === 'closed')
193
+ return latest;
194
+ const closed = { ...latest, state: 'closed', fullyIdle: false,
195
+ closedReason: '์„ธ์…˜ ํ˜ธ์ŠคํŠธ์˜ ๋กœ์ปฌ ์—ฐ๊ฒฐ ํŒŒ์ดํ”„๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์ •์ƒ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ ์—†์ด ๋๋‚œ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.' };
196
+ // ์ €์žฅ์— ์‹คํŒจํ•ด๋„ ์ด๋ฒˆ ์กฐํšŒ์—๋Š” ๋‹ซํž˜์œผ๋กœ ๋ณด๊ณ ํ•œ๋‹ค. ๋‹ค์Œ ์กฐํšŒ๊ฐ€ ๋‹ค์‹œ ์‹œ๋„ํ•œ๋‹ค.
197
+ try {
198
+ writeJSON(target, closed);
199
+ }
200
+ catch { /* ์œ„์™€ ๊ฐ™๋‹ค */ }
201
+ return closed;
107
202
  }
108
203
  export function sessions(cwd) {
109
204
  const dir = path.join(bridgeHome(), 'sessions');
110
205
  if (!fs.existsSync(dir))
111
206
  return [];
112
207
  const repo = repoKey(cwd);
208
+ let pipes;
209
+ const listPipes = () => (pipes ??= openPipes());
113
210
  return fs.readdirSync(dir).filter(n => UUID.test(n.replace(/\.json$/, '')) && n.endsWith('.json'))
114
- .map(n => readJSON(path.join(dir, n))).filter(s => s.repos.includes(repo)).map(publicSession);
211
+ .map(n => [path.join(dir, n), readJSON(path.join(dir, n))])
212
+ .filter(([, s]) => s.repos.includes(repo))
213
+ .map(([target, s]) => {
214
+ if (!['pty', 'stdio'].includes(s.transport || '') || s.state === 'closed')
215
+ return publicSession(s);
216
+ const liveness = hostLiveness(s, listPipes);
217
+ return liveness === 'dead' ? publicSession(markClosed(target, s), 'dead') : publicSession(s, liveness);
218
+ });
115
219
  }
116
220
  function sessionFor(id, cwd) {
117
221
  const s = readJSON(file('sessions', id));
118
222
  if (!s.repos.includes(repoKey(cwd)))
119
223
  throw new Error('์ด ์„ธ์…˜์€ ํ˜„์žฌ ์ž‘์—… ์ €์žฅ์†Œ์— ๋“ฑ๋ก๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.');
120
- if (s.provider !== 'agy' && !['pty', 'stdio'].includes(s.transport || ''))
224
+ if (s.provider !== 'agy' && !(s.provider === 'claude' && s.transport === 'console') && !['pty', 'stdio'].includes(s.transport || ''))
121
225
  throw new Error('์•„์ง ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ˆ˜์‹  ์„ธ์…˜์ž…๋‹ˆ๋‹ค.');
122
226
  return s;
123
227
  }
@@ -149,16 +253,32 @@ export function requestResult(id, cwd) {
149
253
  const request = readJSON(target);
150
254
  if (request.repo !== repoKey(cwd))
151
255
  throw new Error('์ด ์š”์ฒญ์€ ํ˜„์žฌ ์ž‘์—… ์ €์žฅ์†Œ์˜ ์š”์ฒญ์ด ์•„๋‹™๋‹ˆ๋‹ค.');
152
- if (!['submitted', 'unknown'].includes(request.state))
256
+ if (!['queued', 'submitted', 'unknown'].includes(request.state))
153
257
  return request;
154
258
  let s = sessionFor(request.to, cwd);
259
+ if (request.state === 'queued' && s.transport === 'console')
260
+ return request;
155
261
  if (['pty', 'stdio'].includes(s.transport || '')) {
156
262
  try {
157
263
  Object.assign(request, terminalRPC(s.id, { action: 'result', id }));
158
264
  }
159
265
  catch (e) {
160
- request.state = 'unknown';
161
- request.error = errorMessage(e);
266
+ const saved = path.join(bridgeHome(), 'jobs', `${id}.json`);
267
+ // A normal host exit persists outcomes before closing the pipe. Never replay
268
+ // a queued job after a crash or mistake an unexecuted job for completion.
269
+ if (s.transport === 'stdio' && fs.existsSync(saved)) {
270
+ const job = readJSON(saved);
271
+ if (job.id === id && job.to === s.id && ['completed', 'failed', 'unknown'].includes(job.state))
272
+ Object.assign(request, job);
273
+ else {
274
+ request.state = 'unknown';
275
+ request.error = errorMessage(e);
276
+ }
277
+ }
278
+ else {
279
+ request.state = 'unknown';
280
+ request.error = errorMessage(e);
281
+ }
162
282
  }
163
283
  writeJSON(target, request);
164
284
  return request;
@@ -206,7 +326,7 @@ export async function waitForRequest(id, cwd, timeoutMs = 30000) {
206
326
  const deadline = performance.now() + timeoutMs;
207
327
  for (;;) {
208
328
  const result = requestResult(id, cwd);
209
- if (result.state !== 'submitted' || performance.now() >= deadline)
329
+ if (!['queued', 'sending', 'submitted'].includes(result.state) || performance.now() >= deadline)
210
330
  return result;
211
331
  await new Promise(resolve => setTimeout(resolve, Math.min(100, Math.max(0, deadline - performance.now()))));
212
332
  }
@@ -216,60 +336,106 @@ export function sessionLeasePath(id) {
216
336
  throw new Error('์„ธ์…˜ ID๋Š” UUID์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
217
337
  return path.join(bridgeHome(), 'dialogues', `session-${id}.lock`);
218
338
  }
219
- export function dispatchRequest({ to, prompt, cwd, from = 'axmap', dialogueId = null }) {
220
- if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 32768)
221
- throw new Error('์š”์ฒญ ๋ณธ๋ฌธ์€ 1~32768๋ฐ”์ดํŠธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
222
- const s = sessionFor(to, cwd);
223
- const lease = sessionLeasePath(to);
224
- if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId)
225
- throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ๋Œ€ํ™”๊ฐ€ ๋๋‚œ ๋’ค ์š”์ฒญํ•˜์„ธ์š”.');
226
- const dir = path.join(bridgeHome(), 'requests');
227
- fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
228
- return withRepoLock(path.join(dir, `${to}.lock`), () => {
339
+ export function dispatchRequest({ to, prompt, cwd, from = 'axmap', dialogueId = null, queueIfBusy = false }) {
340
+ // ๋ณด๋‚ด๊ธฐ ์‹œ์ž‘ ์ „์— ๋‚œ ์˜ค๋ฅ˜(์ด์ „ ์š”์ฒญ ๋ฏธํ™•์ •, ๋Œ€๊ธฐ ๋ฏธํ™•์ธ ๋“ฑ)๋Š” ํ™•์‹คํžˆ ์ „๋‹ฌ๋˜์ง€ ์•Š์€ ๊ฒƒ์ด๋‹ค.
341
+ // ๋ฐ›์€ ํŽธ์ง€ํ•จ์ด ์ด๋ฅผ "์ „๋‹ฌ ๋ถˆ๋ช…"๊ณผ ๊ตฌ๋ณ„ํ•ด์•ผ ์ชฝ์ง€๋ฅผ ๋‹ค์‹œ ๋Œ€๊ธฐ์—ด๋กœ ๋Œ๋ฆด ์ˆ˜ ์žˆ๋‹ค.
342
+ let sending = false;
343
+ try {
344
+ if (typeof prompt !== 'string' || !prompt.trim() || Buffer.byteLength(prompt) > 32768)
345
+ throw new Error('์š”์ฒญ ๋ณธ๋ฌธ์€ 1~32768๋ฐ”์ดํŠธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค.');
346
+ const s = sessionFor(to, cwd);
347
+ const lease = sessionLeasePath(to);
229
348
  if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId)
230
- throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค.');
231
- for (const n of fs.readdirSync(dir).filter(n => n.endsWith('.json'))) {
232
- const previous = readJSON(path.join(dir, n));
233
- if (previous.to !== to || !['sending', 'submitted', 'unknown'].includes(previous.state))
234
- continue;
235
- const latest = previous.state === 'sending' ? previous : requestResult(previous.id, cwd);
236
- if (['sending', 'submitted', 'unknown'].includes(latest.state))
237
- throw new Error(`์ด ์„ธ์…˜์˜ ์ด์ „ ์š”์ฒญ์ด ์•„์ง ํ™•์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${latest.id}`);
238
- }
239
- // No fallback to `agy -p` or --conversation: both would start a separate process.
240
- let consolePreflight;
241
- if (s.transport === 'console') {
349
+ throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค. ํ•ด๋‹น ๋Œ€ํ™”๊ฐ€ ๋๋‚œ ๋’ค ์š”์ฒญํ•˜์„ธ์š”.');
350
+ const dir = path.join(bridgeHome(), 'requests');
351
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
352
+ if (queueIfBusy && s.provider === 'claude' && s.transport === 'console') {
242
353
  validateConsolePrompt(prompt, from);
243
- consolePreflight = checkConsole(s);
244
- }
245
- if (!['pty', 'stdio', 'console'].includes(s.transport || '')) {
246
- runAPI(s, ['get-conversation-metadata', to]);
247
- if (!s.fullyIdle || s.event !== 'Stop')
248
- throw new Error('์ƒ๋Œ€ ์„ธ์…˜์˜ ๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ์•„์ง ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž‘์—… ์ข…๋ฃŒ ํ›…์„ ๊ธฐ๋‹ค๋ฆฌ์„ธ์š”.');
249
- }
250
- const request = { id: randomUUID(), to, from, repo: repoKey(cwd), prompt,
251
- createdAt: new Date().toISOString(), baseline: ['pty', 'stdio'].includes(s.transport || '') ? null : s.transcript ? transcript(s).length : 0, state: 'sending' };
252
- const target = file('requests', request.id);
253
- writeJSON(target, request);
254
- try {
255
- request.receipt = s.transport === 'console' ? submitConsole(s, request, consolePreflight) : ['pty', 'stdio'].includes(s.transport || '')
256
- ? terminalRPC(to, { action: 'submit', request })
257
- : runAPI(s, ['send-message', to, `[axMap request ${request.id}]\n์š”์ฒญ์ž: ${from}\n${prompt}`]);
258
- request.state = 'submitted';
259
- }
260
- catch (e) {
261
- // A timeout may occur after delivery. Never retry automatically.
262
- request.state = 'unknown';
263
- request.error = errorMessage(e);
354
+ if (s.state === 'closed')
355
+ throw new Error('Claude terminal is closed');
356
+ const request = withRepoLock(path.join(dir, `${to}.lock`), () => {
357
+ const pending = fs.readdirSync(dir).filter(n => n.endsWith('.json')).map(n => readJSON(path.join(dir, n)))
358
+ .filter(r => r.to === to && r.state === 'queued');
359
+ if (pending.length >= 100)
360
+ throw new Error('Claude command queue is full');
361
+ const request = { id: randomUUID(), to, from, repo: repoKey(cwd), prompt, createdAt: new Date().toISOString(), baseline: null, state: 'queued' };
362
+ writeJSON(file('requests', request.id), request);
363
+ return request;
364
+ });
365
+ const worker = spawn(process.execPath, [path.join(ROOT, 'tools', 'console-queue.mjs'), to, cwd], { detached: true, windowsHide: true, stdio: 'ignore' });
366
+ worker.on('error', () => { });
367
+ worker.unref();
368
+ return request;
264
369
  }
265
- writeJSON(target, request);
266
- return request;
267
- });
370
+ return withRepoLock(path.join(dir, `${to}.lock`), () => {
371
+ if (fs.existsSync(lease) && readJSON(lease).dialogueId !== dialogueId)
372
+ throw new Error('์ด ์„ธ์…˜์€ ์ž๋™ ๋Œ€ํ™”๊ฐ€ ์‚ฌ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค.');
373
+ for (const n of fs.readdirSync(dir).filter(n => n.endsWith('.json'))) {
374
+ const previous = readJSON(path.join(dir, n));
375
+ if (previous.to !== to || !['queued', 'sending', 'submitted', 'unknown'].includes(previous.state))
376
+ continue;
377
+ const latest = previous.state === 'sending' ? previous : requestResult(previous.id, cwd);
378
+ if (queueIfBusy && s.transport === 'stdio' && ['queued', 'submitted'].includes(latest.state))
379
+ continue;
380
+ if (['queued', 'sending', 'submitted', 'unknown'].includes(latest.state))
381
+ throw new Error(`์ด ์„ธ์…˜์˜ ์ด์ „ ์š”์ฒญ์ด ์•„์ง ํ™•์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${latest.id}`);
382
+ }
383
+ // No fallback to `agy -p` or --conversation: both would start a separate process.
384
+ let consolePreflight;
385
+ if (s.transport === 'console') {
386
+ validateConsolePrompt(prompt, from);
387
+ consolePreflight = checkConsole(s);
388
+ }
389
+ if (!['pty', 'stdio', 'console'].includes(s.transport || '')) {
390
+ runAPI(s, ['get-conversation-metadata', to]);
391
+ if (!s.fullyIdle || s.event !== 'Stop')
392
+ throw new Error('์ƒ๋Œ€ ์„ธ์…˜์˜ ๋Œ€๊ธฐ ์ƒํƒœ๋ฅผ ์•„์ง ํ™•์ธํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์ž‘์—… ์ข…๋ฃŒ ํ›…์„ ๊ธฐ๋‹ค๋ฆฌ์„ธ์š”.');
393
+ }
394
+ const request = { id: randomUUID(), to, from, repo: repoKey(cwd), prompt,
395
+ createdAt: new Date().toISOString(), baseline: ['pty', 'stdio'].includes(s.transport || '') ? null : s.transcript ? transcript(s).length : 0, state: 'sending' };
396
+ const target = file('requests', request.id);
397
+ writeJSON(target, request);
398
+ sending = true;
399
+ try {
400
+ request.receipt = s.transport === 'console' ? submitConsole(s, request, consolePreflight) : ['pty', 'stdio'].includes(s.transport || '')
401
+ ? terminalRPC(to, { action: 'submit', request, ...(queueIfBusy && s.transport === 'stdio' ? { queueIfBusy: true } : {}) })
402
+ : runAPI(s, ['send-message', to, `[axMap request ${request.id}]\n์š”์ฒญ์ž: ${from}\n${prompt}`]);
403
+ request.state = request.receipt?.queued ? 'queued' : 'submitted';
404
+ }
405
+ catch (e) {
406
+ if (notDelivered(e)) {
407
+ // ๋กœ์ปฌ ํŒŒ์ดํ”„์— ์—ฐ๊ฒฐ์กฐ์ฐจ ๋ชป ํ–ˆ๋‹ค(ENOENTยทECONNREFUSED). ํ•œ ๋ฐ”์ดํŠธ๋„ ๋ณด๋‚ด์ง€
408
+ // ์•Š์•˜์œผ๋ฏ€๋กœ ํ™•์‹คํžˆ ์ „๋‹ฌ๋˜์ง€ ์•Š์•˜๋‹ค. unknown ์œผ๋กœ ๋‘๋ฉด ์ด ์„ธ์…˜์˜ ๋‹ค์Œ ์š”์ฒญ์ด
409
+ // ์ „๋ถ€ ๋ง‰ํžŒ๋‹ค. ์ž๋™ ์žฌ์ „์†ก์€ ์—ฌ์ „ํžˆ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
410
+ request.state = 'failed';
411
+ request.error = `์ „๋‹ฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค: ${errorMessage(e)}`;
412
+ }
413
+ else {
414
+ // A timeout may occur after delivery. Never retry automatically.
415
+ request.state = 'unknown';
416
+ request.error = errorMessage(e);
417
+ }
418
+ }
419
+ writeJSON(target, request);
420
+ return request;
421
+ });
422
+ }
423
+ catch (e) {
424
+ if (!sending && e instanceof Error)
425
+ Object.assign(e, { notDelivered: true });
426
+ throw e;
427
+ }
268
428
  }
429
+ /** ์—ฐ๊ฒฐ ์ „์— ์‹คํŒจํ•ด ์•„๋ฌด๊ฒƒ๋„ ๋ณด๋‚ด์ง€ ์•Š์€ ์˜ค๋ฅ˜๋งŒ true. ๋ฌธ๊ตฌ๊ฐ€ ์•„๋‹ˆ๋ผ ์ข…๋ฃŒ ์ฝ”๋“œ๋‚˜ ๋ณด๋‚ด๊ธฐ ์ „ ๋‹จ๊ณ„๋กœ ๊ฐ€๋ฅธ๋‹ค. */
430
+ export const notDelivered = (e) => e instanceof Error && e.notDelivered === true;
269
431
  function terminalRPC(id, message) {
270
432
  const r = spawnSync(process.execPath, [path.join(ROOT, 'tools', 'run.mjs'), 'rpc', id], {
271
433
  input: JSON.stringify(message), encoding: 'utf8', windowsHide: true, timeout: 15000,
272
434
  });
435
+ // ์‹œ๊ฐ„ ์ดˆ๊ณผ(r.error)๋‚˜ ๊ทธ ๋ฐ–์˜ ์‹คํŒจ๋Š” ์—ฐ๊ฒฐ ๋’ค์— ๋‚ฌ์„ ์ˆ˜ ์žˆ์–ด ์ „๋‹ฌ ์—ฌ๋ถ€๋ฅผ ๋ชจ๋ฅธ๋‹ค.
436
+ if (!r.error && r.status === RPC_NOT_DELIVERED_EXIT) {
437
+ throw Object.assign(new Error(r.stderr?.trim() || '๋กœ์ปฌ ์„ธ์…˜ ํŒŒ์ดํ”„์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'), { notDelivered: true });
438
+ }
273
439
  if (r.error || r.status !== 0)
274
440
  throw new Error(r.error?.message || r.stderr?.trim() || 'ํ„ฐ๋ฏธ๋„ ์„ธ์…˜์— ์—ฐ๊ฒฐํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.');
275
441
  return JSON.parse(r.stdout);
@@ -333,3 +499,89 @@ export function installHooks({ executable, home = os.homedir() }) {
333
499
  return { hooks: target, backup: original === null ? null : backup,
334
500
  note: '๊ธฐ์กด ์‹คํ–‰ ์ค‘์ธ CLI๊ฐ€ ์ƒˆ ํ›…์„ ์ฝ์ง€ ์•Š์œผ๋ฉด ๊ฐ™์€ ๋Œ€ํ™”๋ฅผ ์žฌ๊ฐœํ•ด ํ•œ ๋ฒˆ ๋‹ค์‹œ ์—ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Remote Control์€ ํ•„์š” ์—†์Šต๋‹ˆ๋‹ค.' };
335
501
  }
502
+ /** One local worker per ordinary Claude terminal; no provider is launched. */
503
+ export function runConsoleQueue(to, cwd) {
504
+ const dir = path.join(bridgeHome(), 'requests');
505
+ const runnerLock = path.join(dir, `${to}.runner.lock`);
506
+ if (fs.existsSync(runnerLock)) {
507
+ const owner = readJSON(runnerLock);
508
+ try {
509
+ process.kill(owner.pid, 0);
510
+ }
511
+ catch (e) {
512
+ if (errorCode(e) === 'ESRCH')
513
+ fs.unlinkSync(runnerLock);
514
+ }
515
+ }
516
+ try {
517
+ withRepoLock(runnerLock, () => {
518
+ for (;;) {
519
+ const records = withRepoLock(path.join(dir, `${to}.lock`), () => fs.readdirSync(dir)
520
+ .filter(n => n.endsWith('.json')).map(n => readJSON(path.join(dir, n)))
521
+ .filter(r => r.to === to).sort((a, b) => a.createdAt.localeCompare(b.createdAt)));
522
+ const queued = records.filter(r => r.state === 'queued');
523
+ if (!queued.length)
524
+ return;
525
+ let waiting = false;
526
+ for (const r of records.filter(r => ['sending', 'submitted', 'unknown'].includes(r.state))) {
527
+ const latest = r.state === 'sending' ? r : requestResult(r.id, cwd);
528
+ if (['unknown', 'sending'].includes(latest.state))
529
+ return;
530
+ if (latest.state === 'submitted')
531
+ waiting = true;
532
+ }
533
+ const s = sessionFor(to, cwd);
534
+ if (s.pid) {
535
+ try {
536
+ process.kill(s.pid, 0);
537
+ }
538
+ catch (e) {
539
+ if (errorCode(e) === 'ESRCH') {
540
+ for (const r of queued)
541
+ writeJSON(file('requests', r.id), { ...r, state: 'failed', error: 'Claude process exited before execution' });
542
+ return;
543
+ }
544
+ }
545
+ }
546
+ if (s.state === 'closed') {
547
+ for (const r of queued)
548
+ writeJSON(file('requests', r.id), { ...r, state: 'failed', error: 'Claude terminal closed before execution' });
549
+ return;
550
+ }
551
+ let live;
552
+ try {
553
+ live = checkConsole(s);
554
+ }
555
+ catch {
556
+ waiting = true;
557
+ }
558
+ if (waiting || !live) {
559
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
560
+ continue;
561
+ }
562
+ withRepoLock(path.join(dir, `${to}.lock`), () => {
563
+ const r = readJSON(file('requests', queued[0].id));
564
+ if (r.state !== 'queued')
565
+ return;
566
+ r.baseline = transcript(s).length;
567
+ r.state = 'sending';
568
+ writeJSON(file('requests', r.id), r);
569
+ try {
570
+ r.receipt = submitConsole(s, r, live);
571
+ r.state = 'submitted';
572
+ }
573
+ catch (e) {
574
+ r.state = notDelivered(e) ? 'queued' : 'unknown';
575
+ r.error = errorMessage(e);
576
+ }
577
+ writeJSON(file('requests', r.id), r);
578
+ });
579
+ }
580
+ }, 1000);
581
+ }
582
+ catch (e) {
583
+ // Another worker won the runner lease. It owns draining; never replay here.
584
+ if (!fs.existsSync(runnerLock))
585
+ throw e;
586
+ }
587
+ }