@meffecta/agent 1.0.9 → 1.0.10

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/engine.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "engineTag": "1.0.9",
3
- "builtFrom": "4769315eb49ce1036f888ffafb03881ec4e91131"
2
+ "engineTag": "1.0.10",
3
+ "builtFrom": "36d54c192cfa454f2e4485414ac854b3be86c8f7"
4
4
  }
package/lib/commands.js CHANGED
@@ -186,8 +186,35 @@ async function status() {
186
186
  );
187
187
  const external = Boolean(env.AGENT_TASKS_QUEUE && env.AGENT_PUBLIC_URL);
188
188
 
189
+ // What is RUNNING, asked of the service rather than inferred from the image tag — which
190
+ // is a moving target: `latest` names a different engine every time main is pushed, so the
191
+ // tag on the revision says what was requested and not what answers. /health is
192
+ // unauthenticated, so this needs no credential; a service that cannot be reached simply
193
+ // does not get the line.
194
+ let engine = "";
195
+ if (svc.status?.url) {
196
+ try {
197
+ const res = await fetch(`${svc.status.url}/health`, { signal: AbortSignal.timeout(25_000) });
198
+ const health = await res.json();
199
+ if (health?.version) {
200
+ engine = health.build ? `${health.version} (build ${health.build})` : health.version;
201
+ } else {
202
+ engine = "up, but too old to report its version";
203
+ }
204
+ } catch {
205
+ // Short timeout on purpose: this is a status command, and asking wakes a
206
+ // scale-to-zero service, so it waits about as long as a cold start and then says
207
+ // nothing rather than hanging. Not a verdict on health — `doctor` is where that is
208
+ // decided.
209
+ engine = "no answer within 25s (a sleeping service can take longer to wake)";
210
+ }
211
+ }
212
+
189
213
  console.log(`▶ ${d.SERVICE} (${d.PROJECT} / ${d.REGION})`);
190
214
  console.log(` url ${svc.status?.url ?? "?"}`);
215
+ if (engine) {
216
+ console.log(` engine ${engine}`);
217
+ }
191
218
  console.log(` revision ${svc.status?.latestReadyRevisionName ?? "?"}`);
192
219
  console.log(` image ${container.image}`);
193
220
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meffecta/agent",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Set up and operate a Meffecta Agent deployment — a self-hosted Claude Code job runner.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/deploy.sh CHANGED
@@ -160,6 +160,51 @@ gcloud run deploy "${SERVICE}" \
160
160
  --execution-environment gen2 \
161
161
  "${SCALE_FLAGS[@]}"
162
162
 
163
+ # What actually landed. `--tag latest` is a request, not an answer: it names whatever GHCR
164
+ # was serving a moment ago, and after this it names whatever it serves next. The digest is
165
+ # the immutable identity — and the one thing that makes a rollback to *this* engine possible
166
+ # in a month, when the tag has moved on.
167
+ read -r URL REVISION < <(gcloud run services describe "${SERVICE}" \
168
+ --project "${PROJECT}" --region "${REGION}" \
169
+ --format 'value[separator=" "](status.url, status.latestReadyRevisionName)' 2>/dev/null || echo " ")
170
+ DIGEST=""
171
+ if [ -n "${REVISION}" ]; then
172
+ DIGEST=$(gcloud run revisions describe "${REVISION}" \
173
+ --project "${PROJECT}" --region "${REGION}" \
174
+ --format 'value(status.imageDigest)' 2>/dev/null || true)
175
+ fi
176
+
177
+ # And which engine is answering. The digest says the image is different; only the service
178
+ # says what it is. This is also the first real request to the new revision, so a rollout
179
+ # that produced an image which cannot boot says so here rather than at the next cron —
180
+ # generously timed, because a scale-to-zero service is cold and this image is a large pull.
181
+ HEALTH=""
182
+ if [ -n "${URL}" ]; then
183
+ HEALTH=$(curl -fsS --max-time 180 "${URL}/health" 2>/dev/null || true)
184
+ fi
185
+ VERSION=$(printf '%s' "${HEALTH}" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
186
+ BUILD=$(printf '%s' "${HEALTH}" | sed -n 's/.*"build":"\([^"]*\)".*/\1/p')
187
+
188
+ echo ""
189
+ if [ -n "${VERSION}" ]; then
190
+ echo "✅ Running engine ${VERSION}${BUILD:+ (build ${BUILD})}"
191
+ elif [ -n "${HEALTH}" ]; then
192
+ # It answered, but without a version: an engine from before /health reported one.
193
+ echo "✅ Service is up — this engine predates version reporting, so it cannot say which it is"
194
+ else
195
+ echo "⚠️ Deployed, but ${SERVICE} did not answer /health within 3 minutes"
196
+ echo " Check it before assuming the rollout is good:"
197
+ if has_cli; then
198
+ echo " $(cmd logs)"
199
+ else
200
+ echo " gcloud run services logs read ${SERVICE} --project ${PROJECT} --region ${REGION} --limit 50"
201
+ fi
202
+ fi
203
+ [ -n "${REVISION}" ] && echo " revision ${REVISION}"
204
+ [ -n "${DIGEST}" ] && echo " image ${DIGEST##*@}"
205
+ [ -n "${DIGEST}" ] && echo " roll back to this exact engine later with:"
206
+ [ -n "${DIGEST}" ] && echo " $(cmd deploy) --image ${DIGEST}"
207
+
163
208
  # Only for a deployment that has opted into external triggers — AGENT_TASKS_QUEUE on the
164
209
  # service is what says so. Creating Scheduler jobs for an always-on one would mean every
165
210
  # cron fires twice, once from outside and once from the in-process timer.
@@ -170,10 +215,8 @@ if [ "${SYNC_SCHEDULER}" = true ] &&
170
215
  "$(dirname "${BASH_SOURCE[0]}")/sync-triggers.sh" ${CONFIG_FILE:+--config "${CONFIG_FILE}"}
171
216
  fi
172
217
 
173
- URL=$(gcloud run services describe "${SERVICE}" \
174
- --project "${PROJECT}" --region "${REGION}" \
175
- --format 'value(status.url)' 2>/dev/null || true)
176
218
  if [ -n "${URL}" ]; then
219
+ echo ""
177
220
  echo "🌍 ${SERVICE} → ${URL}"
178
221
  echo " Liveness: curl -fsS ${URL}/health"
179
222
  echo " Ask it: ${URL}/ask (any username, AGENT_API_SECRET as the password)"
@@ -742,9 +742,8 @@ if (unknownCredentials.length) {
742
742
  // knowing anything about the systems themselves: the register travels into every run, so
743
743
  // it is right here in the working directory. A credential named in it is deliberate, and
744
744
  // proving it works is Part 2's job; one named nowhere is a key no skill will ever reach
745
- // for, however valid it is. Printing the whole list as a to-do told the operator to
746
- // register things that were already registered, which is how a real gap gets skimmed
747
- // past.
745
+ // for, however valid it is. Splitting them matters because a real gap listed among
746
+ // things that are already fine is a gap that gets skimmed past.
748
747
  const registered = new Set();
749
748
  const registerDir = join(process.cwd(), "systems");
750
749
  if (existsSync(registerDir)) {