@topolo/cli 0.13.17 → 0.13.21
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/README.md +18 -4
- package/agent-packs/README.md +469 -1
- package/agent-packs/coverage_changed.py +1 -1
- package/agent-packs/docs/selected-artifacts.md +47 -0
- package/agent-packs/documentation/cloud-task-state.md +56 -0
- package/agent-packs/domain-contexts/README.md +25 -0
- package/agent-packs/domain-contexts/cicd.json +8 -0
- package/agent-packs/gui/feed-adapter.js +4 -0
- package/agent-packs/gui/index.html +9 -0
- package/agent-packs/gui/studio.css +14 -0
- package/agent-packs/gui/studio.js +121 -0
- package/agent-packs/gui/studio_server.py +355 -0
- package/agent-packs/roles/cicd/CLAUDE.md +24 -0
- package/agent-packs/specializations/cicd--cicd.json +5 -0
- package/agent-packs/specializations/cicd--pipeline-engineering.json +5 -0
- package/agent-packs/specializations/cicd--promotion-deployment.json +5 -0
- package/agent-packs/specializations/cicd--queue-recovery.json +5 -0
- package/agent-packs/specializations/cicd--release-verification.json +5 -0
- package/agent-packs/specializations/cicd--runner-capacity.json +5 -0
- package/agent-packs/specializations/intakes--engineering-02.json +5 -0
- package/agent-packs/specializations/intakes--engineering-03.json +5 -0
- package/agent-packs/specializations/intakes--engineering.json +5 -0
- package/agent-packs/specializations/intakes--operator-01.json +5 -0
- package/agent-packs/specializations/intakes--operator-02.json +5 -0
- package/agent-packs/specializations/operator-skills--audio-and-narration-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--brand-and-copywriting-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--capability-research-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--data-analysis-and-visualization.json +5 -0
- package/agent-packs/specializations/operator-skills--financial-evidence-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operating-template-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skill-library.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-skills.json +5 -0
- package/agent-packs/specializations/operator-skills--operator-workflow-evaluation.json +5 -0
- package/agent-packs/specializations/operator-skills--privacy-and-agreement-review.json +5 -0
- package/agent-packs/specializations/operator-skills--storytelling-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--ux-and-accessibility-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--video-production-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--visual-design-specialist.json +5 -0
- package/agent-packs/specializations/operator-skills--workflow-integration-specialist.json +5 -0
- package/agent-packs/specializations/operators--access-and-privacy.json +5 -0
- package/agent-packs/specializations/operators--accounting-evidence-review.json +5 -0
- package/agent-packs/specializations/operators--agreement-operations.json +5 -0
- package/agent-packs/specializations/operators--books-operator.json +5 -0
- package/agent-packs/specializations/operators--brand-operator.json +5 -0
- package/agent-packs/specializations/operators--commerce-operations.json +5 -0
- package/agent-packs/specializations/operators--communications-operator.json +5 -0
- package/agent-packs/specializations/operators--content-and-assets-operator.json +5 -0
- package/agent-packs/specializations/operators--customer-operations.json +5 -0
- package/agent-packs/specializations/operators--design-operator.json +5 -0
- package/agent-packs/specializations/operators--design-production-02.json +5 -0
- package/agent-packs/specializations/operators--design-production-03.json +5 -0
- package/agent-packs/specializations/operators--design-production-04.json +5 -0
- package/agent-packs/specializations/operators--device-and-feed-operator.json +5 -0
- package/agent-packs/specializations/operators--director-operator.json +5 -0
- package/agent-packs/specializations/operators--experience-review.json +5 -0
- package/agent-packs/specializations/operators--growth-operations.json +5 -0
- package/agent-packs/specializations/operators--learn-operator.json +5 -0
- package/agent-packs/specializations/operators--localization-operator.json +5 -0
- package/agent-packs/specializations/operators--operators.json +5 -0
- package/agent-packs/specializations/operators--pay-operator.json +5 -0
- package/agent-packs/specializations/operators--research-and-intelligence.json +5 -0
- package/agent-packs/specializations/operators--roadmap-operator.json +5 -0
- package/agent-packs/specializations/operators--social-content-operator.json +5 -0
- package/agent-packs/specializations/operators--talent-operations.json +5 -0
- package/agent-packs/specializations/operators--web-operator.json +5 -0
- package/agent-packs/specializations/operators--workflow-operator.json +5 -0
- package/agent-packs/specializations/specialists--adversarial-qa.json +5 -0
- package/agent-packs/specializations/specialists--backlog-dispatch.json +5 -0
- package/agent-packs/specializations/specialists--build-01.json +5 -0
- package/agent-packs/specializations/specialists--build-02.json +5 -0
- package/agent-packs/specializations/specialists--build-03.json +5 -0
- package/agent-packs/specializations/specialists--developer-environment-steward.json +5 -0
- package/agent-packs/specializations/specialists--documentation-knowledge-steward.json +5 -0
- package/agent-packs/specializations/specialists--execution-readiness.json +5 -0
- package/agent-packs/specializations/specialists--incident-triage.json +5 -0
- package/agent-packs/specializations/specialists--platform-package-steward.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-01.json +5 -0
- package/agent-packs/specializations/specialists--product-direction-02.json +5 -0
- package/agent-packs/specializations/specialists--release-recovery.json +5 -0
- package/agent-packs/specializations/specialists--release-watch.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-01.json +5 -0
- package/agent-packs/specializations/specialists--roadmap-planning-02.json +5 -0
- package/agent-packs/specializations/specialists--security-permissions-review.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-01.json +5 -0
- package/agent-packs/specializations/specialists--system-architecture-02.json +5 -0
- package/agent-packs/specializations/specialists--translation.json +5 -0
- package/agent-packs/specializations/specialists--ux-consistency-review.json +5 -0
- package/agent-packs/specializations/specialists--workflow-improvement.json +5 -0
- package/agent-packs/specializations/specialists--worktree-retirement.json +5 -0
- package/agent-packs/src/topolo_agent_pack/cicd.py +101 -0
- package/agent-packs/src/topolo_agent_pack/claude.py +22 -7
- package/agent-packs/src/topolo_agent_pack/claude_activation.py +157 -0
- package/agent-packs/src/topolo_agent_pack/claude_completion_recovery.py +216 -0
- package/agent-packs/src/topolo_agent_pack/claude_desktop.py +181 -0
- package/agent-packs/src/topolo_agent_pack/claude_dispatch.py +240 -0
- package/agent-packs/src/topolo_agent_pack/claude_prompt.py +74 -0
- package/agent-packs/src/topolo_agent_pack/claude_runner.py +207 -0
- package/agent-packs/src/topolo_agent_pack/claude_service.py +397 -0
- package/agent-packs/src/topolo_agent_pack/cli.py +188 -18
- package/agent-packs/src/topolo_agent_pack/closed_ledger_fence.py +118 -0
- package/agent-packs/src/topolo_agent_pack/cloud_attach.py +128 -0
- package/agent-packs/src/topolo_agent_pack/cloud_cutover.py +134 -0
- package/agent-packs/src/topolo_agent_pack/cloud_delta.py +162 -0
- package/agent-packs/src/topolo_agent_pack/cloud_migrate.py +142 -0
- package/agent-packs/src/topolo_agent_pack/cloud_recovery.py +42 -0
- package/agent-packs/src/topolo_agent_pack/cloud_work.py +126 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_activity.py +27 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_context.py +91 -0
- package/agent-packs/src/topolo_agent_pack/coordinator_refresh.py +116 -0
- package/agent-packs/src/topolo_agent_pack/desktop_acceptance.py +45 -0
- package/agent-packs/src/topolo_agent_pack/desktop_launcher.py +114 -0
- package/agent-packs/src/topolo_agent_pack/domain_context.py +38 -0
- package/agent-packs/src/topolo_agent_pack/execution.py +104 -7
- package/agent-packs/src/topolo_agent_pack/fresh.py +1 -1
- package/agent-packs/src/topolo_agent_pack/frontdoor.py +23 -0
- package/agent-packs/src/topolo_agent_pack/linked_task_context.py +80 -0
- package/agent-packs/src/topolo_agent_pack/live.py +285 -18
- package/agent-packs/src/topolo_agent_pack/native.py +160 -4
- package/agent-packs/src/topolo_agent_pack/policy.py +8 -0
- package/agent-packs/src/topolo_agent_pack/review_context.py +33 -0
- package/agent-packs/src/topolo_agent_pack/role_catalog.py +137 -0
- package/agent-packs/src/topolo_agent_pack/role_context.py +63 -0
- package/agent-packs/src/topolo_agent_pack/role_refresh.py +50 -0
- package/agent-packs/src/topolo_agent_pack/task_models.py +11 -7
- package/agent-packs/src/topolo_agent_pack/throughput.py +149 -0
- package/agent-packs/src/topolo_agent_pack/topolo.py +7 -1
- package/agent-packs/src/topolo_agent_pack/work.py +322 -0
- package/agent-packs/src/topolo_agent_pack/work_adoption.py +122 -0
- package/agent-packs/src/topolo_agent_pack/work_artifacts.py +84 -0
- package/agent-packs/src/topolo_agent_pack/work_context.py +77 -0
- package/agent-packs/src/topolo_agent_pack/work_gui.py +51 -0
- package/agent-packs/src/topolo_agent_pack/work_native.py +414 -0
- package/agent-packs/src/topolo_agent_pack/work_wait.py +151 -0
- package/agent-packs/templates/topolo-coordinators/AGENTS.md +7 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/AGENTS.md +6 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/cicd/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/engineering/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/AGENTS.md +3 -0
- package/agent-packs/templates/topolo-coordinators/agents/intakes/operator-01/config.json +4 -0
- package/agent-packs/templates/topolo-coordinators/pack.json +56 -0
- package/dist/chunk-NYDIAYVY.js +144 -0
- package/dist/{claude-pack-backend-YEVPAOID.js → chunk-UY5LEU5C.js} +1659 -21
- package/dist/claude-desktop-profile-JCQKSY3K.js +13 -0
- package/dist/claude-pack-activation-Q25MCEPU.js +506 -0
- package/dist/claude-pack-backend-HUTKGIDS.js +90 -0
- package/dist/index.js +408 -164
- package/package.json +8 -6
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Bounded allocation decisions, without claim writes, wakeups or ownership changes.
|
|
2
|
+
|
|
3
|
+
Callers supply current canonical and native observations, then atomically claim
|
|
4
|
+
against snapshotDigest before dispatch. A plan is never evidence of execution.
|
|
5
|
+
"""
|
|
6
|
+
from .storage import PackError
|
|
7
|
+
from .task_models import digest, resolve
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _require(condition, message):
|
|
11
|
+
if not condition:
|
|
12
|
+
raise PackError(message)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _keys(value, fields):
|
|
16
|
+
_require(isinstance(value, dict) and set(value) == set(fields.split()),
|
|
17
|
+
'Allocation input requires exact fields: ' + fields)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _text(value):
|
|
21
|
+
return isinstance(value, str) and 0 < len(value) <= 240
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _integer(value, minimum, maximum):
|
|
25
|
+
return type(value) is int and minimum <= value <= maximum
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _names(value):
|
|
29
|
+
return (isinstance(value, list) and len(value) <= 256 and
|
|
30
|
+
all(_text(item) for item in value) and len(value) == len(set(value)))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def allocate(snapshot):
|
|
34
|
+
"""Choose compatible idle workers fairly; isolate stale/blocked work.
|
|
35
|
+
|
|
36
|
+
Observations are supplied evidence, not a native status probe. Existing
|
|
37
|
+
owners stay pinned. Retained custody is reusable only at an explicit safe
|
|
38
|
+
checkpoint with the actual native worker idle. Resource slots are separate
|
|
39
|
+
from chat availability and apply to both existing and proposed workers.
|
|
40
|
+
"""
|
|
41
|
+
_keys(snapshot, 'schemaVersion now maxAgeSeconds slots maxNewLanes tasks lanes completed configuration catalog')
|
|
42
|
+
_require(snapshot['schemaVersion'] == 1 and
|
|
43
|
+
_integer(snapshot['now'], 1, 2**53) and
|
|
44
|
+
_integer(snapshot['maxAgeSeconds'], 1, 3600) and
|
|
45
|
+
_integer(snapshot['slots'], 0, 32) and
|
|
46
|
+
_integer(snapshot['maxNewLanes'], 0, 8) and
|
|
47
|
+
_names(snapshot['completed']), 'Invalid allocation bounds')
|
|
48
|
+
tasks, lanes = snapshot['tasks'], snapshot['lanes']
|
|
49
|
+
_require(isinstance(tasks, list) and len(tasks) <= 256 and
|
|
50
|
+
isinstance(lanes, list) and len(lanes) <= 256, 'Bounded task and lane lists required')
|
|
51
|
+
task_ids, lane_ids = set(), set()
|
|
52
|
+
for task in tasks:
|
|
53
|
+
_keys(task, 'id capability priority owner dependencies authorized paused modelTask')
|
|
54
|
+
_require(_text(task['id']) and task['id'] not in task_ids and
|
|
55
|
+
_text(task['capability']) and _integer(task['priority'], 0, 9) and
|
|
56
|
+
(task['owner'] is None or _text(task['owner'])) and
|
|
57
|
+
_names(task['dependencies']) and task['id'] not in task['dependencies'] and
|
|
58
|
+
type(task['authorized']) is bool and type(task['paused']) is bool,
|
|
59
|
+
'Malformed or duplicate allocation task')
|
|
60
|
+
task_ids.add(task['id'])
|
|
61
|
+
for lane in lanes:
|
|
62
|
+
_keys(lane, 'id capabilities observedAt registryState nativeState activeTask retainedTasks reusableCheckpoint assignments lastAssigned constraints')
|
|
63
|
+
_require(_text(lane['id']) and lane['id'] not in lane_ids and
|
|
64
|
+
_names(lane['capabilities']) and _names(lane['retainedTasks']) and
|
|
65
|
+
_integer(lane['observedAt'], 1, 2**53) and
|
|
66
|
+
lane['registryState'] in {'idle', 'busy', 'held', 'paused', 'unknown'} and
|
|
67
|
+
lane['nativeState'] in {'idle', 'busy', 'unknown'} and
|
|
68
|
+
(lane['activeTask'] is None or _text(lane['activeTask'])) and
|
|
69
|
+
type(lane['reusableCheckpoint']) is bool and
|
|
70
|
+
_integer(lane['assignments'], 0, 2**53) and
|
|
71
|
+
_integer(lane['lastAssigned'], 0, 2**53), 'Malformed or duplicate allocation lane')
|
|
72
|
+
lane_ids.add(lane['id'])
|
|
73
|
+
completed = set(snapshot['completed'])
|
|
74
|
+
_require(not completed.intersection(task_ids), 'Completed tasks must not remain in the ready queue')
|
|
75
|
+
result = {'format': 'topolo-allocation-plan', 'schemaVersion': 1,
|
|
76
|
+
'snapshotDigest': digest(snapshot), 'assignments': [], 'createRequests': [],
|
|
77
|
+
'blocked': [], 'laneIssues': [], 'effectsStarted': False,
|
|
78
|
+
'executionRequirements': ['current exclusive claim against snapshotDigest',
|
|
79
|
+
'native model and effort acknowledgement',
|
|
80
|
+
'verified authorized task delivery and result readback']}
|
|
81
|
+
eligible = []
|
|
82
|
+
for lane in lanes:
|
|
83
|
+
age = snapshot['now'] - lane['observedAt']
|
|
84
|
+
issue = None
|
|
85
|
+
if age < 0 or age > snapshot['maxAgeSeconds']:
|
|
86
|
+
issue = 'stale-observation'
|
|
87
|
+
elif lane['registryState'] in {'paused', 'unknown'} or lane['nativeState'] == 'unknown':
|
|
88
|
+
issue = 'paused-or-unknown'
|
|
89
|
+
elif lane['activeTask'] is not None or lane['nativeState'] == 'busy':
|
|
90
|
+
issue = 'occupied'
|
|
91
|
+
elif lane['registryState'] == 'busy':
|
|
92
|
+
issue = 'reconcile-registry-with-idle-native'
|
|
93
|
+
elif lane['retainedTasks'] or lane['registryState'] == 'held':
|
|
94
|
+
if not lane['reusableCheckpoint']:
|
|
95
|
+
issue = 'custody-checkpoint-required'
|
|
96
|
+
if issue:
|
|
97
|
+
result['laneIssues'].append({'laneId': lane['id'], 'reason': issue})
|
|
98
|
+
else:
|
|
99
|
+
eligible.append(lane)
|
|
100
|
+
slots = snapshot['slots']
|
|
101
|
+
for task in sorted(tasks, key=lambda row: (row['priority'], row['id'])):
|
|
102
|
+
reason = None
|
|
103
|
+
if task['paused']:
|
|
104
|
+
reason = 'task-paused'
|
|
105
|
+
elif not task['authorized']:
|
|
106
|
+
reason = 'task-unauthorized'
|
|
107
|
+
elif not set(task['dependencies']).issubset(completed):
|
|
108
|
+
reason = 'dependencies-pending'
|
|
109
|
+
elif not slots:
|
|
110
|
+
reason = 'execution-capacity-exhausted'
|
|
111
|
+
if reason:
|
|
112
|
+
result['blocked'].append({'taskId': task['id'], 'reason': reason})
|
|
113
|
+
continue
|
|
114
|
+
compatible = [lane for lane in lanes if task['capability'] in lane['capabilities'] and
|
|
115
|
+
(task['owner'] is None or lane['id'] == task['owner'])]
|
|
116
|
+
candidates = sorted((lane for lane in compatible if lane in eligible),
|
|
117
|
+
key=lambda row: (row['assignments'], row['lastAssigned'], row['id']))
|
|
118
|
+
failures = []
|
|
119
|
+
for lane in candidates:
|
|
120
|
+
try:
|
|
121
|
+
resolution = resolve(snapshot['configuration'], task['modelTask'],
|
|
122
|
+
snapshot['catalog'].get('harness'), snapshot['catalog'], lane['constraints'])
|
|
123
|
+
except (PackError, AttributeError) as error:
|
|
124
|
+
failures.append({'laneId': lane['id'], 'reason': str(error)})
|
|
125
|
+
continue
|
|
126
|
+
result['assignments'].append({'taskId': task['id'], 'laneId': lane['id'],
|
|
127
|
+
'resolution': resolution,
|
|
128
|
+
'retainedTasks': list(lane['retainedTasks'])})
|
|
129
|
+
eligible.remove(lane)
|
|
130
|
+
slots -= 1
|
|
131
|
+
break
|
|
132
|
+
else:
|
|
133
|
+
uncertain = any(issue['laneId'] in {lane['id'] for lane in compatible} and
|
|
134
|
+
issue['reason'] in {'stale-observation', 'paused-or-unknown',
|
|
135
|
+
'reconcile-registry-with-idle-native'}
|
|
136
|
+
for issue in result['laneIssues'])
|
|
137
|
+
if (task['owner'] is None and not candidates and not uncertain and
|
|
138
|
+
len(result['createRequests']) < snapshot['maxNewLanes']):
|
|
139
|
+
result['createRequests'].append({'taskId': task['id'], 'capability': task['capability'],
|
|
140
|
+
'reason': 'no-compatible-free-lane',
|
|
141
|
+
'requires': 'registered native lane and verified task model before claim'})
|
|
142
|
+
slots -= 1
|
|
143
|
+
else:
|
|
144
|
+
result['blocked'].append({'taskId': task['id'],
|
|
145
|
+
'reason': ('model-unavailable' if failures else
|
|
146
|
+
'lane-state-reconciliation-required' if uncertain else
|
|
147
|
+
'owner-or-capability-unavailable'),
|
|
148
|
+
'modelFailures': failures})
|
|
149
|
+
return result
|
|
@@ -51,7 +51,7 @@ class Client:
|
|
|
51
51
|
if any(selected.get(k) for k in ['TOPOLO_API_KEY','TOPOLO_ACCESS_TOKEN','TOPOLO_'+environment.upper()+'_API_KEY','TOPOLO_'+environment.upper()+'_ACCESS_TOKEN']):
|
|
52
52
|
raise PackError('Pinned connections require the CLI protected account context, not injected credentials')
|
|
53
53
|
self.environment=environment;self.context=context
|
|
54
|
-
self.binary=binary or shutil.which('topolo')
|
|
54
|
+
self.binary=binary or shutil.which('topolo') or next((str(p) for p in [pathlib.Path('/opt/homebrew/bin/topolo'),pathlib.Path('/usr/local/bin/topolo')] if p.is_file() and os.access(p,os.X_OK)),None)
|
|
55
55
|
if not self.binary:
|
|
56
56
|
raise PackError('Install the public @topolo/cli and authenticate before connecting')
|
|
57
57
|
self.env={**selected,'TOPOLO_CONTEXT':context}
|
|
@@ -81,6 +81,12 @@ class Client:
|
|
|
81
81
|
if upload:
|
|
82
82
|
result=self.run(['actions','upload',*common,'--file',str(upload),'--content-type',input.get('type','application/octet-stream'),'--confirm'])
|
|
83
83
|
else:result=self.run(['actions','call',*common,'--detail',*(['--confirm'] if write else [])])
|
|
84
|
+
# Public CLI action calls wrap the full service envelope in result.
|
|
85
|
+
# Do not mistake that transport wrapper for the actual cloud record.
|
|
86
|
+
if isinstance(result,dict) and 'actionId' in result and 'result' in result:
|
|
87
|
+
if result['actionId']!=action:
|
|
88
|
+
raise PackError('Topolo action response identity mismatch')
|
|
89
|
+
result=result['result']
|
|
84
90
|
if isinstance(result,dict) and result.get('success') is False:
|
|
85
91
|
raise PackError('Topolo action failed')
|
|
86
92
|
return result.get('data',result) if isinstance(result,dict) else result
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Durable task records and single-use native workers; no model or transport mocks.
|
|
2
|
+
|
|
3
|
+
The SQLite record owns continuity. Native threads are disposable execution slots.
|
|
4
|
+
A reserved/uncertain send is never replayed by process recovery.
|
|
5
|
+
"""
|
|
6
|
+
import copy
|
|
7
|
+
import hashlib
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import re
|
|
12
|
+
import sqlite3
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
from contextlib import contextmanager
|
|
16
|
+
from .storage import PackError, canonical
|
|
17
|
+
|
|
18
|
+
STATES={'queued','routing','executing','reviewing','completed','needs_input','blocked','failed','unknown'}
|
|
19
|
+
TERMINAL={'completed','needs_input','blocked','failed','unknown'}
|
|
20
|
+
|
|
21
|
+
def require(ok,message):
|
|
22
|
+
if not ok:raise PackError(message)
|
|
23
|
+
|
|
24
|
+
def text(value,limit=16000):
|
|
25
|
+
return isinstance(value,str) and 0<len(value.strip())<=limit
|
|
26
|
+
|
|
27
|
+
class Store:
|
|
28
|
+
def __init__(self,root):
|
|
29
|
+
self.root=pathlib.Path(root)/'work';self.root.mkdir(mode=0o700,exist_ok=True)
|
|
30
|
+
require(not self.root.is_symlink() and self.root.stat().st_uid==os.getuid() and not self.root.stat().st_mode&0o022,'Work store must be privately owned')
|
|
31
|
+
self.path=self.root/'tasks.sqlite'
|
|
32
|
+
require(not self.path.is_symlink(),'Work database cannot be a symlink')
|
|
33
|
+
with self.db() as db:
|
|
34
|
+
db.executescript('''CREATE TABLE IF NOT EXISTS tasks(id TEXT PRIMARY KEY, input_hash TEXT NOT NULL, body TEXT NOT NULL);
|
|
35
|
+
CREATE TABLE IF NOT EXISTS workers(id TEXT PRIMARY KEY,task_id TEXT NOT NULL,body TEXT NOT NULL);
|
|
36
|
+
CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, task_id TEXT NOT NULL, body TEXT NOT NULL);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS retained(id TEXT PRIMARY KEY,body TEXT NOT NULL);''')
|
|
38
|
+
db.execute('CREATE TABLE IF NOT EXISTS responses(id TEXT PRIMARY KEY,input_hash TEXT NOT NULL,task_id TEXT NOT NULL)')
|
|
39
|
+
self.path.chmod(0o600)
|
|
40
|
+
@contextmanager
|
|
41
|
+
def db(self):
|
|
42
|
+
require(not (self.root/'cutover.json').exists(),'Legacy work is durably fenced for cloud cutover')
|
|
43
|
+
db=sqlite3.connect(self.path,timeout=10)
|
|
44
|
+
try:
|
|
45
|
+
db.execute('BEGIN IMMEDIATE')
|
|
46
|
+
require(not (self.root/'cutover.json').exists(),'Legacy work is durably fenced for cloud cutover')
|
|
47
|
+
yield db;db.commit()
|
|
48
|
+
except BaseException:db.rollback();raise
|
|
49
|
+
finally:db.close()
|
|
50
|
+
def event(self,db,tid,kind,summary):
|
|
51
|
+
db.execute('INSERT INTO events(task_id,body) VALUES (?,?)',(tid,json.dumps({'taskId':tid,'kind':kind,'summary':summary,'at':time.time()})))
|
|
52
|
+
def submit(self,request):
|
|
53
|
+
require(isinstance(request,dict) and set(request)=={'requestId','objective','channel','canonicalClaimId'},'Exact work request fields required')
|
|
54
|
+
try:valid=str(uuid.UUID(request['requestId']))==request['requestId']
|
|
55
|
+
except (ValueError,TypeError,AttributeError):valid=False
|
|
56
|
+
require(valid and text(request['objective']) and request['channel'] in {'engineering','operations','cicd'} and (request['canonicalClaimId'] is None or text(request['canonicalClaimId'],240)),'Invalid work request')
|
|
57
|
+
identity=request['requestId'];fingerprint=hashlib.sha256(canonical(request)).hexdigest()
|
|
58
|
+
with self.db() as db:
|
|
59
|
+
row=db.execute('SELECT input_hash,body FROM tasks WHERE id=?',(identity,)).fetchone()
|
|
60
|
+
if row:
|
|
61
|
+
require(row[0]==fingerprint,'Request ID already has a different objective');return json.loads(row[1]),False
|
|
62
|
+
claim=request['canonicalClaimId']
|
|
63
|
+
if claim:
|
|
64
|
+
retained=db.execute('SELECT body FROM retained WHERE id=?',(claim,)).fetchone()
|
|
65
|
+
require(retained is None,'Existing claim retains its owner and checkpoint; per-task adoption is required before dispatch')
|
|
66
|
+
raise PackError('Unknown canonical claim; reconcile its current owner before dispatch')
|
|
67
|
+
task={**copy.deepcopy(request),'id':identity,'revision':1,'attempt':1,'state':'queued','phase':'route','summary':'Request recorded; routing pending.','createdAt':time.time(),'updatedAt':time.time(),'deliveryOwner':'task:'+identity,'workerIds':[],'checkpoints':[]}
|
|
68
|
+
db.execute('INSERT INTO tasks VALUES (?,?,?)',(identity,fingerprint,json.dumps(task)));self.event(db,identity,'requested',task['summary']);return task,True
|
|
69
|
+
def get(self,tid):
|
|
70
|
+
with self.db() as db:
|
|
71
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');return json.loads(row[0])
|
|
72
|
+
def adopt_retained(self,request,prepared,receipt,parent_proof=None):
|
|
73
|
+
"""Commit a broker-verified receipt and unique continuation atomically.
|
|
74
|
+
|
|
75
|
+
Retained inventory is immutable. The original delivery task, source
|
|
76
|
+
owner, failed jobs and all uncertain effects retain their custody.
|
|
77
|
+
"""
|
|
78
|
+
from .work_adoption import validate,digest,request_digest
|
|
79
|
+
validate(request)
|
|
80
|
+
require(prepared.get('requestSHA256')==request_digest(request)
|
|
81
|
+
and receipt.get('planSHA256')==digest(prepared)
|
|
82
|
+
and receipt.get('authority')=='current-authenticated-human-instruction'
|
|
83
|
+
and receipt.get('scope')==prepared.get('scope')=='read-only-checkpoint-diagnostic',
|
|
84
|
+
'Verified current human diagnostic authorization required')
|
|
85
|
+
identity=request['requestId'];fingerprint=digest(request)
|
|
86
|
+
with self.db() as db:
|
|
87
|
+
row=db.execute('SELECT input_hash,body FROM tasks WHERE id=?',(identity,)).fetchone()
|
|
88
|
+
if row:
|
|
89
|
+
require(row[0]==fingerprint,'Request ID already has a different adoption')
|
|
90
|
+
task=json.loads(row[1])
|
|
91
|
+
require(task.get('adoption',{}).get('receipt')==receipt,'Adoption receipt differs')
|
|
92
|
+
return task,False
|
|
93
|
+
row=db.execute('SELECT body FROM retained WHERE id=?',(request['canonicalClaimId'],)).fetchone()
|
|
94
|
+
require(row is not None and digest(json.loads(row[0]))==prepared['retainedSHA256'],
|
|
95
|
+
'Retained claim changed; preserve original custody')
|
|
96
|
+
tasks=[json.loads(r[0]) for r in db.execute('SELECT body FROM tasks')]
|
|
97
|
+
require(not any(t.get('canonicalClaimId')==request['canonicalClaimId'] for t in tasks),
|
|
98
|
+
'Canonical claim already has a continuation; no duplicate dispatch')
|
|
99
|
+
related={t['id'] for t in tasks if t.get('canonicalClaimId')==request['canonicalClaimId']}|{request['continuationOwnerTaskId']}
|
|
100
|
+
workers=[json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
|
|
101
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True for w in workers if w['taskId'] in related),
|
|
102
|
+
'Current cloud worker retains active or unresolved custody')
|
|
103
|
+
parents=[t for t in tasks if t['id']==request['continuationOwnerTaskId']]
|
|
104
|
+
require(len(parents)==1 and parents[0]['channel']=='engineering'
|
|
105
|
+
and parents[0]['state'] not in {'unknown','completed','failed'},
|
|
106
|
+
'Originating delivery task is missing, closed or uncertain')
|
|
107
|
+
parent=parents[0]
|
|
108
|
+
require(parent['revision']==request['continuationRevision'],'Parent revision changed; reread before adoption')
|
|
109
|
+
from .work_adoption import parent_heads
|
|
110
|
+
owned_workers=[w for w in workers if w['taskId']==parent['id']]
|
|
111
|
+
require(digest(parent_heads(parent,owned_workers))==request['parentCheckpointSHA256'],'Parent checkpoint heads changed')
|
|
112
|
+
if parent.get('ownerInstallationId')!=receipt['installationId']:
|
|
113
|
+
require(parent_proof is not None and parent['state'] in {'blocked','needs_input'}
|
|
114
|
+
and parent_proof['fromInstallationId']==parent.get('ownerInstallationId')
|
|
115
|
+
and parent_proof['toInstallationId']==receipt['installationId']
|
|
116
|
+
and parent_proof['revision']==parent['revision']
|
|
117
|
+
and parent_proof['checkpointHeadsSHA256']==request['parentCheckpointSHA256'],
|
|
118
|
+
'Closed parent cloud checkpoint transfer is unverified')
|
|
119
|
+
parent['ownerInstallationId']=receipt['installationId']
|
|
120
|
+
self.event(db,parent['id'],'closed-parent-owner-adopted','Current human authorized portable checkpoint ownership; parent state and holds unchanged.')
|
|
121
|
+
task={k:copy.deepcopy(request[k]) for k in ['requestId','objective','channel','canonicalClaimId']}
|
|
122
|
+
task.update(id=identity,revision=1,attempt=1,state='executing',phase='execute',
|
|
123
|
+
summary='Current human authorized diagnostic-only continuation; original source and UNKNOWN holds preserved.',
|
|
124
|
+
createdAt=time.time(),updatedAt=time.time(),
|
|
125
|
+
deliveryOwner=parents[0]['deliveryOwner'],workerIds=[],checkpoints=[],
|
|
126
|
+
route={'role':request['role'],'task':{'type':'retained-checkpoint-diagnostic',
|
|
127
|
+
'complexity':'complex','risk':'security-critical'},'acceptance':request['acceptance']},
|
|
128
|
+
adoption={'request':copy.deepcopy(request),'plan':copy.deepcopy(prepared),'receipt':copy.deepcopy(receipt),'parentTransfer':copy.deepcopy(parent_proof)},
|
|
129
|
+
effectBoundary='Read-only checkpoint diagnosis. Source, tests, provider writes and releases require their separate existing gates.')
|
|
130
|
+
db.execute('INSERT INTO tasks VALUES (?,?,?)',(identity,fingerprint,json.dumps(task)))
|
|
131
|
+
parent=parents[0];parent.setdefault('continuations',[]).append({'taskId':identity,'canonicalClaimId':request['canonicalClaimId']})
|
|
132
|
+
parent.update(revision=parent['revision']+1,updatedAt=time.time())
|
|
133
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(parent),parent['id']))
|
|
134
|
+
self.event(db,identity,'retained-checkpoint-adopted',task['summary'])
|
|
135
|
+
self.event(db,parent['id'],'continuation-linked','Canonical claim checkpoint continuation linked; delivery acceptance remains open.')
|
|
136
|
+
return task,True
|
|
137
|
+
def list(self):
|
|
138
|
+
with self.db() as db:return [json.loads(x[0]) for x in db.execute('SELECT body FROM tasks ORDER BY rowid DESC LIMIT 500')]
|
|
139
|
+
def update(self,tid,revision,changes,kind):
|
|
140
|
+
require(isinstance(changes,dict) and not(set(changes)-{'state','phase','summary','route','workerIds','checkpoints','result','verification','archiveError','contextRef'}),'Unsupported work transition')
|
|
141
|
+
if 'state' in changes:require(changes['state'] in STATES,'Unknown work state')
|
|
142
|
+
with self.db() as db:
|
|
143
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0]);require(task['revision']==revision,'Task changed; reread before transition')
|
|
144
|
+
task.update(copy.deepcopy(changes));task.update(revision=revision+1,updatedAt=time.time());db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,kind,task['summary']);return task
|
|
145
|
+
def re_review(self,request):
|
|
146
|
+
require(isinstance(request,dict) and set(request)=={'requestId','taskId','revision','reviewWorkerId','reviewCheckpoint','note'},'Exact review-only continuation required')
|
|
147
|
+
require(type(request['revision']) is int and text(request['requestId'],100) and text(request['note'],8000),'Review continuation identity and reason required')
|
|
148
|
+
fingerprint=hashlib.sha256(canonical(request)).hexdigest()
|
|
149
|
+
with self.db() as db:
|
|
150
|
+
previous=db.execute('SELECT input_hash,task_id FROM responses WHERE id=?',(request['requestId'],)).fetchone()
|
|
151
|
+
if previous:
|
|
152
|
+
require(previous==(fingerprint,request['taskId']),'Review continuation identity already used')
|
|
153
|
+
return json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()[0]),False
|
|
154
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()
|
|
155
|
+
require(row is not None,'Unknown review task');task=json.loads(row[0])
|
|
156
|
+
require(task['revision']==request['revision'] and task['state'] in {'blocked','needs_input'}
|
|
157
|
+
and task.get('phase')=='review','Task is not the exact rejected review checkpoint')
|
|
158
|
+
workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(task['id'],))]
|
|
159
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in workers),'Existing native closure is unresolved; no review continuation')
|
|
160
|
+
rejected=[w for w in workers if w['id']==request['reviewWorkerId']]
|
|
161
|
+
require(len(rejected)==1 and rejected[0]['phase']=='review' and rejected[0]['state']=='archived'
|
|
162
|
+
and rejected[0].get('nativeArchived') is True
|
|
163
|
+
and rejected[0].get('attempt',1)==task.get('attempt',1)
|
|
164
|
+
and rejected[0].get('reviewGeneration',0)==task.get('reviewGeneration',0)
|
|
165
|
+
and rejected[0].get('checkpoint')==request['reviewCheckpoint']
|
|
166
|
+
and task.get('verification',{}).get('nativeCheckpoint')==request['reviewCheckpoint']
|
|
167
|
+
and task['verification']['verdict'] in {'blocked','needs_input'},'Exact latest rejected review must be preserved')
|
|
168
|
+
result=task.get('result',{});execution=[w for w in workers if w['id']==result.get('workerId')]
|
|
169
|
+
require(len(execution)==1 and execution[0]['phase']=='execute' and execution[0]['state']=='archived'
|
|
170
|
+
and execution[0].get('nativeArchived') is True
|
|
171
|
+
and execution[0].get('attempt',1)==task.get('attempt',1)
|
|
172
|
+
and execution[0].get('checkpoint')==result.get('checkpoint') and task.get('route'),
|
|
173
|
+
'Existing exact execution result is required; execution cannot replay')
|
|
174
|
+
task.setdefault('reviewHistory',[]).append({'reviewWorkerId':request['reviewWorkerId'],
|
|
175
|
+
'generation':task.get('reviewGeneration',0),'verification':copy.deepcopy(task['verification']),
|
|
176
|
+
'continuation':copy.deepcopy(request)})
|
|
177
|
+
task.update(state='reviewing',phase='review',reviewGeneration=task.get('reviewGeneration',0)+1,
|
|
178
|
+
revision=task['revision']+1,updatedAt=time.time(),
|
|
179
|
+
summary='Review-only continuation authorized; original execution and rejected review retained.')
|
|
180
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
|
|
181
|
+
db.execute('INSERT INTO responses VALUES (?,?,?)',(request['requestId'],fingerprint,task['id']))
|
|
182
|
+
self.event(db,task['id'],'review-continuation-requested',task['summary'])
|
|
183
|
+
return task,True
|
|
184
|
+
|
|
185
|
+
def reserve(self,tid,phase,role,resolution,capacity):
|
|
186
|
+
require(phase in {'route','execute','review'} and type(capacity) is int and 1<=capacity<=16,'Invalid worker reservation')
|
|
187
|
+
with self.db() as db:
|
|
188
|
+
rows=[json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
|
|
189
|
+
require(sum(w['state'] in {'reserved','creating','running','archiving','unknown'} for w in rows)<capacity,'Execution capacity occupied')
|
|
190
|
+
task=json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone()[0]);attempt=task.get('attempt',1)
|
|
191
|
+
require(not any(w['taskId']==tid and w['phase']==phase and w.get('attempt',1)==attempt and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in rows),'A task phase already has a worker; never reuse or replay it')
|
|
192
|
+
wid=str(uuid.uuid4());worker={'id':wid,'taskId':tid,'phase':phase,'attempt':attempt,'role':role,'resolution':copy.deepcopy(resolution),'state':'reserved','nativeId':None,'turnId':None,'createdAt':time.time()}
|
|
193
|
+
if phase=='review':worker['reviewGeneration']=task.get('reviewGeneration',0)
|
|
194
|
+
db.execute('INSERT INTO workers VALUES (?,?,?)',(wid,tid,json.dumps(worker)));self.event(db,tid,'worker-reserved',phase+' worker reserved');return worker
|
|
195
|
+
def worker(self,wid):
|
|
196
|
+
with self.db() as db:
|
|
197
|
+
row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker');return json.loads(row[0])
|
|
198
|
+
def workers(self):
|
|
199
|
+
with self.db() as db:return [json.loads(r[0]) for r in db.execute('SELECT body FROM workers')]
|
|
200
|
+
def worker_update(self,wid,changes):
|
|
201
|
+
require(not(set(changes)-{'state','nativeId','turnId','cwd','checkpoint','error','nativeArchived','roleSource','resolution','nativeRetirement','nativePlan'}),'Unsupported worker transition')
|
|
202
|
+
with self.db() as db:
|
|
203
|
+
row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker');worker=json.loads(row[0]);worker.update(copy.deepcopy(changes));db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),wid));return worker
|
|
204
|
+
def progress(self,wid,item_id,summary):
|
|
205
|
+
require(text(item_id,200) and text(summary,1600),'Invalid worker progress')
|
|
206
|
+
with self.db() as db:
|
|
207
|
+
row=db.execute('SELECT body FROM workers WHERE id=?',(wid,)).fetchone();require(row is not None,'Unknown worker')
|
|
208
|
+
worker=json.loads(row[0])
|
|
209
|
+
if worker['state']!='running' or item_id in worker.get('progressItems',[]):
|
|
210
|
+
return False
|
|
211
|
+
task=json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(worker['taskId'],)).fetchone()[0])
|
|
212
|
+
if task['state'] in TERMINAL:
|
|
213
|
+
return False
|
|
214
|
+
worker.setdefault('progressItems',[]).append(item_id)
|
|
215
|
+
task.update(summary='Reported progress: '+summary,revision=task['revision']+1,updatedAt=time.time())
|
|
216
|
+
db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),wid))
|
|
217
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']))
|
|
218
|
+
self.event(db,task['id'],'worker-progress',task['summary'])
|
|
219
|
+
return True
|
|
220
|
+
def adopt_ready(self,tid,revision,installation):
|
|
221
|
+
require(text(installation,200),'Installation identity required')
|
|
222
|
+
with self.db() as db:
|
|
223
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
|
|
224
|
+
require(task['revision']==revision,'Task changed; reread before checkpoint adoption')
|
|
225
|
+
require(task.get('resumeAfterCheckpoint') is True and task['state'] in {'queued','executing','reviewing'},'Task is held, uncertain or has no checkpoint-resumption authority')
|
|
226
|
+
workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(tid,))]
|
|
227
|
+
require(all(w['state']=='archived' and w.get('nativeArchived') is True or w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True) for w in workers),'Existing worker closure is unresolved; preserve its owner')
|
|
228
|
+
phase={'queued':'route','executing':'execute','reviewing':'review'}[task['state']]
|
|
229
|
+
require(not any(w['phase']==phase and w.get('attempt',1)==task.get('attempt',1) and (phase!='review' or w.get('reviewGeneration',0)==task.get('reviewGeneration',0)) for w in workers),'Existing phase result needs reconciliation, not replay')
|
|
230
|
+
if task.get('ownerInstallationId')==installation:
|
|
231
|
+
return task
|
|
232
|
+
previous=task.get('ownerInstallationId');require(isinstance(previous,str) and previous,'Previous installation ownership is missing')
|
|
233
|
+
task.update(ownerInstallationId=installation,revision=revision+1,updatedAt=time.time(),summary='Verified closed checkpoint adopted; only the pending phase may execute.')
|
|
234
|
+
task.setdefault('ownershipCheckpoints',[]).append({'from':previous,'to':installation,'at':task['updatedAt'],'pendingPhase':phase})
|
|
235
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,'checkpoint-owner-adopted',task['summary']);return task
|
|
236
|
+
def portable_delta(self,tid,revision,source_digest,workers,changes,operation):
|
|
237
|
+
require(set(changes)=={'contextRef','result','verification'} and isinstance(workers,dict) and isinstance(operation,str) and len(operation)==64,'Exact portable delta transition required')
|
|
238
|
+
with self.db() as db:
|
|
239
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(tid,)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
|
|
240
|
+
current=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=? ORDER BY id',(tid,))]
|
|
241
|
+
require(task['state'] in TERMINAL and set(workers)=={w['id'] for w in current} and all(w['state']=='archived' and w.get('nativeArchived') is True for w in current),'Only verified archived terminal task checkpoints may be converted')
|
|
242
|
+
migration={'operationId':operation,'sourceDigest':source_digest,'nativeIdentities':{w['id']:{k:w.get(k) for k in ['nativeId','turnId','phase']} for w in current}}
|
|
243
|
+
if task.get('portableMigration')==migration:
|
|
244
|
+
require(all(task.get(k)==v for k,v in changes.items()) and all(w['checkpoint']==workers[w['id']] for w in current),'Applied portable delta differs');return task
|
|
245
|
+
require(task['revision']==revision and hashlib.sha256(canonical({'task':task,'workers':current})).hexdigest()==source_digest,'Portable delta source changed; no records overwritten')
|
|
246
|
+
require(isinstance(changes['contextRef'].get('artifact'),dict) and all(isinstance(p.get('artifact'),dict) for p in workers.values()),'Verified cloud references are required')
|
|
247
|
+
for worker in current:
|
|
248
|
+
worker['historicalCheckpoint']=worker['checkpoint'];worker['checkpoint']=copy.deepcopy(workers[worker['id']]);db.execute('UPDATE workers SET body=? WHERE id=?',(json.dumps(worker),worker['id']))
|
|
249
|
+
task.update(copy.deepcopy(changes),portableMigration=migration,revision=revision+1,updatedAt=time.time())
|
|
250
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),tid));self.event(db,tid,'portable-delta-bound','Existing closed task checkpoints are verified in cloud storage; no work was replayed.');return task
|
|
251
|
+
def retained_import(self,claims,source):
|
|
252
|
+
with self.db() as db:
|
|
253
|
+
for index,claim in enumerate(claims):
|
|
254
|
+
identity=claim.get('claimId') or 'legacy:'+str(index)+':'+str(claim.get('taskId'))
|
|
255
|
+
value={k:claim.get(k) for k in ['claimId','taskId','ownerRole','ownerThreadId','state','worktree','branch']};value.update(source=source,pointer='/claims/'+str(index),disposition='retained_original_owner_no_dispatch')
|
|
256
|
+
db.execute('INSERT INTO retained VALUES (?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body',(identity,json.dumps(value)))
|
|
257
|
+
return db.execute('SELECT COUNT(*) FROM retained').fetchone()[0]
|
|
258
|
+
def retained(self):
|
|
259
|
+
with self.db() as db:return [json.loads(r[0]) for r in db.execute('SELECT body FROM retained')]
|
|
260
|
+
def retained_page(self,params):
|
|
261
|
+
require(isinstance(params,dict) and not set(params)-{'cursor','limit'},'Unsupported retained inventory fields')
|
|
262
|
+
cursor=params.get('cursor',0);limit=params.get('limit',50)
|
|
263
|
+
require(type(cursor) is int and cursor>=0 and type(limit) is int and 1<=limit<=100,'Invalid retained inventory page')
|
|
264
|
+
with self.db() as db:
|
|
265
|
+
total=db.execute('SELECT COUNT(*) FROM retained').fetchone()[0]
|
|
266
|
+
rows=db.execute('SELECT rowid,body FROM retained WHERE rowid>? ORDER BY rowid LIMIT ?',(cursor,limit+1)).fetchall()
|
|
267
|
+
chosen=[];size=0;last=cursor
|
|
268
|
+
for rowid,body in rows[:limit]:
|
|
269
|
+
value=json.loads(body);count=len(canonical(value))
|
|
270
|
+
if count>800000:
|
|
271
|
+
value={'source':value.get('source'),'pointer':value.get('pointer'),'oversizedRecord':True,'bytes':count,'disposition':'retained_original_owner_no_dispatch'};count=len(canonical(value))
|
|
272
|
+
if size+count>900000 and chosen:break
|
|
273
|
+
chosen.append(value);size+=count;last=rowid
|
|
274
|
+
more=bool(rows and (len(rows)>len(chosen)))
|
|
275
|
+
return {'claims':chosen,'total':total,'nextCursor':last if more else None,'disposition':'inventory_only_no_work_resumed'}
|
|
276
|
+
|
|
277
|
+
def events(self,tid=None):
|
|
278
|
+
with self.db() as db:
|
|
279
|
+
rows=db.execute('SELECT sequence,body FROM events'+(' WHERE task_id=?' if tid else '')+' ORDER BY sequence', (tid,) if tid else ())
|
|
280
|
+
return [{'sequence':r[0],**json.loads(r[1])} for r in rows]
|
|
281
|
+
def recover(self):
|
|
282
|
+
# Native effects may have happened between a send and its receipt. Keep
|
|
283
|
+
# custody and prohibit replay; recovery never starts a thread or turn.
|
|
284
|
+
for worker in self.workers():
|
|
285
|
+
if worker['state'] not in {'archived','failed','unknown'}:
|
|
286
|
+
self.worker_update(worker['id'],{'state':'unknown','error':'Owning connection ended; reconcile native receipt before any replay.'})
|
|
287
|
+
task=self.get(worker['taskId'])
|
|
288
|
+
self.update(task['id'],task['revision'],{'state':'unknown','summary':'Native delivery requires reconciliation; no retry was started.'},'recovery-required')
|
|
289
|
+
|
|
290
|
+
def respond(self,request):
|
|
291
|
+
require(isinstance(request,dict) and set(request)=={'requestId','taskId','revision','intent','note'},'Exact task response required')
|
|
292
|
+
require(text(request['requestId'],100) and text(request['note'],8000) and request['intent'] in {'continue','request_changes','hold'},'Invalid task response')
|
|
293
|
+
fingerprint=hashlib.sha256(canonical(request)).hexdigest()
|
|
294
|
+
with self.db() as db:
|
|
295
|
+
previous=db.execute('SELECT input_hash,task_id FROM responses WHERE id=?',(request['requestId'],)).fetchone()
|
|
296
|
+
if previous:
|
|
297
|
+
require(previous==(fingerprint,request['taskId']),'Response identity already used')
|
|
298
|
+
return json.loads(db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone()[0]),False
|
|
299
|
+
row=db.execute('SELECT body FROM tasks WHERE id=?',(request['taskId'],)).fetchone();require(row is not None,'Unknown task');task=json.loads(row[0])
|
|
300
|
+
require(task['revision']==request['revision'],'Task changed; review current progress before responding')
|
|
301
|
+
require(task['state'] in TERMINAL and task['state']!='unknown','Task is active or delivery is uncertain; reconcile before a new attempt')
|
|
302
|
+
workers=[json.loads(w[0]) for w in db.execute('SELECT body FROM workers WHERE task_id=?',(task['id'],))]
|
|
303
|
+
require(all(w['state']=='archived' or (w['state']=='failed' and not w.get('turnId') and (not w.get('nativeId') or w.get('nativeArchived') is True)) for w in workers),'Native worker closure is unresolved; no new attempt')
|
|
304
|
+
task.setdefault('humanResponses',[]).append(copy.deepcopy(request));task['revision']+=1;task['updatedAt']=time.time()
|
|
305
|
+
if request['intent']=='hold':task.update(state='blocked',summary='Held by your request: '+request['note'])
|
|
306
|
+
else:task.update(state='queued',phase='route',attempt=task.get('attempt',1)+1,objective=task['objective']+'\n\nCurrent human response: '+request['note'],summary='Your response is recorded; a fresh specialist attempt will be routed.')
|
|
307
|
+
db.execute('UPDATE tasks SET body=? WHERE id=?',(json.dumps(task),task['id']));db.execute('INSERT INTO responses VALUES (?,?,?)',(request['requestId'],fingerprint,task['id']));self.event(db,task['id'],'human-response',task['summary']);return task,True
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def parse_route(text_value,roles):
|
|
311
|
+
raw=text_value.strip()
|
|
312
|
+
if raw.startswith('```'):raw=re.sub(r'^```(?:json)?\s*|\s*```$','',raw)
|
|
313
|
+
try:value=json.loads(raw)
|
|
314
|
+
except (TypeError,ValueError):raise PackError('Coordinator did not return a valid route; no worker started')
|
|
315
|
+
require(isinstance(value,dict) and set(value)=={'role','task','acceptance'},'Coordinator route has unsupported fields')
|
|
316
|
+
require(value['role'] in roles,'Coordinator selected an unknown discipline')
|
|
317
|
+
if isinstance(value['acceptance'],list):
|
|
318
|
+
require(1<=len(value['acceptance'])<=30 and all(text(v,4000) for v in value['acceptance']),'Coordinator acceptance criteria are invalid')
|
|
319
|
+
value['acceptance']='\n'.join(value['acceptance'])
|
|
320
|
+
require(text(value['acceptance'],12000),'Coordinator returned no bounded acceptance criteria')
|
|
321
|
+
require(isinstance(value['task'],dict) and set(value['task'])=={'type','complexity','risk'},'Coordinator must classify type, complexity and risk')
|
|
322
|
+
return value
|