Skip to content

Commit 6d02b99

Browse files
chitcommitclaude
andcommitted
fix(daemon,docs,upload): resolve P1+6 P2 Codex findings on PR #105
P1 entrypoint stub executor → refusal (codex 3352439151): The stub returned a sentinel dispatchedTaskId, causing the leader loop to call markIntentDispatched + completeIntent and record real pending intents as 'done' without execution. Replace with an explicit throw so the loop's failure path runs (intent → failed with clear refusal message). The real executor lands in PR #107. P2 entrypoint SIGTERM release sessionId (codex 3353069328): releaseLeadership gates on session ownership (codex-p2 PR#101); the fallback shutdown release passed no sessionId, so it always no-op'd against our own session-stamped lease. Pass the loop's sessionId. P2 install-daemon-vm.sh devDeps prune (codex 3352439158): npm ci --omit=dev pruned typescript, breaking 'npm run build:daemon'. Install full deps for the repo build step; runtime image in still gets --omit=dev separately. P2 systemd unit NODE_BIN (codex 3352439162): Hard-coded /usr/bin/node breaks nvm / /usr/local/bin installs. Ship unit with @@NODE_BIN@@ placeholder; install script substitutes the detected node path before installing. P2 systemd MDWE+JIT (codex 3352439169): MemoryDenyWriteExecute=true is documented incompatible with V8 JIT and would abort node at startup. Remove the flag; document why. P2 launchd plist missing env (codex 3352439167): launchd has no EnvironmentFile equivalent. Plist invoked node directly without DATABASE_URL/NODE_CHITTY_ID/NODE_DESCRIPTOR, hitting entrypoint.ts's fatal-missing-env branch on Mac Mini nodes. Add a launchd-shim.sh that sources /etc/chittycommand/env (same shape as systemd EnvironmentFile) then execs node. P2 migration 0016 single-upload conflict (codex 3352439160): Unique partial index on r2_key broke single /upload's plain INSERT...RETURNING — re-uploads now 500'd on unique violation. Add ON CONFLICT (r2_key) WHERE (r2_key IS NOT NULL) DO NOTHING to match batch path; fall back to SELECT-existing returning 200. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1361080 commit 6d02b99

6 files changed

Lines changed: 115 additions & 16 deletions

File tree

daemon/runtime/chittycommand-daemon.service

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ User=chittycommand
1111
Group=chittycommand
1212
WorkingDirectory=/opt/chittycommand
1313
EnvironmentFile=/etc/chittycommand/env
14-
ExecStart=/usr/bin/node /opt/chittycommand/dist/daemon/runtime/entrypoint.js
14+
# NOTE: install-daemon-vm.sh substitutes @@NODE_BIN@@ with the detected
15+
# node path (command -v node) before installing this unit, so deployments
16+
# using nvm or /usr/local/bin/node still start. If editing this file by
17+
# hand, replace @@NODE_BIN@@ with the absolute path to node.
18+
# Codex P2 PR#105: previously hard-coded /usr/bin/node failed when node was
19+
# installed elsewhere (nvm, /usr/local/bin).
20+
ExecStart=@@NODE_BIN@@ /opt/chittycommand/dist/daemon/runtime/entrypoint.js
1521
Restart=always
1622
RestartSec=5
1723
KillSignal=SIGTERM
@@ -32,7 +38,10 @@ ProtectControlGroups=true
3238
RestrictNamespaces=true
3339
RestrictRealtime=true
3440
LockPersonality=true
35-
MemoryDenyWriteExecute=true
41+
# MemoryDenyWriteExecute is intentionally OMITTED.
42+
# Codex P2 PR#105: systemd documents MDWE as incompatible with JIT engines
43+
# (V8 generates executable code pages at runtime). Enabling it would abort
44+
# Node at startup. Source maps in NODE_OPTIONS do not affect this.
3645
ReadWritePaths=/var/log/chittycommand
3746

3847
[Install]

daemon/runtime/entrypoint.ts

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,12 @@ async function main(): Promise<void> {
7272
log('signal_received', { signal });
7373
controller.abort();
7474
// Belt-and-suspenders release in case the loop is wedged before the
75-
// abort path reaches releaseLeadership.
75+
// abort path reaches releaseLeadership. Pass sessionId — releaseLeadership
76+
// gates on session ownership (codex-p2 PR#101 finding-2), so omitting it
77+
// would no-op against a lease claimed with our sessionId.
7678
releaseLeadership({ DATABASE_URL: env.DATABASE_URL }, env.NODE_CHITTY_ID, {
7779
role: META_LEADER_ROLE,
80+
sessionId,
7881
})
7982
.then((released) => log('release_on_signal', { released }))
8083
.catch((err) =>
@@ -89,15 +92,24 @@ async function main(): Promise<void> {
8992

9093
const executor = async (intent: { id: string; intentType: string }) => {
9194
// Foundation entrypoint: no real executor wired yet — the ActionAgent
92-
// bridge ships in the follow-up PR per ADR-001 out-of-scope list.
93-
// We mark the intent as dispatched to a sentinel ID so the leader loop
94-
// makes forward progress in smoke tests without inventing fake work.
95-
log('intent_executor_stub', {
95+
// bridge ships in PR #107 (feat/daemon-loop-executes-intents) per
96+
// ADR-001 out-of-scope list. Until then, claimed intents must NOT be
97+
// recorded as `done`. Throwing here routes the intent through the
98+
// loop's failure path (failIntent), which records a clear refusal
99+
// reason instead of inventing a successful dispatch.
100+
//
101+
// Codex P1 PR#105: previously returned a sentinel dispatchedTaskId,
102+
// causing markIntentDispatched + completeIntent to record real work
103+
// as `done` without execution. Refuse instead.
104+
log('intent_executor_unwired', {
96105
intentId: intent.id,
97106
intentType: intent.intentType,
98-
note: 'executor wiring deferred to follow-up PR',
107+
note: 'real executor lands in PR #107; refusing to record fake success',
99108
});
100-
return { dispatchedTaskId: `pending-executor:${intent.id}` };
109+
throw new Error(
110+
`daemon executor not wired on PR #105 (foundation only); ` +
111+
`intent ${intent.id} routed to failed — real executor lands in PR #107`,
112+
);
101113
};
102114

103115
try {

daemon/runtime/launchd-shim.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env bash
2+
#
3+
# launchd-shim.sh — macOS env-loading shim for the ChittyCommand daemon.
4+
#
5+
# launchd has no native EnvironmentFile equivalent (unlike systemd), so this
6+
# shim sources /etc/chittycommand/env before exec'ing node. The systemd unit
7+
# uses EnvironmentFile=/etc/chittycommand/env directly; this shim keeps the
8+
# macOS path consistent.
9+
#
10+
# Codex P2 PR#105: previously the launchd plist invoked node directly with
11+
# only NODE_ENV/NODE_OPTIONS exported, which meant entrypoint.ts's readEnv()
12+
# always tripped its fatal-missing-env branch on Mac Mini nodes.
13+
#
14+
# Install path: /opt/chittycommand/dist/daemon/runtime/launchd-shim.sh
15+
# Mode: 0755, owned by chittycommand:chittycommand
16+
#
17+
# canonical-uri: chittycanon://docs/architecture/chittycommand/daemon-supervisor
18+
19+
set -euo pipefail
20+
21+
ENV_FILE="${CHITTYCOMMAND_ENV_FILE:-/etc/chittycommand/env}"
22+
NODE_BIN="${CHITTYCOMMAND_NODE_BIN:-/usr/local/bin/node}"
23+
ENTRYPOINT="/opt/chittycommand/dist/daemon/runtime/entrypoint.js"
24+
25+
if [[ ! -r "${ENV_FILE}" ]]; then
26+
echo "[chittycommand-daemon-shim] fatal: env file not readable: ${ENV_FILE}" >&2
27+
exit 7
28+
fi
29+
30+
# Source env file. The file is the same KEY=VALUE format the systemd
31+
# EnvironmentFile expects, rendered by `op inject` at install time.
32+
set -a
33+
# shellcheck disable=SC1090
34+
. "${ENV_FILE}"
35+
set +a
36+
37+
# Preserve NODE_ENV / NODE_OPTIONS if launchd set them.
38+
export NODE_ENV="${NODE_ENV:-production}"
39+
export NODE_OPTIONS="${NODE_OPTIONS:---enable-source-maps}"
40+
41+
if [[ ! -x "${NODE_BIN}" ]]; then
42+
echo "[chittycommand-daemon-shim] fatal: node not executable at ${NODE_BIN}" >&2
43+
exit 8
44+
fi
45+
46+
exec "${NODE_BIN}" "${ENTRYPOINT}"

daemon/runtime/launchd/com.chittyos.chittycommand-daemon.plist

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,16 @@
1919
<key>Label</key>
2020
<string>com.chittyos.chittycommand-daemon</string>
2121

22+
<!--
23+
Codex P2 PR#105: launchd has no native EnvironmentFile equivalent and
24+
entrypoint.ts requires NODE_CHITTY_ID, DATABASE_URL, and
25+
NODE_DESCRIPTOR/HOSTNAME. Invoke a shim that sources
26+
/etc/chittycommand/env before exec'ing node, so the daemon sees the
27+
same env shape on macOS as on systemd Linux.
28+
-->
2229
<key>ProgramArguments</key>
2330
<array>
24-
<string>/usr/local/bin/node</string>
25-
<string>/opt/chittycommand/dist/daemon/runtime/entrypoint.js</string>
31+
<string>/opt/chittycommand/dist/daemon/runtime/launchd-shim.sh</string>
2632
</array>
2733

2834
<key>WorkingDirectory</key>

scripts/install-daemon-vm.sh

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,12 @@ else
8383
fi
8484

8585
# 2. Build
86+
# Build needs typescript (devDependency). We install ALL deps for the repo
87+
# build step here, then install --omit=dev separately into ${INSTALL_DIR}
88+
# below so the runtime image is dev-free.
89+
# Codex P2 PR#105: previously --omit=dev pruned tsc, breaking build:daemon.
8690
log "building daemon (npm run build:daemon)"
87-
run "cd ${REPO_ROOT} && npm ci --omit=dev --no-audit --no-fund || npm install --no-audit --no-fund"
91+
run "cd ${REPO_ROOT} && npm ci --no-audit --no-fund || npm install --no-audit --no-fund"
8892
run "cd ${REPO_ROOT} && npm run build:daemon"
8993

9094
# 3. Install dir + artifact sync
@@ -118,8 +122,18 @@ else
118122
chown "root:${SERVICE_USER}" "${ENV_FILE}"
119123
fi
120124

121-
# 6. systemd unit
122-
run "install -m 0644 -o root -g root ${UNIT_SRC} ${UNIT_DST}"
125+
# 6. systemd unit — substitute @@NODE_BIN@@ with detected node path.
126+
# Codex P2 PR#105: the unit ships with a placeholder so installs that use
127+
# nvm or /usr/local/bin/node don't fail on ExecStart=/usr/bin/node.
128+
if (( DRY_RUN )); then
129+
plan "sed s|@@NODE_BIN@@|${NODE_BIN}| ${UNIT_SRC} > /tmp/chittycommand-daemon.service"
130+
plan "install -m 0644 -o root -g root /tmp/chittycommand-daemon.service ${UNIT_DST}"
131+
else
132+
RENDERED_UNIT="$(mktemp)"
133+
sed "s|@@NODE_BIN@@|${NODE_BIN}|g" "${UNIT_SRC}" > "${RENDERED_UNIT}"
134+
install -m 0644 -o root -g root "${RENDERED_UNIT}" "${UNIT_DST}"
135+
rm -f "${RENDERED_UNIT}"
136+
fi
123137
run "systemctl daemon-reload"
124138
run "systemctl enable chittycommand-daemon.service"
125139

src/routes/documents.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,24 @@ documentRoutes.post('/upload', async (c) => {
104104
httpMetadata: { contentType: file.type },
105105
customMetadata: { filename: safeName, source: 'chittycommand' },
106106
});
107-
const [doc] = await sql`
107+
// Codex P2 PR#105: migration 0016 adds a unique partial index on r2_key,
108+
// so re-uploading an already-ingested sha256/* key would raise a unique-
109+
// violation 500 here. Match the batch path's ON CONFLICT semantics: skip
110+
// the insert, then SELECT the existing row to return.
111+
const inserted = await sql`
108112
INSERT INTO cc_documents (doc_type, source, filename, r2_key, processing_status)
109113
VALUES ('upload', 'manual', ${safeName}, ${r2Key}, 'pending')
114+
ON CONFLICT (r2_key) WHERE (r2_key IS NOT NULL) DO NOTHING
110115
RETURNING *
111116
`;
112-
return c.json(doc, 201);
117+
if (inserted.length > 0) {
118+
return c.json(inserted[0], 201);
119+
}
120+
// Existing row already had this r2_key — return it with 200 instead of 201.
121+
const [existing] = await sql`
122+
SELECT * FROM cc_documents WHERE r2_key = ${r2Key} LIMIT 1
123+
`;
124+
return c.json(existing, 200);
113125
});
114126

115127
// Batch upload via ChittyStorage

0 commit comments

Comments
 (0)