(convKey: string, userText: string, sink: TurnSink, signal: AbortSignal)
| 78 | const { readEpisodes } = await import('../context/episodic-memory.js'); |
| 79 | const sid = convKey ? await this.map.get(convKey) : undefined; |
| 80 | const eps = await readEpisodes(this.cwdFor(sid)); |
| 81 | return eps.slice(-limit).reverse().map(e => ({ when: relTime(e.ts), prompt: e.prompt, summary: e.summary })); |
| 82 | } |
| 83 | |
| 84 | /** Rebind a conversation to a past session by full id OR short prefix (as shown by /sessions). */ |
| 85 | async resume(convKey: string, idOrPrefix: string): Promise<boolean> { |
| 86 | const store = getSessionStore(); |
| 87 | const match = store.loadSession(idOrPrefix) ? idOrPrefix |
| 88 | : store.listRecentSessions(100).find(s => s.id.startsWith(idOrPrefix))?.id; |
| 89 | if (!match) return false; |
| 90 | await this.map.set(convKey, match); |
| 91 | return true; |
| 92 | } |
| 93 | |
| 94 | async runTurn(convKey: string, userText: string, sink: TurnSink, signal: AbortSignal): Promise<string> { |
| 95 | const store = getSessionStore(); |
| 96 | const model = this.modelFor(convKey); |
| 97 | const config = model === this.deps.config.defaults.model |
| 98 | ? this.deps.config |
| 99 | : { ...this.deps.config, defaults: { ...this.deps.config.defaults, model } }; // per-chat model override |
| 100 | // Resolve (or create) the durable session FIRST so the AgentLoop is born |
| 101 | // with that session's cwd — not the directory the bot process happened to |
| 102 | // start in. `/continue` binds a CLI session; tools must edit THAT project. |
| 103 | let sessionId = await this.map.get(convKey); |
| 104 | const existing = sessionId ? store.loadSession(sessionId) : null; |
| 105 | const cwd = this.cwdFor(sessionId); |
| 106 | |
| 107 | const agent = new AgentLoop({ |
| 108 | router: this.deps.router, |
| 109 | registry: this.deps.registry, |
| 110 | permissions: this.deps.permissions, |
| 111 | config, |
| 112 | cwd, |
| 113 | }); |
| 114 | |
| 115 | let messages; |
| 116 | if (existing) { |
| 117 | messages = [...existing.messages, { role: 'user' as const, content: userText }]; |
| 118 | } else { |
| 119 | sessionId = store.createSession(cwd, model); |
| 120 | await this.map.set(convKey, sessionId); |
| 121 | messages = await agent.buildInitialMessages(userText, 'normal', model); |
| 122 | } |
| 123 | const sid: string = sessionId!; // always set by the branch above |
| 124 | store.recordTurn(sid, [{ role: 'user', content: userText }], { input: 0, output: 0, costUsd: 0 }); |
| 125 | |
| 126 | // /auto on → approve permission prompts automatically (skip the buttons) for the affirmative |
| 127 | // option only; a prompt with no clear "yes/allow" still falls back to asking, so we never |
| 128 | // silently green-light something ambiguous. |
| 129 | const auto = this.autoByKey.get(convKey) ?? false; |
| 130 | const askUser = async (prompt: string, options: string[] = ['yes', 'no']): Promise<string> => { |
| 131 | if (auto) { const yes = options.find(o => /^(y|yes|allow|approve|ok|always)\b/i.test(o.trim())); if (yes) return yes; } |
| 132 | return getOperatorHub().requestApproval('bot', prompt, options, { |
| 133 | lane: botLane(convKey), |
| 134 | origin: convKey, |
| 135 | }); |
| 136 | }; |
| 137 |
nothing calls this directly
no test coverage detected