@agfpd/iapeer 0.2.5 → 0.2.7
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 +1 -1
- package/src/launch/adapters/claude.ts +40 -13
- package/src/launch/index.ts +74 -0
- package/src/launch/launch.test.ts +61 -7
- package/src/launch/launchdRun.ts +7 -1
- package/src/launch/types.ts +11 -0
- package/src/lifecycle/index.ts +4 -0
package/package.json
CHANGED
|
@@ -30,15 +30,17 @@ import type { ControlCommand, ControlPlan, LaunchAdapterConfig, LaunchSpec, Runt
|
|
|
30
30
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
33
|
+
* Markers that a claude startup dialog / picker is on screen — used by isInputReady
|
|
34
|
+
* to GATE delivery (a dialog up ⇒ not ready). The KEYS that clear each live in
|
|
35
|
+
* bootDialogKeys (NOT uniformly Enter — the resume picker must be NAVIGATED, see there):
|
|
35
36
|
* - 'trust this folder' — first-run folder-trust modal.
|
|
36
37
|
* - 'Allow external CLAUDE.md file imports?' — external-import consent.
|
|
37
38
|
* - 'I am using this for local development' — dev-channels accept
|
|
38
39
|
* (claude-start.sh:337), shown when PEER_START_ARGS carries
|
|
39
40
|
* --dangerously-load-development-channels.
|
|
40
|
-
* - 'Resume from summary'
|
|
41
|
-
*
|
|
41
|
+
* - 'Resume from summary' — the resume compact-picker (default cursor =
|
|
42
|
+
* "summary (recommended)", which compacts; bootDialogKeys picks "full" instead).
|
|
43
|
+
* - 'Resuming the full session' — the post-select load state (still not ready).
|
|
42
44
|
*/
|
|
43
45
|
const CLAUDE_BOOT_DIALOG_MARKERS = [
|
|
44
46
|
'trust this folder',
|
|
@@ -104,7 +106,7 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
104
106
|
|
|
105
107
|
/**
|
|
106
108
|
* argv = claudeBin + headless flags + (system-prompt-file when set) +
|
|
107
|
-
* (--
|
|
109
|
+
* (--continue when resuming) + extraArgs.
|
|
108
110
|
*
|
|
109
111
|
* - '--dangerously-skip-permissions' claude-start.sh:318, spawner.ts:776 —
|
|
110
112
|
* headless peer has no interactive owner to grant per-tool permission.
|
|
@@ -117,8 +119,15 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
117
119
|
* ONLY when set (a tui runtime that usesDoctrine composes one). Swaps the
|
|
118
120
|
* CC coding baseline for the merged peer doctrine; plugin/MCP/CLAUDE.md
|
|
119
121
|
* layers stay intact (claude-start.sh:293-303).
|
|
120
|
-
* - '--
|
|
121
|
-
*
|
|
122
|
+
* - '--continue' when spec.resume — continue the cwd's MOST-RECENT session
|
|
123
|
+
* (one session-lineage per peer cwd, so most-recent == the warm peer's
|
|
124
|
+
* session, == the newest transcript resolveResume validated). NOT
|
|
125
|
+
* '--resume <uuid>': in claude 2.1.169 `--resume <arg>` treats arg as a
|
|
126
|
+
* SEARCH QUERY (opens a session-list picker), not a session-id — so a bare
|
|
127
|
+
* uuid no longer resumes directly. `--continue` resumes the last session
|
|
128
|
+
* with no session-list step. The summary-vs-full compact picker still
|
|
129
|
+
* appears (handled in bootDialogKeys: pick "full", never the recommended
|
|
130
|
+
* summary). resolveResume still gates resume-vs-fresh upstream (fail-loud).
|
|
122
131
|
* - ...extraArgs PEER_START_ARGS passthrough (LaunchSpec.extraArgs).
|
|
123
132
|
* NO currency — no marketplace/install/update on this path.
|
|
124
133
|
*/
|
|
@@ -129,19 +138,37 @@ export const claudeAdapter: RuntimeAdapter = {
|
|
|
129
138
|
'--disallowedTools',
|
|
130
139
|
'AskUserQuestion',
|
|
131
140
|
...(spec.systemPromptFile ? ['--system-prompt-file', spec.systemPromptFile] : []),
|
|
132
|
-
...(spec.
|
|
141
|
+
...(spec.resume ? ['--continue'] : []),
|
|
133
142
|
...(spec.extraArgs ?? []),
|
|
134
143
|
]
|
|
135
144
|
},
|
|
136
145
|
|
|
137
146
|
/**
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
147
|
+
* Map a visible startup dialog to the keys that clear it correctly:
|
|
148
|
+
*
|
|
149
|
+
* - RESUME COMPACT-PICKER ('Resume from summary …') → ['Down','Enter'].
|
|
150
|
+
* This menu's cursor DEFAULTS to "1. Resume from summary (recommended)",
|
|
151
|
+
* and selecting it COMPACTS the session (claude implements that choice as an
|
|
152
|
+
* internal /compact). A bare Enter would therefore silently compact a warm
|
|
153
|
+
* peer on EVERY idle-reap→resume — losing its full context. Move the cursor
|
|
154
|
+
* DOWN one to "2. Resume full session as-is" and confirm, keeping full
|
|
155
|
+
* context. (Verified against the live picker: ❯ on option 1, ↑↓ navigation,
|
|
156
|
+
* "Enter to confirm".)
|
|
157
|
+
* - OTHER MODALS (folder-trust / external-import / dev-channels) → ['Enter']:
|
|
158
|
+
* their default-highlighted option IS the proceed path, so a bare Enter clears
|
|
159
|
+
* each (claude-start.sh:341).
|
|
160
|
+
* - anything else (incl. the post-select "Resuming…" load state) → null (wait).
|
|
142
161
|
*/
|
|
143
162
|
bootDialogKeys(pane: string): string[] | null {
|
|
144
|
-
|
|
163
|
+
if (pane.includes('Resume from summary')) return ['Down', 'Enter']
|
|
164
|
+
if (
|
|
165
|
+
pane.includes('trust this folder') ||
|
|
166
|
+
pane.includes('Allow external CLAUDE.md file imports?') ||
|
|
167
|
+
pane.includes('I am using this for local development')
|
|
168
|
+
) {
|
|
169
|
+
return ['Enter']
|
|
170
|
+
}
|
|
171
|
+
return null
|
|
145
172
|
},
|
|
146
173
|
|
|
147
174
|
/**
|
package/src/launch/index.ts
CHANGED
|
@@ -107,6 +107,72 @@ function ready(identity: string): LaunchResult {
|
|
|
107
107
|
return { status: 'READY', identity, process_address: identity }
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
|
+
// Exit-cause observability — capture WHY a session's process died, AT THE MOMENT
|
|
112
|
+
// of death. The daemon's 60 s supervise tick only learns of a death post-factum
|
|
113
|
+
// (reaped-gone), by which time the exit code/signal — and often the whole tmux
|
|
114
|
+
// server — is gone (the boris-fresh-style blind spot one level deeper than the
|
|
115
|
+
// supervise log). A tmux `pane-died` hook closes it: it fires the instant the
|
|
116
|
+
// pane's leader process exits (with `remain-on-exit on` retaining the dead pane so
|
|
117
|
+
// `#{pane_dead_status}`/`#{pane_dead_signal}` are populated), logs one logfmt line,
|
|
118
|
+
// then kill-sessions the now-dead pane so the daemon's `has-session` death
|
|
119
|
+
// detection (and the always-on KeepAlive block-watch) stay intact.
|
|
120
|
+
//
|
|
121
|
+
// Scope — verified live on tmux 3.6a (3 death modes + the daemon-reap path):
|
|
122
|
+
// • graceful exit → `dead_status=<code> dead_signal=` (code, no signal)
|
|
123
|
+
// • SIGTERM/SIGKILL/crash to the PROCESS → `dead_status= dead_signal=<name>`
|
|
124
|
+
// • daemon-initiated `kill-session` (idle-reap / self-TTL / stop) does NOT fire
|
|
125
|
+
// pane-died → NO line here (those are already in lifecycle.log — no double-log).
|
|
126
|
+
// IRREDUCIBLE GAP: SIGKILL to the tmux SERVER process itself runs no hook (the
|
|
127
|
+
// event loop is gone); only the daemon's post-factum reaped-gone catches that.
|
|
128
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/** The exit-cause log file inside `exitLogDir` (sibling to lifecycle.log). */
|
|
131
|
+
export function exitLogPath(exitLogDir: string): string {
|
|
132
|
+
return `${exitLogDir}/exits.log`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the tmux `pane-died` hook command string (the value of `set-hook -t <id>
|
|
137
|
+
* pane-died <value>`). On the pane leader's death it appends ONE logfmt line —
|
|
138
|
+
* `ts=<ISO> ev=session-exit identity=<id> dead_status=#{…} dead_signal=#{…}`
|
|
139
|
+
* — to `exitLogFile`, then runs a tmux-NATIVE `kill-session` (no shell `tmux`, so
|
|
140
|
+
* it needs no PATH — launchd gives always-on servers a minimal one). Pure (no I/O)
|
|
141
|
+
* so the exact string is unit-testable. Quoting: the `run-shell` arg is wrapped in
|
|
142
|
+
* tmux SINGLE quotes (literal at the tmux layer, still `#{}`-format-expanded) with
|
|
143
|
+
* sh DOUBLE quotes inside — the two levels never collide; `\n`/`$(…)` pass through
|
|
144
|
+
* tmux untouched to sh. `identity`/`exitLogFile` are assumed free of single quotes
|
|
145
|
+
* (runtime-personality identities and the ~/.iapeer/logs path always are). */
|
|
146
|
+
export function exitCauseHook(identity: string, exitLogFile: string): string {
|
|
147
|
+
const line =
|
|
148
|
+
`ts=%s ev=session-exit identity=${identity} ` +
|
|
149
|
+
`dead_status=#{pane_dead_status} dead_signal=#{pane_dead_signal}\\n`
|
|
150
|
+
const log =
|
|
151
|
+
`printf "${line}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "${exitLogFile}"`
|
|
152
|
+
return `run-shell '${log}' ; kill-session -t "${identity}"`
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Install the exit-cause observability on a freshly-created session: ensure the
|
|
156
|
+
* exit-log dir exists, turn `remain-on-exit` on (so pane-died can read the dead
|
|
157
|
+
* pane's status/signal) and register the hook. Best-effort — a tmux/FS hiccup
|
|
158
|
+
* here must never fail the launch (observability is never load-bearing). No-op
|
|
159
|
+
* when `exitLogDir` is falsy (a partial/test cfg): `remain-on-exit` stays OFF so
|
|
160
|
+
* behavior is byte-identical to before (and no dead pane can linger un-reaped). */
|
|
161
|
+
function installExitHook(sock: string, identity: string, exitLogDir: string | undefined): void {
|
|
162
|
+
if (!exitLogDir) return
|
|
163
|
+
try {
|
|
164
|
+
mkdirSync(exitLogDir, { recursive: true, mode: 0o700 })
|
|
165
|
+
// remain-on-exit must be ON before the process can die, else pane-died won't
|
|
166
|
+
// retain the dead pane and the status/signal are lost. Set it (and the hook)
|
|
167
|
+
// immediately after new-session — the only un-coverable window is the few ms
|
|
168
|
+
// before this runs, irrelevant for a runtime that takes seconds to initialize.
|
|
169
|
+
tmux(sock, 'set-option', '-t', identity, 'remain-on-exit', 'on')
|
|
170
|
+
tmux(sock, 'set-hook', '-t', identity, 'pane-died', exitCauseHook(identity, exitLogPath(exitLogDir)))
|
|
171
|
+
} catch {
|
|
172
|
+
/* observability is best-effort — never block a wake on a hook-install hiccup */
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
110
176
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
111
177
|
// launch — bring up ONE session (runtime-agnostic via the adapter)
|
|
112
178
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -178,6 +244,14 @@ export const launch: LaunchFn = async (
|
|
|
178
244
|
return fail(identity, `tmux new-session failed: ${(start.stderr ?? '').trim() || 'exit ' + start.status}`)
|
|
179
245
|
}
|
|
180
246
|
|
|
247
|
+
// (2.5) Exit-cause observability: a `pane-died` hook that records WHY this
|
|
248
|
+
// session's process dies (status/signal) at the moment of death into
|
|
249
|
+
// <exitLogDir>/exits.log, then kill-sessions the dead pane (so the
|
|
250
|
+
// daemon's has-session death detection + always-on KeepAlive stay intact).
|
|
251
|
+
// Installed ASAP — before pipe-pane — so even a runtime that dies during
|
|
252
|
+
// boot leaves a cause. No-op without cfg.exitLogDir (remain-on-exit off).
|
|
253
|
+
installExitHook(sock, identity, cfg.exitLogDir)
|
|
254
|
+
|
|
181
255
|
// (3) pipe-pane the session output to the per-identity log.
|
|
182
256
|
mkdirSync(cfg.logDir, { recursive: true, mode: 0o700 })
|
|
183
257
|
const paneLog = `${cfg.logDir}/${identity}.log`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { getAdapter, launch } from './index.ts'
|
|
2
|
+
import { exitCauseHook, exitLogPath, getAdapter, launch } from './index.ts'
|
|
3
3
|
import { claudeAdapter } from './adapters/claude.ts'
|
|
4
4
|
import { codexAdapter } from './adapters/codex.ts'
|
|
5
5
|
import { telegramAdapter } from './adapters/telegram.ts'
|
|
@@ -120,9 +120,12 @@ describe('claudeAdapter', () => {
|
|
|
120
120
|
])
|
|
121
121
|
})
|
|
122
122
|
|
|
123
|
-
test('buildArgv with system-prompt-file + resume + extras (
|
|
123
|
+
test('buildArgv with system-prompt-file + resume + extras → --continue (NOT --resume <uuid>)', () => {
|
|
124
|
+
// resume uses `--continue` (continue the cwd's most-recent session), NOT
|
|
125
|
+
// `--resume <uuid>` — in claude 2.1.169 `--resume <arg>` is a search query, not a
|
|
126
|
+
// session-id. resumeRef is set by the daemon but no longer consumed by the launch.
|
|
124
127
|
const argv = claudeAdapter.buildArgv(
|
|
125
|
-
spec({ systemPromptFile: '/tmp/sp.md', resumeRef: 'uuid-1', extraArgs: ['--foo'] }),
|
|
128
|
+
spec({ systemPromptFile: '/tmp/sp.md', resume: true, resumeRef: 'uuid-1', extraArgs: ['--foo'] }),
|
|
126
129
|
cfg,
|
|
127
130
|
)
|
|
128
131
|
expect(argv).toEqual([
|
|
@@ -132,10 +135,17 @@ describe('claudeAdapter', () => {
|
|
|
132
135
|
'AskUserQuestion',
|
|
133
136
|
'--system-prompt-file',
|
|
134
137
|
'/tmp/sp.md',
|
|
135
|
-
'--
|
|
136
|
-
'uuid-1',
|
|
138
|
+
'--continue',
|
|
137
139
|
'--foo',
|
|
138
140
|
])
|
|
141
|
+
expect(argv).not.toContain('--resume')
|
|
142
|
+
expect(argv).not.toContain('uuid-1')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('buildArgv: resume FALSE (fresh) → no --continue / --resume', () => {
|
|
146
|
+
const argv = claudeAdapter.buildArgv(spec({ resume: false, resumeRef: 'uuid-1' }), cfg)
|
|
147
|
+
expect(argv).not.toContain('--continue')
|
|
148
|
+
expect(argv).not.toContain('--resume')
|
|
139
149
|
})
|
|
140
150
|
|
|
141
151
|
test('buildArgv carries NO currency (no marketplace/plugin tokens)', () => {
|
|
@@ -153,12 +163,23 @@ describe('claudeAdapter', () => {
|
|
|
153
163
|
expect(claudeAdapter.isInputReady('❯ just a prompt')).toBe(false)
|
|
154
164
|
})
|
|
155
165
|
|
|
156
|
-
test('bootDialogKeys:
|
|
166
|
+
test('bootDialogKeys: proceed-modals → [Enter]; clean/load pane → null', () => {
|
|
157
167
|
expect(claudeAdapter.bootDialogKeys('I am using this for local development')).toEqual(['Enter'])
|
|
158
|
-
expect(claudeAdapter.bootDialogKeys('
|
|
168
|
+
expect(claudeAdapter.bootDialogKeys('trust this folder')).toEqual(['Enter'])
|
|
169
|
+
expect(claudeAdapter.bootDialogKeys('Allow external CLAUDE.md file imports?')).toEqual(['Enter'])
|
|
170
|
+
// the post-select "Resuming…" load state is NOT a modal to Enter — just wait.
|
|
171
|
+
expect(claudeAdapter.bootDialogKeys('Resuming the full session')).toBeNull()
|
|
159
172
|
expect(claudeAdapter.bootDialogKeys('❯ ready')).toBeNull()
|
|
160
173
|
})
|
|
161
174
|
|
|
175
|
+
test('bootDialogKeys: resume compact-picker → [Down, Enter] (pick "full", NOT the recommended summary)', () => {
|
|
176
|
+
// Default cursor is "1. Resume from summary (recommended)"; bare Enter would compact.
|
|
177
|
+
// Move DOWN to "2. Resume full session as-is" and confirm → full context preserved.
|
|
178
|
+
expect(
|
|
179
|
+
claudeAdapter.bootDialogKeys('❯ 1. Resume from summary (recommended)\n 2. Resume full session as-is'),
|
|
180
|
+
).toEqual(['Down', 'Enter'])
|
|
181
|
+
})
|
|
182
|
+
|
|
162
183
|
test('permissionDialog: proceed prompt → active, [Enter]', () => {
|
|
163
184
|
expect(claudeAdapter.permissionDialogActive('Do you want to proceed?')).toBe(true)
|
|
164
185
|
expect(claudeAdapter.permissionDialogActive('nothing')).toBe(false)
|
|
@@ -166,6 +187,39 @@ describe('claudeAdapter', () => {
|
|
|
166
187
|
})
|
|
167
188
|
})
|
|
168
189
|
|
|
190
|
+
// ─── exit-cause observability: the pane-died hook builder (pure string) ──────
|
|
191
|
+
describe('exitCauseHook (exit-cause observability)', () => {
|
|
192
|
+
const hook = exitCauseHook('claude-iapeer', '/r/logs/iapeer/exits.log')
|
|
193
|
+
|
|
194
|
+
test('exitLogPath → exits.log sibling to lifecycle.log', () => {
|
|
195
|
+
expect(exitLogPath('/r/logs/iapeer')).toBe('/r/logs/iapeer/exits.log')
|
|
196
|
+
})
|
|
197
|
+
test('reads pane_dead_status AND pane_dead_signal (both death classes)', () => {
|
|
198
|
+
// graceful exit populates #{pane_dead_status}; a signal populates #{pane_dead_signal}.
|
|
199
|
+
expect(hook).toContain('dead_status=#{pane_dead_status}')
|
|
200
|
+
expect(hook).toContain('dead_signal=#{pane_dead_signal}')
|
|
201
|
+
})
|
|
202
|
+
test('one logfmt line: ts + ev=session-exit + identity, appended to the exit log', () => {
|
|
203
|
+
expect(hook).toContain('ev=session-exit')
|
|
204
|
+
expect(hook).toContain('identity=claude-iapeer')
|
|
205
|
+
expect(hook).toContain('ts=%s')
|
|
206
|
+
expect(hook).toContain('>> "/r/logs/iapeer/exits.log"')
|
|
207
|
+
expect(hook).toContain('\\n') // literal backslash-n for sh printf, not a real newline
|
|
208
|
+
})
|
|
209
|
+
test('logs BEFORE it reaps: run-shell (sync, no -b) then tmux-native kill-session', () => {
|
|
210
|
+
// run-shell must NOT be backgrounded (-b) — the printf has to finish before the
|
|
211
|
+
// kill tears the server down, else the line is lost to the race (verified live).
|
|
212
|
+
expect(hook).not.toContain('run-shell -b')
|
|
213
|
+
expect(hook.indexOf('run-shell')).toBeLessThan(hook.indexOf('kill-session'))
|
|
214
|
+
// tmux-NATIVE kill-session (no shell `tmux`) → needs no PATH (launchd minimal env).
|
|
215
|
+
expect(hook).toContain('kill-session -t "claude-iapeer"')
|
|
216
|
+
})
|
|
217
|
+
test('quoting: single-quoted run-shell arg (tmux layer) wrapping double-quoted sh', () => {
|
|
218
|
+
expect(hook).toMatch(/run-shell '.*'/)
|
|
219
|
+
expect(hook).not.toContain("''") // no empty/again-collapsed single-quote pair
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
169
223
|
// ─── Ф-A #2: deliveryMarkers OWNED by the adapter (07.06 refactor) ───────────
|
|
170
224
|
describe('deliveryMarkers (adapter-owned, was transport PROMPT_GLYPHS)', () => {
|
|
171
225
|
test('claude: ❯ glyph + paste patterns', () => {
|
package/src/launch/launchdRun.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { spawnSync } from 'child_process'
|
|
|
20
20
|
import { join } from 'path'
|
|
21
21
|
import { INFRA_RUNTIME_BIN_ENV, isInfraRuntime, resolveSockDir } from '../core/constants.ts'
|
|
22
22
|
import { buildProcessAddress, buildSocketPath } from '../core/socket.ts'
|
|
23
|
-
import { peerLogsDir } from '../storage/index.ts'
|
|
23
|
+
import { peerLogsDir, pluginLogsDir } from '../storage/index.ts'
|
|
24
24
|
import { readPeerProfile } from '../identity/index.ts'
|
|
25
25
|
import { getAdapter, launch } from './index.ts'
|
|
26
26
|
import type { LaunchConfig, LaunchSpec } from './types.ts'
|
|
@@ -102,6 +102,12 @@ export async function runAlwaysOn(personality: string, runtime: string, cwd: str
|
|
|
102
102
|
// GLOBAL infra logs (Фаза §8): ~/.iapeer/logs/<personality>/ — match the plist's
|
|
103
103
|
// stdout/stderr dir (installAlwaysOnPlist), not per-peer <cwd>/.iapeer/logs/.
|
|
104
104
|
logDir: peerLogsDir(personality, { env }),
|
|
105
|
+
// Exit-cause log → the shared ~/.iapeer/logs/iapeer (== lifecycle eventLogDir),
|
|
106
|
+
// so an infra peer's self-death is recorded next to lifecycle.log too. The hook
|
|
107
|
+
// also reaps the dead pane: without it remain-on-exit would linger a dead pane,
|
|
108
|
+
// keeping sessionAlive() true so runAlwaysOn block-watches forever and KeepAlive
|
|
109
|
+
// never respawns — the hook prevents that regression as well as logging the cause.
|
|
110
|
+
exitLogDir: pluginLogsDir('iapeer', { env }),
|
|
105
111
|
env,
|
|
106
112
|
alwaysOn: true,
|
|
107
113
|
}
|
package/src/launch/types.ts
CHANGED
|
@@ -266,6 +266,17 @@ export interface LaunchConfig extends LaunchAdapterConfig {
|
|
|
266
266
|
maxAgeSecs: number
|
|
267
267
|
/** Log dir for pipe-pane output. */
|
|
268
268
|
logDir: string
|
|
269
|
+
/**
|
|
270
|
+
* Durable EXIT-CAUSE log dir (~/.iapeer/logs/iapeer — next to lifecycle.log,
|
|
271
|
+
* where the investigator looks). When set, launch installs a tmux `pane-died`
|
|
272
|
+
* hook that records WHY the session's process died (exit status / signal) AT THE
|
|
273
|
+
* MOMENT of death, into `<exitLogDir>/exits.log` — the blind spot the daemon's
|
|
274
|
+
* 60 s supervise tick (reaped-gone) can only see post-factum, after the exit code
|
|
275
|
+
* is already lost. Routed through cfg (NOT re-resolved from env) so it is isolated
|
|
276
|
+
* by the same sandbox as the rest of launch; a FALSY dir → no hook installed and
|
|
277
|
+
* `remain-on-exit` stays off (original behavior — a partial/test cfg never writes
|
|
278
|
+
* and never lingers a dead pane). See exitCauseHook in index.ts. */
|
|
279
|
+
exitLogDir?: string
|
|
269
280
|
env?: NodeJS.ProcessEnv
|
|
270
281
|
/**
|
|
271
282
|
* Always-on bring-up (infra runtimes held by launchd KeepAlive): SKIP the
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -805,6 +805,10 @@ export async function wakeOrSpawn(args: WakeArgs, deps: WakeDeps = {}): Promise<
|
|
|
805
805
|
readyGateSecs: cfg.readyGateSecs,
|
|
806
806
|
maxAgeSecs: cfg.maxAgeSecs,
|
|
807
807
|
logDir: cfg.logDir,
|
|
808
|
+
// Exit-cause log → next to lifecycle.log (~/.iapeer/logs/iapeer), where the
|
|
809
|
+
// investigator already looks: a self-death now leaves `exits.log` with the
|
|
810
|
+
// status/signal the daemon's post-factum reaped-gone could never recover.
|
|
811
|
+
exitLogDir: cfg.eventLogDir,
|
|
808
812
|
env,
|
|
809
813
|
}
|
|
810
814
|
// C2 — initial_prompt (launch-seed): on a FRESH wake, seed the first turn with
|